changeset 29:f5a2c2dab208

fix files structure
author Dennis C. M. <dennis@denniscm.com>
date Thu, 10 Nov 2022 10:27:28 +0100
parents f51b70c2cccc
children eb23effeede7
files GeoQuiz.xcodeproj/project.pbxproj GeoQuiz/Controllers/CityGameController.swift GeoQuiz/Controllers/CoreDataController.swift GeoQuiz/Controllers/CountryGameController.swift GeoQuiz/Controllers/GameProtocol+Extension.swift GeoQuiz/Controllers/GameViewProtocol+Extension.swift GeoQuiz/Controllers/HapticsController.swift GeoQuiz/Controllers/MapController.swift GeoQuiz/Controllers/PersistenceController.swift GeoQuiz/Models/Controllers/CityGameController.swift GeoQuiz/Models/Controllers/CoreDataController.swift GeoQuiz/Models/Controllers/CountryGameController.swift GeoQuiz/Models/Controllers/GameProtocol+Extension.swift GeoQuiz/Models/Controllers/GameViewProtocol+Extension.swift GeoQuiz/Models/Controllers/HapticsController.swift GeoQuiz/Models/Controllers/MapController.swift GeoQuiz/Models/Controllers/PersistenceController.swift GeoQuiz/Models/Controllers/StoreController.swift GeoQuiz/Models/Controllers/UserController.swift GeoQuiz/ViewModels/ContentView-ViewModel.swift
diffstat 20 files changed, 584 insertions(+), 732 deletions(-) [+]
line wrap: on
line diff
--- 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 = "<group>"; };
 		950C535228F2FA3300179C78 /* BuyPremiumModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyPremiumModalView.swift; sourceTree = "<group>"; };
 		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 = "<group>"; };
 		951AFAE828E5655C00A4A4BD /* cities.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = cities.json; sourceTree = "<group>"; };
 		951AFAEC28E5657500A4A4BD /* CityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityModel.swift; sourceTree = "<group>"; };
 		951AFAEE28E565FE00A4A4BD /* CountryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryModel.swift; sourceTree = "<group>"; };
-		951AFAF028E5735400A4A4BD /* CityGameController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityGameController.swift; sourceTree = "<group>"; };
 		951B630128D1A87C004F9877 /* GuessTheCapitalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheCapitalView.swift; sourceTree = "<group>"; };
 		951DCDBA291A420C00BAE20C /* mc.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mc.png; sourceTree = "<group>"; };
 		951DCDBB291A420C00BAE20C /* gr.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gr.png; sourceTree = "<group>"; };
@@ -503,6 +501,16 @@
 		951DCE90291A424900BAE20C /* ve.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ve.png; sourceTree = "<group>"; };
 		951DCE91291A424900BAE20C /* zm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = zm.png; sourceTree = "<group>"; };
 		951DCF6A291A6B3700BAE20C /* np.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = np.png; sourceTree = "<group>"; };
+		951F0BFD291CFADF0026D5A2 /* GameProtocol+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GameProtocol+Extension.swift"; sourceTree = "<group>"; };
+		951F0BFE291CFADF0026D5A2 /* CoreDataController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataController.swift; sourceTree = "<group>"; };
+		951F0BFF291CFADF0026D5A2 /* MapController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapController.swift; sourceTree = "<group>"; };
+		951F0C00291CFADF0026D5A2 /* GameViewProtocol+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GameViewProtocol+Extension.swift"; sourceTree = "<group>"; };
+		951F0C01291CFADF0026D5A2 /* HapticsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticsController.swift; sourceTree = "<group>"; };
+		951F0C02291CFADF0026D5A2 /* StoreController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreController.swift; sourceTree = "<group>"; };
+		951F0C03291CFADF0026D5A2 /* CountryGameController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryGameController.swift; sourceTree = "<group>"; };
+		951F0C04291CFADF0026D5A2 /* CityGameController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CityGameController.swift; sourceTree = "<group>"; };
+		951F0C05291CFADF0026D5A2 /* PersistenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
+		951F0C06291CFADF0026D5A2 /* UserController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserController.swift; sourceTree = "<group>"; };
 		952E41E828DC521200198643 /* GameAlertsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameAlertsModifier.swift; sourceTree = "<group>"; };
 		952E41EC28DC658900198643 /* SettingsModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModalView.swift; sourceTree = "<group>"; };
 		952E41F028DC6F6D00198643 /* correctAnswer.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = correctAnswer.wav; sourceTree = "<group>"; };
@@ -514,13 +522,9 @@
 		954AF4672905397A00180065 /* PlayedGamesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayedGamesList.swift; sourceTree = "<group>"; };
 		954AF46B2905433300180065 /* GeoQuiz.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GeoQuiz.entitlements; sourceTree = "<group>"; };
 		955A658028D703EB00CEEC6D /* GameToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameToolbar.swift; sourceTree = "<group>"; };
-		955A658228D733E400CEEC6D /* GameProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameProtocol+Extension.swift"; sourceTree = "<group>"; };
-		955A65A828D7815E00CEEC6D /* HapticsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsController.swift; sourceTree = "<group>"; };
 		957822472918F445005F2D50 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
 		9590359428E098FF00B24560 /* ProfileModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModalView.swift; sourceTree = "<group>"; };
-		95919DB528F076BF00F21F8F /* UserController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserController.swift; sourceTree = "<group>"; };
 		95919DBB28F08D0600F21F8F /* SettingsRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRow.swift; sourceTree = "<group>"; };
-		95A4F42829040E350018DFAC /* CoreDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataController.swift; sourceTree = "<group>"; };
 		95A4F42A29043DC00018DFAC /* UserImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserImage.swift; sourceTree = "<group>"; };
 		95AF322928DF293900023ACC /* GuessTheCountryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheCountryView.swift; sourceTree = "<group>"; };
 		95BC392C28EC42570049AB49 /* CityMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityMap.swift; sourceTree = "<group>"; };
@@ -529,21 +533,17 @@
 		95C6456B28FE87E4000CD570 /* UserDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataModel.swift; sourceTree = "<group>"; };
 		95C6456D28FE8C04000CD570 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
 		95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditModalView.swift; sourceTree = "<group>"; };
-		95C6457328FFC8E0000CD570 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
 		95C6457628FFC934000CD570 /* GeoQuiz.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = GeoQuiz.xcdatamodel; sourceTree = "<group>"; };
 		95C6459828FFE5A3000CD570 /* PlayedGame+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayedGame+CoreDataClass.swift"; sourceTree = "<group>"; };
 		95C6459928FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayedGame+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		95C6459C290003E1000CD570 /* RecentGame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentGame.swift; sourceTree = "<group>"; };
 		95CA294F28F6BB4500CE0B7A /* ActivityAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAlert.swift; sourceTree = "<group>"; };
 		95D8BF31291BAF8C006FC606 /* SettingsModalView-ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsModalView-ViewModel.swift"; sourceTree = "<group>"; };
-		95D8BF35291BB1F7006FC606 /* GameViewProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewProtocol+Extension.swift"; sourceTree = "<group>"; };
 		95D8BF37291BBB3D006FC606 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
 		95D8BF39291BC5DA006FC606 /* GuessTheFlagView-ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GuessTheFlagView-ViewModel.swift"; sourceTree = "<group>"; };
 		95DB7C00290492FC007D01D8 /* GameInfoModel+Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameInfoModel+Protocol.swift"; sourceTree = "<group>"; };
-		95DB7C022904A968007D01D8 /* MapController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapController.swift; sourceTree = "<group>"; };
 		95E6188428DDDB5C003359ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
 		95FA409928D9876B00129B60 /* GuessTheFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheFlagView.swift; sourceTree = "<group>"; };
-		95FA409B28D9881100129B60 /* CountryGameController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryGameController.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -790,6 +790,23 @@
 			path = Flags;
 			sourceTree = "<group>";
 		};
+		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 = "<group>";
+		};
 		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 = "<group>";
 		};
-		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 = "<group>";
-		};
 /* 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;
 		};
--- /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
+    }
+}
+
--- /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<PlayedGame>, with moc: NSManagedObjectContext) {
+        for offset in offsets {
+            let game = games[offset]
+            moc.delete(game)
+        }
+        
+        try? moc.save()
+    }
+}
--- /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
+    }
+}
--- /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")
+            }
+        }
+    }
+}
--- /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")!
+    }
+    
+}
--- /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)
+        }
+    }
+}
--- /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)
+            }
+        }
+    }
+}
--- /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
+}
--- 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
-    }
-}
-
--- 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<PlayedGame>, with moc: NSManagedObjectContext) {
-        for offset in offsets {
-            let game = games[offset]
-            moc.delete(game)
-        }
-        
-        try? moc.save()
-    }
-}
--- 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
-    }
-}
--- 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")
-            }
-        }
-    }
-}
--- 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")!
-    }
-    
-}
--- 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)
-        }
-    }
-}
--- 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)
-            }
-        }
-    }
-}
--- 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
-}
--- 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
-        }
-    }
-}
--- 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
-            }
-        }
-    }
-}
--- 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)
-        }
-    }
-}