# HG changeset patch # User Dennis C. M. # Date 1668072448 -3600 # Node ID f5a2c2dab2086df729e80ffb20d1e871b437037c # Parent f51b70c2cccc44cbb2a61687ccddbb9cdb915cd9 fix files structure diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz.xcodeproj/project.pbxproj --- a/GeoQuiz.xcodeproj/project.pbxproj Thu Nov 10 10:12:58 2022 +0100 +++ b/GeoQuiz.xcodeproj/project.pbxproj Thu Nov 10 10:27:28 2022 +0100 @@ -13,11 +13,9 @@ 950C535328F2FA3300179C78 /* BuyPremiumModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950C535228F2FA3300179C78 /* BuyPremiumModalView.swift */; }; 950C535628F3172C00179C78 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 950C535528F3172C00179C78 /* RevenueCat */; }; 950C535928F3178B00179C78 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 950C535828F3178B00179C78 /* StoreKit.framework */; }; - 95197EFD28F339AE00FE67E9 /* StoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95197EFC28F339AE00FE67E9 /* StoreController.swift */; }; 951AFAEA28E5655C00A4A4BD /* cities.json in Resources */ = {isa = PBXBuildFile; fileRef = 951AFAE828E5655C00A4A4BD /* cities.json */; }; 951AFAED28E5657500A4A4BD /* CityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951AFAEC28E5657500A4A4BD /* CityModel.swift */; }; 951AFAEF28E565FE00A4A4BD /* CountryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951AFAEE28E565FE00A4A4BD /* CountryModel.swift */; }; - 951AFAF128E5735400A4A4BD /* CityGameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951AFAF028E5735400A4A4BD /* CityGameController.swift */; }; 951B630228D1A87C004F9877 /* GuessTheCapitalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951B630128D1A87C004F9877 /* GuessTheCapitalView.swift */; }; 951DCE92291A424900BAE20C /* mc.png in Resources */ = {isa = PBXBuildFile; fileRef = 951DCDBA291A420C00BAE20C /* mc.png */; }; 951DCE93291A424900BAE20C /* gr.png in Resources */ = {isa = PBXBuildFile; fileRef = 951DCDBB291A420C00BAE20C /* gr.png */; }; @@ -235,6 +233,16 @@ 951DCF68291A424A00BAE20C /* ve.png in Resources */ = {isa = PBXBuildFile; fileRef = 951DCE90291A424900BAE20C /* ve.png */; }; 951DCF69291A424A00BAE20C /* zm.png in Resources */ = {isa = PBXBuildFile; fileRef = 951DCE91291A424900BAE20C /* zm.png */; }; 951DCF6B291A6B3700BAE20C /* np.png in Resources */ = {isa = PBXBuildFile; fileRef = 951DCF6A291A6B3700BAE20C /* np.png */; }; + 951F0C07291CFADF0026D5A2 /* GameProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0BFD291CFADF0026D5A2 /* GameProtocol+Extension.swift */; }; + 951F0C08291CFADF0026D5A2 /* CoreDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0BFE291CFADF0026D5A2 /* CoreDataController.swift */; }; + 951F0C09291CFADF0026D5A2 /* MapController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0BFF291CFADF0026D5A2 /* MapController.swift */; }; + 951F0C0A291CFADF0026D5A2 /* GameViewProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0C00291CFADF0026D5A2 /* GameViewProtocol+Extension.swift */; }; + 951F0C0B291CFADF0026D5A2 /* HapticsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0C01291CFADF0026D5A2 /* HapticsController.swift */; }; + 951F0C0C291CFADF0026D5A2 /* StoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0C02291CFADF0026D5A2 /* StoreController.swift */; }; + 951F0C0D291CFADF0026D5A2 /* CountryGameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0C03291CFADF0026D5A2 /* CountryGameController.swift */; }; + 951F0C0E291CFADF0026D5A2 /* CityGameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0C04291CFADF0026D5A2 /* CityGameController.swift */; }; + 951F0C0F291CFADF0026D5A2 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0C05291CFADF0026D5A2 /* PersistenceController.swift */; }; + 951F0C10291CFADF0026D5A2 /* UserController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F0C06291CFADF0026D5A2 /* UserController.swift */; }; 952E41E928DC521200198643 /* GameAlertsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952E41E828DC521200198643 /* GameAlertsModifier.swift */; }; 952E41ED28DC658900198643 /* SettingsModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952E41EC28DC658900198643 /* SettingsModalView.swift */; }; 952E41F228DC6F6E00198643 /* correctAnswer.wav in Resources */ = {isa = PBXBuildFile; fileRef = 952E41F028DC6F6D00198643 /* correctAnswer.wav */; }; @@ -244,13 +252,9 @@ 9539829A28C51EDF00B70973 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9539829928C51EDF00B70973 /* Preview Assets.xcassets */; }; 954AF4682905397A00180065 /* PlayedGamesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954AF4672905397A00180065 /* PlayedGamesList.swift */; }; 955A658128D703EB00CEEC6D /* GameToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A658028D703EB00CEEC6D /* GameToolbar.swift */; }; - 955A658328D733E400CEEC6D /* GameProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A658228D733E400CEEC6D /* GameProtocol+Extension.swift */; }; - 955A65A928D7815E00CEEC6D /* HapticsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A65A828D7815E00CEEC6D /* HapticsController.swift */; }; 957822482918F445005F2D50 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957822472918F445005F2D50 /* Extensions.swift */; }; 9590359528E098FF00B24560 /* ProfileModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9590359428E098FF00B24560 /* ProfileModalView.swift */; }; - 95919DB628F076BF00F21F8F /* UserController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95919DB528F076BF00F21F8F /* UserController.swift */; }; 95919DBC28F08D0600F21F8F /* SettingsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95919DBB28F08D0600F21F8F /* SettingsRow.swift */; }; - 95A4F42929040E350018DFAC /* CoreDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A4F42829040E350018DFAC /* CoreDataController.swift */; }; 95A4F42B29043DC00018DFAC /* UserImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A4F42A29043DC00018DFAC /* UserImage.swift */; }; 95AF322A28DF293900023ACC /* GuessTheCountryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AF322928DF293900023ACC /* GuessTheCountryView.swift */; }; 95BC392D28EC42570049AB49 /* CityMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BC392C28EC42570049AB49 /* CityMap.swift */; }; @@ -259,20 +263,16 @@ 95C6456C28FE87E4000CD570 /* UserDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6456B28FE87E4000CD570 /* UserDataModel.swift */; }; 95C6456E28FE8C04000CD570 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6456D28FE8C04000CD570 /* UserProfile.swift */; }; 95C6457228FFC4DC000CD570 /* ProfileEditModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */; }; - 95C6457428FFC8E0000CD570 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6457328FFC8E0000CD570 /* PersistenceController.swift */; }; 95C6457728FFC934000CD570 /* GeoQuiz.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 95C6457528FFC934000CD570 /* GeoQuiz.xcdatamodeld */; }; 95C6459A28FFE5A3000CD570 /* PlayedGame+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6459828FFE5A3000CD570 /* PlayedGame+CoreDataClass.swift */; }; 95C6459B28FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6459928FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift */; }; 95C6459D290003E1000CD570 /* RecentGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6459C290003E1000CD570 /* RecentGame.swift */; }; 95CA295028F6BB4500CE0B7A /* ActivityAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CA294F28F6BB4500CE0B7A /* ActivityAlert.swift */; }; 95D8BF32291BAF8C006FC606 /* SettingsModalView-ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8BF31291BAF8C006FC606 /* SettingsModalView-ViewModel.swift */; }; - 95D8BF36291BB1F7006FC606 /* GameViewProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8BF35291BB1F7006FC606 /* GameViewProtocol+Extension.swift */; }; 95D8BF38291BBB3D006FC606 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95D8BF37291BBB3D006FC606 /* LaunchScreen.storyboard */; }; 95D8BF3A291BC5DA006FC606 /* GuessTheFlagView-ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8BF39291BC5DA006FC606 /* GuessTheFlagView-ViewModel.swift */; }; 95DB7C01290492FC007D01D8 /* GameInfoModel+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DB7C00290492FC007D01D8 /* GameInfoModel+Protocol.swift */; }; - 95DB7C032904A968007D01D8 /* MapController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DB7C022904A968007D01D8 /* MapController.swift */; }; 95FA409A28D9876B00129B60 /* GuessTheFlagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FA409928D9876B00129B60 /* GuessTheFlagView.swift */; }; - 95FA409C28D9881100129B60 /* CountryGameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FA409B28D9881100129B60 /* CountryGameController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -281,11 +281,9 @@ 9509A8E128E5A3D700CFCDBA /* GuessThePopulationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessThePopulationView.swift; sourceTree = ""; }; 950C535228F2FA3300179C78 /* BuyPremiumModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyPremiumModalView.swift; sourceTree = ""; }; 950C535828F3178B00179C78 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; - 95197EFC28F339AE00FE67E9 /* StoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreController.swift; sourceTree = ""; }; 951AFAE828E5655C00A4A4BD /* cities.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = cities.json; sourceTree = ""; }; 951AFAEC28E5657500A4A4BD /* CityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityModel.swift; sourceTree = ""; }; 951AFAEE28E565FE00A4A4BD /* CountryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryModel.swift; sourceTree = ""; }; - 951AFAF028E5735400A4A4BD /* CityGameController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityGameController.swift; sourceTree = ""; }; 951B630128D1A87C004F9877 /* GuessTheCapitalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheCapitalView.swift; sourceTree = ""; }; 951DCDBA291A420C00BAE20C /* mc.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mc.png; sourceTree = ""; }; 951DCDBB291A420C00BAE20C /* gr.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gr.png; sourceTree = ""; }; @@ -503,6 +501,16 @@ 951DCE90291A424900BAE20C /* ve.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ve.png; sourceTree = ""; }; 951DCE91291A424900BAE20C /* zm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = zm.png; sourceTree = ""; }; 951DCF6A291A6B3700BAE20C /* np.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = np.png; sourceTree = ""; }; + 951F0BFD291CFADF0026D5A2 /* GameProtocol+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GameProtocol+Extension.swift"; sourceTree = ""; }; + 951F0BFE291CFADF0026D5A2 /* CoreDataController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataController.swift; sourceTree = ""; }; + 951F0BFF291CFADF0026D5A2 /* MapController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapController.swift; sourceTree = ""; }; + 951F0C00291CFADF0026D5A2 /* GameViewProtocol+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GameViewProtocol+Extension.swift"; sourceTree = ""; }; + 951F0C01291CFADF0026D5A2 /* HapticsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticsController.swift; sourceTree = ""; }; + 951F0C02291CFADF0026D5A2 /* StoreController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreController.swift; sourceTree = ""; }; + 951F0C03291CFADF0026D5A2 /* CountryGameController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryGameController.swift; sourceTree = ""; }; + 951F0C04291CFADF0026D5A2 /* CityGameController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CityGameController.swift; sourceTree = ""; }; + 951F0C05291CFADF0026D5A2 /* PersistenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; + 951F0C06291CFADF0026D5A2 /* UserController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserController.swift; sourceTree = ""; }; 952E41E828DC521200198643 /* GameAlertsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameAlertsModifier.swift; sourceTree = ""; }; 952E41EC28DC658900198643 /* SettingsModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModalView.swift; sourceTree = ""; }; 952E41F028DC6F6D00198643 /* correctAnswer.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = correctAnswer.wav; sourceTree = ""; }; @@ -514,13 +522,9 @@ 954AF4672905397A00180065 /* PlayedGamesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayedGamesList.swift; sourceTree = ""; }; 954AF46B2905433300180065 /* GeoQuiz.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GeoQuiz.entitlements; sourceTree = ""; }; 955A658028D703EB00CEEC6D /* GameToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameToolbar.swift; sourceTree = ""; }; - 955A658228D733E400CEEC6D /* GameProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameProtocol+Extension.swift"; sourceTree = ""; }; - 955A65A828D7815E00CEEC6D /* HapticsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsController.swift; sourceTree = ""; }; 957822472918F445005F2D50 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 9590359428E098FF00B24560 /* ProfileModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModalView.swift; sourceTree = ""; }; - 95919DB528F076BF00F21F8F /* UserController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserController.swift; sourceTree = ""; }; 95919DBB28F08D0600F21F8F /* SettingsRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRow.swift; sourceTree = ""; }; - 95A4F42829040E350018DFAC /* CoreDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataController.swift; sourceTree = ""; }; 95A4F42A29043DC00018DFAC /* UserImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserImage.swift; sourceTree = ""; }; 95AF322928DF293900023ACC /* GuessTheCountryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheCountryView.swift; sourceTree = ""; }; 95BC392C28EC42570049AB49 /* CityMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityMap.swift; sourceTree = ""; }; @@ -529,21 +533,17 @@ 95C6456B28FE87E4000CD570 /* UserDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataModel.swift; sourceTree = ""; }; 95C6456D28FE8C04000CD570 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditModalView.swift; sourceTree = ""; }; - 95C6457328FFC8E0000CD570 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 95C6457628FFC934000CD570 /* GeoQuiz.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = GeoQuiz.xcdatamodel; sourceTree = ""; }; 95C6459828FFE5A3000CD570 /* PlayedGame+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayedGame+CoreDataClass.swift"; sourceTree = ""; }; 95C6459928FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayedGame+CoreDataProperties.swift"; sourceTree = ""; }; 95C6459C290003E1000CD570 /* RecentGame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentGame.swift; sourceTree = ""; }; 95CA294F28F6BB4500CE0B7A /* ActivityAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAlert.swift; sourceTree = ""; }; 95D8BF31291BAF8C006FC606 /* SettingsModalView-ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsModalView-ViewModel.swift"; sourceTree = ""; }; - 95D8BF35291BB1F7006FC606 /* GameViewProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewProtocol+Extension.swift"; sourceTree = ""; }; 95D8BF37291BBB3D006FC606 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 95D8BF39291BC5DA006FC606 /* GuessTheFlagView-ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GuessTheFlagView-ViewModel.swift"; sourceTree = ""; }; 95DB7C00290492FC007D01D8 /* GameInfoModel+Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameInfoModel+Protocol.swift"; sourceTree = ""; }; - 95DB7C022904A968007D01D8 /* MapController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapController.swift; sourceTree = ""; }; 95E6188428DDDB5C003359ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 95FA409928D9876B00129B60 /* GuessTheFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheFlagView.swift; sourceTree = ""; }; - 95FA409B28D9881100129B60 /* CountryGameController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryGameController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -790,6 +790,23 @@ path = Flags; sourceTree = ""; }; + 951F0BFC291CFADF0026D5A2 /* Controllers */ = { + isa = PBXGroup; + children = ( + 951F0BFD291CFADF0026D5A2 /* GameProtocol+Extension.swift */, + 951F0BFE291CFADF0026D5A2 /* CoreDataController.swift */, + 951F0BFF291CFADF0026D5A2 /* MapController.swift */, + 951F0C00291CFADF0026D5A2 /* GameViewProtocol+Extension.swift */, + 951F0C01291CFADF0026D5A2 /* HapticsController.swift */, + 951F0C02291CFADF0026D5A2 /* StoreController.swift */, + 951F0C03291CFADF0026D5A2 /* CountryGameController.swift */, + 951F0C04291CFADF0026D5A2 /* CityGameController.swift */, + 951F0C05291CFADF0026D5A2 /* PersistenceController.swift */, + 951F0C06291CFADF0026D5A2 /* UserController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; 9520ABBA28C86D0300A3D4D7 /* Resources */ = { isa = PBXGroup; children = ( @@ -841,7 +858,7 @@ 95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */, 957822462918EED3005F2D50 /* Helpers */, 957822452918EECA005F2D50 /* Models */, - 959303AC2918F58F00E3E099 /* Controllers */, + 951F0BFC291CFADF0026D5A2 /* Controllers */, 9520ABBA28C86D0300A3D4D7 /* Resources */, 9539829828C51EDF00B70973 /* Preview Content */, ); @@ -888,24 +905,6 @@ path = Helpers; sourceTree = ""; }; - 959303AC2918F58F00E3E099 /* Controllers */ = { - isa = PBXGroup; - children = ( - 95D8BF35291BB1F7006FC606 /* GameViewProtocol+Extension.swift */, - 955A658228D733E400CEEC6D /* GameProtocol+Extension.swift */, - 95FA409B28D9881100129B60 /* CountryGameController.swift */, - 951AFAF028E5735400A4A4BD /* CityGameController.swift */, - 955A65A828D7815E00CEEC6D /* HapticsController.swift */, - 95919DB528F076BF00F21F8F /* UserController.swift */, - 95197EFC28F339AE00FE67E9 /* StoreController.swift */, - 95A4F42829040E350018DFAC /* CoreDataController.swift */, - 95C6457328FFC8E0000CD570 /* PersistenceController.swift */, - 95DB7C022904A968007D01D8 /* MapController.swift */, - ); - name = Controllers; - path = Models/Controllers; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1203,47 +1202,47 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 955A65A928D7815E00CEEC6D /* HapticsController.swift in Sources */, 95BC392D28EC42570049AB49 /* CityMap.swift in Sources */, 95C6459A28FFE5A3000CD570 /* PlayedGame+CoreDataClass.swift in Sources */, 95A4F42B29043DC00018DFAC /* UserImage.swift in Sources */, 952E41E928DC521200198643 /* GameAlertsModifier.swift in Sources */, - 95197EFD28F339AE00FE67E9 /* StoreController.swift in Sources */, 9509A8E228E5A3D700CFCDBA /* GuessThePopulationView.swift in Sources */, - 955A658328D733E400CEEC6D /* GameProtocol+Extension.swift in Sources */, - 95A4F42929040E350018DFAC /* CoreDataController.swift in Sources */, - 95C6457428FFC8E0000CD570 /* PersistenceController.swift in Sources */, - 95919DB628F076BF00F21F8F /* UserController.swift in Sources */, 95C6459D290003E1000CD570 /* RecentGame.swift in Sources */, 95C6456E28FE8C04000CD570 /* UserProfile.swift in Sources */, 95C4315628C64A8C00212131 /* ContentView.swift in Sources */, 954AF4682905397A00180065 /* PlayedGamesList.swift in Sources */, 95C4315928C6500000212131 /* GameButton.swift in Sources */, 951AFAED28E5657500A4A4BD /* CityModel.swift in Sources */, + 951F0C07291CFADF0026D5A2 /* GameProtocol+Extension.swift in Sources */, + 951F0C0D291CFADF0026D5A2 /* CountryGameController.swift in Sources */, 950C535328F2FA3300179C78 /* BuyPremiumModalView.swift in Sources */, 951B630228D1A87C004F9877 /* GuessTheCapitalView.swift in Sources */, + 951F0C10291CFADF0026D5A2 /* UserController.swift in Sources */, + 951F0C09291CFADF0026D5A2 /* MapController.swift in Sources */, 9539829328C51EDE00B70973 /* GeoQuizApp.swift in Sources */, + 951F0C0A291CFADF0026D5A2 /* GameViewProtocol+Extension.swift in Sources */, 95AF322A28DF293900023ACC /* GuessTheCountryView.swift in Sources */, - 95DB7C032904A968007D01D8 /* MapController.swift in Sources */, + 951F0C0E291CFADF0026D5A2 /* CityGameController.swift in Sources */, 95919DBC28F08D0600F21F8F /* SettingsRow.swift in Sources */, + 951F0C0C291CFADF0026D5A2 /* StoreController.swift in Sources */, + 951F0C08291CFADF0026D5A2 /* CoreDataController.swift in Sources */, 951AFAEF28E565FE00A4A4BD /* CountryModel.swift in Sources */, 95030CEA28D1BA4D001AA3A1 /* AnswerButton.swift in Sources */, - 95FA409C28D9881100129B60 /* CountryGameController.swift in Sources */, 95CA295028F6BB4500CE0B7A /* ActivityAlert.swift in Sources */, 95D8BF32291BAF8C006FC606 /* SettingsModalView-ViewModel.swift in Sources */, 95D8BF3A291BC5DA006FC606 /* GuessTheFlagView-ViewModel.swift in Sources */, 955A658128D703EB00CEEC6D /* GameToolbar.swift in Sources */, 957822482918F445005F2D50 /* Extensions.swift in Sources */, 95C6457728FFC934000CD570 /* GeoQuiz.xcdatamodeld in Sources */, - 95D8BF36291BB1F7006FC606 /* GameViewProtocol+Extension.swift in Sources */, 9590359528E098FF00B24560 /* ProfileModalView.swift in Sources */, 95C6456C28FE87E4000CD570 /* UserDataModel.swift in Sources */, + 951F0C0B291CFADF0026D5A2 /* HapticsController.swift in Sources */, 95C6459B28FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift in Sources */, 95C6457228FFC4DC000CD570 /* ProfileEditModalView.swift in Sources */, 952E41ED28DC658900198643 /* SettingsModalView.swift in Sources */, 95FA409A28D9876B00129B60 /* GuessTheFlagView.swift in Sources */, 95DB7C01290492FC007D01D8 /* GameInfoModel+Protocol.swift in Sources */, - 951AFAF128E5735400A4A4BD /* CityGameController.swift in Sources */, + 951F0C0F291CFADF0026D5A2 /* PersistenceController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/CityGameController.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/CityGameController.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,103 @@ +// +// CityGameController.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 29/9/22. +// + +import Foundation +import AVFAudio + +class CityGameController: Game, ObservableObject { + + // Define type of generics + typealias T = CityModel.City + + var data: [String: T] + var dataAsked = [String: T]() + + // Data + @Published var correctAnswer = ( + key: String(), + value: T(country: String(), lat: Double(), lon: Double()) + ) + + // User + @Published var userChoices = [String: T]() + @Published var userScore = 0 + @Published var userLives = 3 + @Published var correctAnswers = [String: T]() + @Published var wrongAnswers = [String: T]() + + // Alerts + @Published var alertTitle = String() + @Published var alertMessage = String() + @Published var showingEndGameAlert = false + @Published var showingWrongAnswerAlert = false + @Published var showingExitGameAlert = false + + // Animations + @Published var scoreScaleAmount = 1.0 + @Published var livesScaleAmount = 1.0 + + // Sound effects + @Published var player: AVAudioPlayer? + + init() { + let data: CityModel = Bundle.main.decode("cities.json") + self.data = data.cities + + let user = UserController() + userLives = user.data.numberOfLives + + if let userData = UserDefaults.standard.data(forKey: "UserData") { + if let decodedUserData = try? JSONDecoder().decode(UserDataModel.self, from: userData) { + userLives = decodedUserData.numberOfLives + } + } + + askQuestion { + selector() + } + } +} + +extension CityGameController { + func selector() { + + // Get random choices + var userChoices = [String: T]() + + while userChoices.count < 2 { + if let choice = data.randomElement() { + let userChoicesCountry = userChoices.map { $0.value.country } + + if !userChoicesCountry.contains(choice.value.country) { + userChoices[choice.key] = choice.value + } + } else { + fatalError("Couldn't get a random value from data") + } + } + + // Get question asked (correct answer) + let userChoicesCountry = userChoices.map { $0.value.country } + let correctAnswer = data.first(where: { + !userChoices.keys.contains($0.key) && // Avoid duplicated cities + !dataAsked.keys.contains($0.key) && // Avoid cities already asked + !userChoicesCountry.contains($0.value.country) // Avoid duplicated country names in userChoices + }) + + // Unwrap optional + if let correctAnswer = correctAnswer { + userChoices[correctAnswer.key] = correctAnswer.value + dataAsked[correctAnswer.key] = correctAnswer.value + self.correctAnswer = correctAnswer + } else { + fatalError("Couldn't unwrap optional value") + } + + self.userChoices = userChoices + } +} + diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/CoreDataController.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/CoreDataController.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,21 @@ +// +// CoreDataController.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 22/10/22. +// + +import Foundation +import SwiftUI +import CoreData + +class CoreDataController { + static func deleteGame(at offsets: IndexSet, from games: FetchedResults, with moc: NSManagedObjectContext) { + for offset in offsets { + let game = games[offset] + moc.delete(game) + } + + try? moc.save() + } +} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/CountryGameController.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/CountryGameController.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,103 @@ +// +// CountryGameController.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 20/9/22. +// + +import Foundation +import AVFAudio + +class CountryGameController: Game, ObservableObject { + + // Define type of generics + typealias T = CountryModel.Country + + var data: [String: T] + var dataAsked = [String: T]() + + // Data + @Published var correctAnswer = ( + key: String(), + value: T(flag: String(), currency: String(), population: Int(), capital: String()) + ) + + // User + @Published var userChoices = [String: T]() + @Published var userScore = 0 + @Published var userLives = 3 + @Published var correctAnswers = [String: T]() + @Published var wrongAnswers = [String: T]() + + // Alerts + @Published var alertTitle = String() + @Published var alertMessage = String() + @Published var showingEndGameAlert = false + @Published var showingWrongAnswerAlert = false + @Published var showingExitGameAlert = false + + // Animations + @Published var scoreScaleAmount = 1.0 + @Published var livesScaleAmount = 1.0 + + // Sound effects + @Published var player: AVAudioPlayer? + + init() { + let data: CountryModel = Bundle.main.decode("countries.json") + self.data = data.countries + + let user = UserController() + userLives = user.data.numberOfLives + + if let userData = UserDefaults.standard.data(forKey: "UserData") { + if let decodedUserData = try? JSONDecoder().decode(UserDataModel.self, from: userData) { + userLives = decodedUserData.numberOfLives + } + } + + askQuestion { + selector() + } + } +} + +extension CountryGameController { + func selector() { + + // Get random choices + var userChoices = [String: T]() + + while userChoices.count < 2 { + if let choice = data.randomElement() { + userChoices[choice.key] = choice.value + } else { + fatalError("Couldn't get a random value from data") + } + } + + // Get correct answer + let randomCountryKeys = data.keys.shuffled() + + let correctCountryKey = randomCountryKeys.first(where: { + !userChoices.keys.contains($0) && + !dataAsked.keys.contains($0) + + }) + + // Unwrap correct answer + if let correctCountryKey = correctCountryKey { + let correctCountryValue = data[correctCountryKey]! + + userChoices[correctCountryKey] = correctCountryValue + dataAsked[correctCountryKey] = correctCountryValue + + let correctAnswer = (key: correctCountryKey, value: correctCountryValue) + self.correctAnswer = correctAnswer + } else { + fatalError("Couldn't unwrap optional value") + } + + self.userChoices = userChoices + } +} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/GameProtocol+Extension.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/GameProtocol+Extension.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,157 @@ +// +// GameProtocol+Extension.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 18/9/22. +// + +import Foundation +import SwiftUI +import AVFAudio +import CoreData + +@objc +public enum GameType: Int16, CaseIterable { + case guessTheFlag + case guessTheCapital + case guessTheCountry + case guessThePopulation +} + +protocol Game: ObservableObject { + + // Define generic type + associatedtype T: Equatable + + // Game + var data: [String: T] { get set} + var dataAsked: [String: T] { get set } + var correctAnswer: (key: String, value: T) { get set } + + // User + var userChoices: [String: T] { get set } + var userScore: Int { get set } + var userLives: Int { get set } + var correctAnswers: [String: T] { get set } + var wrongAnswers: [String: T] { get set } + + // Alerts + var alertTitle: String { get set } + var alertMessage: String { get set } + var showingEndGameAlert: Bool { get set } + var showingWrongAnswerAlert: Bool { get set } + var showingExitGameAlert: Bool { get set } + + // Animations + var scoreScaleAmount: Double { get set } + var livesScaleAmount: Double { get set } + + // Sound effects + var player: AVAudioPlayer? { get set } + + func selector() +} + +extension Game { + var questionCounter: Int { + dataAsked.count + } + + func askQuestion(selector: () -> Void) { + guard questionCounter < data.count else { + alertTitle = "⭐️ Congratulations ⭐️" + alertMessage = "You completed the game." + showingEndGameAlert = true + + return + } + + selector() + } + + func answer(choice: (key: String, value: T), wrongMessage: String, selector: () -> Void) { + let haptics = HapticsController() + + if correctAnswer == choice { + haptics.success() + playSound("correctAnswer") + + withAnimation(.easeIn(duration: 0.5)) { + scoreScaleAmount += 1 + userScore += 1 + } + + correctAnswers[correctAnswer.key] = correctAnswer.value + askQuestion { + selector() + } + } else { + haptics.error() + playSound("wrongAnswer") + + withAnimation(.easeIn(duration: 0.5)) { + livesScaleAmount += 1 + userLives -= 1 + } + + wrongAnswers[choice.key] = choice.value + + if userLives == 0 { + alertTitle = "🤕 Game over 🤕" + alertMessage = "Get up and try again." + showingEndGameAlert = true + } else { + alertTitle = "🔴 Wrong 🔴" + alertMessage = "\(wrongMessage). You have \(userLives) lives left." + showingWrongAnswerAlert = true + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [self] in + withAnimation(.easeIn(duration: 0.5)) { + scoreScaleAmount = 1 + livesScaleAmount = 1 + } + } + } + + func save(_ gameType: GameType, with moc: NSManagedObjectContext) { + let playedGame = PlayedGame(context: moc) + + playedGame.type = gameType + playedGame.date = Date() + playedGame.score = Int32(userScore) + playedGame.correctAnswers = Array(correctAnswers.keys) + playedGame.wrongAnswers = Array(wrongAnswers.keys) + + do { + try moc.save() + } catch { + print("Couldn't save object to CoreData: \(error)") + } + } + + private func playSound(_ filename: String) { + let user = UserController() + + if user.data.sound { + guard let soundFileURL = Bundle.main.url(forResource: filename, withExtension: "wav") else { + fatalError("Sound file \(filename) couldn't be found") + } + + do { + try AVAudioSession.sharedInstance().setCategory(.ambient) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + fatalError("Couldn't activate session") + } + + do { + player = try AVAudioPlayer(contentsOf: soundFileURL) + player?.play() + } catch { + fatalError("Couldn't play sound effect") + } + } + } +} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/GameViewProtocol+Extension.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/GameViewProtocol+Extension.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,19 @@ +// +// GameViewProtocol+Extension.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 9/11/22. +// + +import Foundation + +protocol GameView { + +} + +extension GameView { + func getFlagPath(forName flagName: String) -> String { + return Bundle.main.path(forResource: flagName, ofType: "png")! + } + +} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/HapticsController.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/HapticsController.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,27 @@ +// +// HapticsController.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 18/9/22. +// + +import Foundation +import SwiftUI + +class HapticsController { + private var user = UserController() + + func success() { + if user.data.haptics { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + } + } + + func error() { + if user.data.haptics { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.error) + } + } +} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/MapController.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/MapController.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,36 @@ +// +// MapController.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 22/10/22. +// + +import Foundation +import MapKit + +class MapController: ObservableObject { + @Published var image: UIImage? = nil + + func getMapImage(lat: Double, lon: Double) { + let region = MKCoordinateRegion( + center: CLLocationCoordinate2D(latitude: lat, longitude: lon), + span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) + ) + + // Map options + let mapOptions = MKMapSnapshotter.Options() + mapOptions.region = region + mapOptions.size = CGSize(width: 500, height: 500) + mapOptions.pointOfInterestFilter = .excludingAll + + // Create the snapshotter and run it + let snapshotter = MKMapSnapshotter(options: mapOptions) + snapshotter.start { (snapshot, error) in + if let snapshot = snapshot { + self.image = snapshot.image + } else if let error = error { + print(error.localizedDescription) + } + } + } +} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Controllers/PersistenceController.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Controllers/PersistenceController.swift Thu Nov 10 10:27:28 2022 +0100 @@ -0,0 +1,70 @@ +// +// PersistenceController.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 19/10/22. +// + +import CoreData +import SwiftUI + +class PersistenceController { + static let shared = PersistenceController() + + let container: NSPersistentCloudKitContainer + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + + #if DEBUG + createMockData(with: viewContext) + #endif + + return result + }() + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "GeoQuiz") + + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + + container.viewContext.automaticallyMergesChangesFromParent = true + } + + #if DEBUG + static func createMockData(with moc: NSManagedObjectContext) { + for _ in 0..<10 { + let playedGame = PlayedGame(context: moc) + + playedGame.type = GameType(rawValue: Int16.random(in: 0...3))! + playedGame.score = Int32.random(in: 0...50) + + let dayComponent = DateComponents(day: Int.random(in: -5...0)) + playedGame.date = Calendar.current.date(byAdding: dayComponent, to: Date()) + + if playedGame.type == .guessTheFlag || playedGame.type == .guessTheCapital { + playedGame.correctAnswers = ["Bangladesh", "Belgium", "Burkina Faso", "Bermuda", "Jamaica"] + playedGame.wrongAnswers = ["Belarus", "Russia"] + } else { + playedGame.correctAnswers = ["Herat", "Lobito", "Darregueira", "San Juan"] + playedGame.wrongAnswers = ["San Luis", "Oranjestad"] + } + } + do { + try moc.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + #endif +} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/CityGameController.swift --- a/GeoQuiz/Models/Controllers/CityGameController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,103 +0,0 @@ -// -// CityGameController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 29/9/22. -// - -import Foundation -import AVFAudio - -class CityGameController: Game, ObservableObject { - - // Define type of generics - typealias T = CityModel.City - - var data: [String: T] - var dataAsked = [String: T]() - - // Data - @Published var correctAnswer = ( - key: String(), - value: T(country: String(), lat: Double(), lon: Double()) - ) - - // User - @Published var userChoices = [String: T]() - @Published var userScore = 0 - @Published var userLives = 3 - @Published var correctAnswers = [String: T]() - @Published var wrongAnswers = [String: T]() - - // Alerts - @Published var alertTitle = String() - @Published var alertMessage = String() - @Published var showingEndGameAlert = false - @Published var showingWrongAnswerAlert = false - @Published var showingExitGameAlert = false - - // Animations - @Published var scoreScaleAmount = 1.0 - @Published var livesScaleAmount = 1.0 - - // Sound effects - @Published var player: AVAudioPlayer? - - init() { - let data: CityModel = Bundle.main.decode("cities.json") - self.data = data.cities - - let user = UserController() - userLives = user.data.numberOfLives - - if let userData = UserDefaults.standard.data(forKey: "UserData") { - if let decodedUserData = try? JSONDecoder().decode(UserDataModel.self, from: userData) { - userLives = decodedUserData.numberOfLives - } - } - - askQuestion { - selector() - } - } -} - -extension CityGameController { - func selector() { - - // Get random choices - var userChoices = [String: T]() - - while userChoices.count < 2 { - if let choice = data.randomElement() { - let userChoicesCountry = userChoices.map { $0.value.country } - - if !userChoicesCountry.contains(choice.value.country) { - userChoices[choice.key] = choice.value - } - } else { - fatalError("Couldn't get a random value from data") - } - } - - // Get question asked (correct answer) - let userChoicesCountry = userChoices.map { $0.value.country } - let correctAnswer = data.first(where: { - !userChoices.keys.contains($0.key) && // Avoid duplicated cities - !dataAsked.keys.contains($0.key) && // Avoid cities already asked - !userChoicesCountry.contains($0.value.country) // Avoid duplicated country names in userChoices - }) - - // Unwrap optional - if let correctAnswer = correctAnswer { - userChoices[correctAnswer.key] = correctAnswer.value - dataAsked[correctAnswer.key] = correctAnswer.value - self.correctAnswer = correctAnswer - } else { - fatalError("Couldn't unwrap optional value") - } - - self.userChoices = userChoices - } -} - diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/CoreDataController.swift --- a/GeoQuiz/Models/Controllers/CoreDataController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -// -// CoreDataController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 22/10/22. -// - -import Foundation -import SwiftUI -import CoreData - -class CoreDataController { - static func deleteGame(at offsets: IndexSet, from games: FetchedResults, with moc: NSManagedObjectContext) { - for offset in offsets { - let game = games[offset] - moc.delete(game) - } - - try? moc.save() - } -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/CountryGameController.swift --- a/GeoQuiz/Models/Controllers/CountryGameController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,103 +0,0 @@ -// -// CountryGameController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 20/9/22. -// - -import Foundation -import AVFAudio - -class CountryGameController: Game, ObservableObject { - - // Define type of generics - typealias T = CountryModel.Country - - var data: [String: T] - var dataAsked = [String: T]() - - // Data - @Published var correctAnswer = ( - key: String(), - value: T(flag: String(), currency: String(), population: Int(), capital: String()) - ) - - // User - @Published var userChoices = [String: T]() - @Published var userScore = 0 - @Published var userLives = 3 - @Published var correctAnswers = [String: T]() - @Published var wrongAnswers = [String: T]() - - // Alerts - @Published var alertTitle = String() - @Published var alertMessage = String() - @Published var showingEndGameAlert = false - @Published var showingWrongAnswerAlert = false - @Published var showingExitGameAlert = false - - // Animations - @Published var scoreScaleAmount = 1.0 - @Published var livesScaleAmount = 1.0 - - // Sound effects - @Published var player: AVAudioPlayer? - - init() { - let data: CountryModel = Bundle.main.decode("countries.json") - self.data = data.countries - - let user = UserController() - userLives = user.data.numberOfLives - - if let userData = UserDefaults.standard.data(forKey: "UserData") { - if let decodedUserData = try? JSONDecoder().decode(UserDataModel.self, from: userData) { - userLives = decodedUserData.numberOfLives - } - } - - askQuestion { - selector() - } - } -} - -extension CountryGameController { - func selector() { - - // Get random choices - var userChoices = [String: T]() - - while userChoices.count < 2 { - if let choice = data.randomElement() { - userChoices[choice.key] = choice.value - } else { - fatalError("Couldn't get a random value from data") - } - } - - // Get correct answer - let randomCountryKeys = data.keys.shuffled() - - let correctCountryKey = randomCountryKeys.first(where: { - !userChoices.keys.contains($0) && - !dataAsked.keys.contains($0) - - }) - - // Unwrap correct answer - if let correctCountryKey = correctCountryKey { - let correctCountryValue = data[correctCountryKey]! - - userChoices[correctCountryKey] = correctCountryValue - dataAsked[correctCountryKey] = correctCountryValue - - let correctAnswer = (key: correctCountryKey, value: correctCountryValue) - self.correctAnswer = correctAnswer - } else { - fatalError("Couldn't unwrap optional value") - } - - self.userChoices = userChoices - } -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/GameProtocol+Extension.swift --- a/GeoQuiz/Models/Controllers/GameProtocol+Extension.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,157 +0,0 @@ -// -// GameProtocol+Extension.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 18/9/22. -// - -import Foundation -import SwiftUI -import AVFAudio -import CoreData - -@objc -public enum GameType: Int16, CaseIterable { - case guessTheFlag - case guessTheCapital - case guessTheCountry - case guessThePopulation -} - -protocol Game: ObservableObject { - - // Define generic type - associatedtype T: Equatable - - // Game - var data: [String: T] { get set} - var dataAsked: [String: T] { get set } - var correctAnswer: (key: String, value: T) { get set } - - // User - var userChoices: [String: T] { get set } - var userScore: Int { get set } - var userLives: Int { get set } - var correctAnswers: [String: T] { get set } - var wrongAnswers: [String: T] { get set } - - // Alerts - var alertTitle: String { get set } - var alertMessage: String { get set } - var showingEndGameAlert: Bool { get set } - var showingWrongAnswerAlert: Bool { get set } - var showingExitGameAlert: Bool { get set } - - // Animations - var scoreScaleAmount: Double { get set } - var livesScaleAmount: Double { get set } - - // Sound effects - var player: AVAudioPlayer? { get set } - - func selector() -} - -extension Game { - var questionCounter: Int { - dataAsked.count - } - - func askQuestion(selector: () -> Void) { - guard questionCounter < data.count else { - alertTitle = "⭐️ Congratulations ⭐️" - alertMessage = "You completed the game." - showingEndGameAlert = true - - return - } - - selector() - } - - func answer(choice: (key: String, value: T), wrongMessage: String, selector: () -> Void) { - let haptics = HapticsController() - - if correctAnswer == choice { - haptics.success() - playSound("correctAnswer") - - withAnimation(.easeIn(duration: 0.5)) { - scoreScaleAmount += 1 - userScore += 1 - } - - correctAnswers[correctAnswer.key] = correctAnswer.value - askQuestion { - selector() - } - } else { - haptics.error() - playSound("wrongAnswer") - - withAnimation(.easeIn(duration: 0.5)) { - livesScaleAmount += 1 - userLives -= 1 - } - - wrongAnswers[choice.key] = choice.value - - if userLives == 0 { - alertTitle = "🤕 Game over 🤕" - alertMessage = "Get up and try again." - showingEndGameAlert = true - } else { - alertTitle = "🔴 Wrong 🔴" - alertMessage = "\(wrongMessage). You have \(userLives) lives left." - showingWrongAnswerAlert = true - } - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [self] in - withAnimation(.easeIn(duration: 0.5)) { - scoreScaleAmount = 1 - livesScaleAmount = 1 - } - } - } - - func save(_ gameType: GameType, with moc: NSManagedObjectContext) { - let playedGame = PlayedGame(context: moc) - - playedGame.type = gameType - playedGame.date = Date() - playedGame.score = Int32(userScore) - playedGame.correctAnswers = Array(correctAnswers.keys) - playedGame.wrongAnswers = Array(wrongAnswers.keys) - - do { - try moc.save() - } catch { - print("Couldn't save object to CoreData: \(error)") - } - } - - private func playSound(_ filename: String) { - let user = UserController() - - if user.data.sound { - guard let soundFileURL = Bundle.main.url(forResource: filename, withExtension: "wav") else { - fatalError("Sound file \(filename) couldn't be found") - } - - do { - try AVAudioSession.sharedInstance().setCategory(.ambient) - try AVAudioSession.sharedInstance().setActive(true) - } catch { - fatalError("Couldn't activate session") - } - - do { - player = try AVAudioPlayer(contentsOf: soundFileURL) - player?.play() - } catch { - fatalError("Couldn't play sound effect") - } - } - } -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/GameViewProtocol+Extension.swift --- a/GeoQuiz/Models/Controllers/GameViewProtocol+Extension.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -// -// GameViewProtocol+Extension.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 9/11/22. -// - -import Foundation - -protocol GameView { - -} - -extension GameView { - func getFlagPath(forName flagName: String) -> String { - return Bundle.main.path(forResource: flagName, ofType: "png")! - } - -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/HapticsController.swift --- a/GeoQuiz/Models/Controllers/HapticsController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ -// -// HapticsController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 18/9/22. -// - -import Foundation -import SwiftUI - -class HapticsController { - private var user = UserController() - - func success() { - if user.data.haptics { - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.success) - } - } - - func error() { - if user.data.haptics { - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.error) - } - } -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/MapController.swift --- a/GeoQuiz/Models/Controllers/MapController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -// -// MapController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 22/10/22. -// - -import Foundation -import MapKit - -class MapController: ObservableObject { - @Published var image: UIImage? = nil - - func getMapImage(lat: Double, lon: Double) { - let region = MKCoordinateRegion( - center: CLLocationCoordinate2D(latitude: lat, longitude: lon), - span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) - ) - - // Map options - let mapOptions = MKMapSnapshotter.Options() - mapOptions.region = region - mapOptions.size = CGSize(width: 500, height: 500) - mapOptions.pointOfInterestFilter = .excludingAll - - // Create the snapshotter and run it - let snapshotter = MKMapSnapshotter(options: mapOptions) - snapshotter.start { (snapshot, error) in - if let snapshot = snapshot { - self.image = snapshot.image - } else if let error = error { - print(error.localizedDescription) - } - } - } -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/PersistenceController.swift --- a/GeoQuiz/Models/Controllers/PersistenceController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,70 +0,0 @@ -// -// PersistenceController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 19/10/22. -// - -import CoreData -import SwiftUI - -class PersistenceController { - static let shared = PersistenceController() - - let container: NSPersistentCloudKitContainer - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - - #if DEBUG - createMockData(with: viewContext) - #endif - - return result - }() - - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "GeoQuiz") - - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - - container.viewContext.automaticallyMergesChangesFromParent = true - } - - #if DEBUG - static func createMockData(with moc: NSManagedObjectContext) { - for _ in 0..<10 { - let playedGame = PlayedGame(context: moc) - - playedGame.type = GameType(rawValue: Int16.random(in: 0...3))! - playedGame.score = Int32.random(in: 0...50) - - let dayComponent = DateComponents(day: Int.random(in: -5...0)) - playedGame.date = Calendar.current.date(byAdding: dayComponent, to: Date()) - - if playedGame.type == .guessTheFlag || playedGame.type == .guessTheCapital { - playedGame.correctAnswers = ["Bangladesh", "Belgium", "Burkina Faso", "Bermuda", "Jamaica"] - playedGame.wrongAnswers = ["Belarus", "Russia"] - } else { - playedGame.correctAnswers = ["Herat", "Lobito", "Darregueira", "San Juan"] - playedGame.wrongAnswers = ["San Luis", "Oranjestad"] - } - } - do { - try moc.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - #endif -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/StoreController.swift --- a/GeoQuiz/Models/Controllers/StoreController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -// -// StoreController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 9/10/22. -// - -import Foundation -import RevenueCat - -class StoreController: ObservableObject { - @Published var errorAlertTitle = "" - @Published var errorAlertMessage = "" - - @Published var showingErrorAlert = false - @Published var showingSuccessAlert = false - @Published var showingActivityAlert = false - - @Published var offerings: Offerings? = nil - @Published var customerInfo: CustomerInfo? { - didSet { - premiumIsActive = customerInfo?.entitlements["Premium"]?.isActive == true - } - } - - @Published var premiumIsActive = false - - init() { - #if DEBUG - premiumIsActive = true - #else - Purchases.shared.getCustomerInfo { (customerInfo, error) in - self.customerInfo = customerInfo - } - #endif - } - - func buy(_ package: Package) { - showingActivityAlert = true - - Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in - if customerInfo?.entitlements["Premium"]?.isActive == true { - self.showingSuccessAlert = true - } - - if let error = error as? RevenueCat.ErrorCode { - switch error { - case .purchaseCancelledError: - self.errorAlertTitle = "Purchase cancelled" - self.errorAlertMessage = "" - self.showingErrorAlert = true - default: - self.errorAlertTitle = "The purchase failed" - self.errorAlertMessage = "If the problem persists, contact me at dmartin@dennistech.io" - self.showingErrorAlert = true - } - } - - self.customerInfo = customerInfo - self.showingActivityAlert = false - } - } - - func restorePurchase() { - showingActivityAlert = true - - Purchases.shared.restorePurchases { customerInfo, error in - if customerInfo?.entitlements["Premium"]?.isActive == true { - self.showingSuccessAlert = true - } else { - self.errorAlertTitle = "Opps!" - self.errorAlertMessage = "You don't have GeoQuiz Premium unlocked." - self.showingErrorAlert = true - } - - if let _ = error { - self.errorAlertTitle = "The purchase couldn't be restored" - self.errorAlertMessage = "If the problem persists, contact me at dmartin@dennistech.io" - self.showingErrorAlert = true - } - - self.customerInfo = customerInfo - self.showingActivityAlert = false - } - } - - func fetchOfferings() { - Purchases.shared.getOfferings { (offerings, error) in - if let _ = error { - self.errorAlertTitle = "The product couldn't be fetched" - self.errorAlertMessage = "If the problem persists, contact me at dmartin@dennistech.io" - self.showingErrorAlert = true - } - - self.offerings = offerings - } - } -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/Models/Controllers/UserController.swift --- a/GeoQuiz/Models/Controllers/UserController.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,26 +0,0 @@ -// -// UserController.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 7/10/22. -// - -import Foundation - -class UserController: ObservableObject { - @Published var data = UserDataModel() { - didSet { - if let userDataEncoded = try? JSONEncoder().encode(data) { - UserDefaults.standard.set(userDataEncoded, forKey: "UserData") - } - } - } - - init() { - if let userData = UserDefaults.standard.data(forKey: "UserData") { - if let decodedUserData = try? JSONDecoder().decode(UserDataModel.self, from: userData) { - data = decodedUserData - } - } - } -} diff -r f51b70c2cccc -r f5a2c2dab208 GeoQuiz/ViewModels/ContentView-ViewModel.swift --- a/GeoQuiz/ViewModels/ContentView-ViewModel.swift Thu Nov 10 10:12:58 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -// -// ContentView-ViewModel.swift -// GeoQuiz -// -// Created by Dennis Concepción Martín on 7/11/22. -// - -import Foundation - -extension ContentView { - @MainActor class ViewModel: ObservableObject { - @Published var path: [GameType] = [] - @Published var showingBuyPremiumModalView = false - @Published var showingSettingsModalView = false - @Published var showingProfileModalView = false - - let premiumGames: [GameType] = [.guessTheCapital, .guessTheCountry, .guessThePopulation] - - func go(to gameType: GameType) { - path.append(gameType) - } - } -}