changeset 16:1011e56b7832

implement user profile
author Dennis C. M. <dennis@denniscm.com>
date Thu, 20 Oct 2022 13:49:42 +0200
parents f1967f8cc67b
children 8dac58bb4569
files GeoQuiz.xcodeproj/project.pbxproj GeoQuiz/Assets.xcassets/Custom colors/Background.colorset/Contents.json GeoQuiz/Components/ColorExtension.swift GeoQuiz/Components/ProfileEditModalView.swift GeoQuiz/Components/ProgressBarHelper.swift GeoQuiz/Components/RecentGameHelper.swift GeoQuiz/Components/UserProfileComponent.swift GeoQuiz/Components/UserProgressComponent.swift GeoQuiz/ContentView.swift GeoQuiz/GeoQuizApp.swift GeoQuiz/Logic/DataController.swift GeoQuiz/Logic/GameInfoProtocol+Extension.swift GeoQuiz/Logic/GameStatsClass.swift GeoQuiz/Logic/GameTypeEnum.swift GeoQuiz/Logic/PersistenceController.swift GeoQuiz/ProfileEditModalView.swift GeoQuiz/ProfileModalView.swift
diffstat 17 files changed, 582 insertions(+), 202 deletions(-) [+]
line wrap: on
line diff
--- a/GeoQuiz.xcodeproj/project.pbxproj	Wed Oct 19 10:04:17 2022 +0200
+++ b/GeoQuiz.xcodeproj/project.pbxproj	Thu Oct 20 13:49:42 2022 +0200
@@ -44,12 +44,17 @@
 		95C6456C28FE87E4000CD570 /* UserDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6456B28FE87E4000CD570 /* UserDataModel.swift */; };
 		95C6456E28FE8C04000CD570 /* UserImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6456D28FE8C04000CD570 /* UserImageHelper.swift */; };
 		95C6457228FFC4DC000CD570 /* ProfileEditModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */; };
-		95C6457428FFC8E0000CD570 /* DataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6457328FFC8E0000CD570 /* DataController.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 /* RecentGameHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C6459C290003E1000CD570 /* RecentGameHelper.swift */; };
+		95C645BE29011EF9000CD570 /* UserProgressComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C645BD29011EF9000CD570 /* UserProgressComponent.swift */; };
+		95C645C029011F8E000CD570 /* GameStatsClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C645BF29011F8E000CD570 /* GameStatsClass.swift */; };
+		95C645C229014442000CD570 /* GameInfoProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C645C129014442000CD570 /* GameInfoProtocol+Extension.swift */; };
+		95C645C42901552B000CD570 /* UserProfileComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C645C32901552B000CD570 /* UserProfileComponent.swift */; };
+		95C645C629015A06000CD570 /* ProgressBarHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C645C529015A06000CD570 /* ProgressBarHelper.swift */; };
 		95CA295028F6BB4500CE0B7A /* ActivityAlertHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */; };
-		95CC404928F98503001F74E1 /* GameTypeEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC404828F98503001F74E1 /* GameTypeEnum.swift */; };
 		95FA409A28D9876B00129B60 /* GuessTheFlagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FA409928D9876B00129B60 /* GuessTheFlagView.swift */; };
 		95FA409C28D9881100129B60 /* CountryGameClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FA409B28D9881100129B60 /* CountryGameClass.swift */; };
 /* End PBXBuildFile section */
@@ -92,12 +97,17 @@
 		95C6456B28FE87E4000CD570 /* UserDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataModel.swift; sourceTree = "<group>"; };
 		95C6456D28FE8C04000CD570 /* UserImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserImageHelper.swift; sourceTree = "<group>"; };
 		95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditModalView.swift; sourceTree = "<group>"; };
-		95C6457328FFC8E0000CD570 /* DataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataController.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 /* RecentGameHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentGameHelper.swift; sourceTree = "<group>"; };
+		95C645BD29011EF9000CD570 /* UserProgressComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProgressComponent.swift; sourceTree = "<group>"; };
+		95C645BF29011F8E000CD570 /* GameStatsClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStatsClass.swift; sourceTree = "<group>"; };
+		95C645C129014442000CD570 /* GameInfoProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameInfoProtocol+Extension.swift"; sourceTree = "<group>"; };
+		95C645C32901552B000CD570 /* UserProfileComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileComponent.swift; sourceTree = "<group>"; };
+		95C645C529015A06000CD570 /* ProgressBarHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarHelper.swift; sourceTree = "<group>"; };
 		95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAlertHelper.swift; sourceTree = "<group>"; };
-		95CC404828F98503001F74E1 /* GameTypeEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameTypeEnum.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 /* CountryGameClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryGameClass.swift; sourceTree = "<group>"; };
@@ -122,15 +132,16 @@
 				951AFAEC28E5657500A4A4BD /* CityModel.swift */,
 				951AFAEE28E565FE00A4A4BD /* CountryModel.swift */,
 				95C6456B28FE87E4000CD570 /* UserDataModel.swift */,
+				95C645C129014442000CD570 /* GameInfoProtocol+Extension.swift */,
 				955A658228D733E400CEEC6D /* GameProtocol+Extension.swift */,
 				955A65A828D7815E00CEEC6D /* HapticsClass.swift */,
 				95FA409B28D9881100129B60 /* CountryGameClass.swift */,
 				951AFAF028E5735400A4A4BD /* CityGameClass.swift */,
 				95919DB528F076BF00F21F8F /* UserClass.swift */,
 				95197EFC28F339AE00FE67E9 /* StoreKitRCClass.swift */,
-				95CC404828F98503001F74E1 /* GameTypeEnum.swift */,
+				95C645BF29011F8E000CD570 /* GameStatsClass.swift */,
 				95AE8D5628C8750E0067F219 /* LoadFunc.swift */,
-				95C6457328FFC8E0000CD570 /* DataController.swift */,
+				95C6457328FFC8E0000CD570 /* PersistenceController.swift */,
 				95C6459828FFE5A3000CD570 /* PlayedGame+CoreDataClass.swift */,
 				95C6459928FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift */,
 			);
@@ -188,6 +199,7 @@
 				950C535228F2FA3300179C78 /* BuyPremiumModalView.swift */,
 				952E41EC28DC658900198643 /* SettingsModalView.swift */,
 				9590359428E098FF00B24560 /* ProfileModalView.swift */,
+				95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */,
 				959D414728C87EA600BAAC14 /* Components */,
 				95030CE728D1B60F001AA3A1 /* Logic */,
 				9520ABBA28C86D0300A3D4D7 /* Resources */,
@@ -215,11 +227,14 @@
 				95919DBB28F08D0600F21F8F /* LinkHelper.swift */,
 				95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */,
 				95C6456D28FE8C04000CD570 /* UserImageHelper.swift */,
+				95C6459C290003E1000CD570 /* RecentGameHelper.swift */,
+				95C645C529015A06000CD570 /* ProgressBarHelper.swift */,
+				95C645BD29011EF9000CD570 /* UserProgressComponent.swift */,
+				95C645C32901552B000CD570 /* UserProfileComponent.swift */,
 				952E41E828DC521200198643 /* GameAlertsModifier.swift */,
 				95C430F828D0A8E500480D23 /* GradientExtension.swift */,
 				951D197228D485E000671FAD /* ColorExtension.swift */,
 				955950BA28F15FF2001BDEE8 /* FormatterExtension.swift */,
-				95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */,
 			);
 			path = Components;
 			sourceTree = "<group>";
@@ -311,29 +326,34 @@
 				95197EFD28F339AE00FE67E9 /* StoreKitRCClass.swift in Sources */,
 				9509A8E228E5A3D700CFCDBA /* GuessThePopulationView.swift in Sources */,
 				955A658328D733E400CEEC6D /* GameProtocol+Extension.swift in Sources */,
-				95CC404928F98503001F74E1 /* GameTypeEnum.swift in Sources */,
-				95C6457428FFC8E0000CD570 /* DataController.swift in Sources */,
+				95C6457428FFC8E0000CD570 /* PersistenceController.swift in Sources */,
 				95919DB628F076BF00F21F8F /* UserClass.swift in Sources */,
+				95C6459D290003E1000CD570 /* RecentGameHelper.swift in Sources */,
 				95C6456E28FE8C04000CD570 /* UserImageHelper.swift in Sources */,
 				95C4315628C64A8C00212131 /* ContentView.swift in Sources */,
+				95C645C229014442000CD570 /* GameInfoProtocol+Extension.swift in Sources */,
 				95C4315928C6500000212131 /* GameButtonHelper.swift in Sources */,
 				956273EA28CB2E98008DC094 /* FlagImageHelper.swift in Sources */,
 				951AFAED28E5657500A4A4BD /* CityModel.swift in Sources */,
 				950C535328F2FA3300179C78 /* BuyPremiumModalView.swift in Sources */,
 				951B630228D1A87C004F9877 /* GuessTheCapitalView.swift in Sources */,
 				9539829328C51EDE00B70973 /* GeoQuizApp.swift in Sources */,
+				95C645C029011F8E000CD570 /* GameStatsClass.swift in Sources */,
 				95AF322A28DF293900023ACC /* GuessTheCountryView.swift in Sources */,
 				95919DBC28F08D0600F21F8F /* LinkHelper.swift in Sources */,
+				95C645BE29011EF9000CD570 /* UserProgressComponent.swift in Sources */,
 				951AFAEF28E565FE00A4A4BD /* CountryModel.swift in Sources */,
 				95030CEA28D1BA4D001AA3A1 /* AnswerButtonHelper.swift in Sources */,
 				95FA409C28D9881100129B60 /* CountryGameClass.swift in Sources */,
 				95CA295028F6BB4500CE0B7A /* ActivityAlertHelper.swift in Sources */,
 				955A658128D703EB00CEEC6D /* GameToolbarHelper.swift in Sources */,
 				95AE8D5728C8750E0067F219 /* LoadFunc.swift in Sources */,
+				95C645C42901552B000CD570 /* UserProfileComponent.swift in Sources */,
 				95C6457728FFC934000CD570 /* GeoQuiz.xcdatamodeld in Sources */,
 				9590359528E098FF00B24560 /* ProfileModalView.swift in Sources */,
 				955950BB28F15FF2001BDEE8 /* FormatterExtension.swift in Sources */,
 				95C6456C28FE87E4000CD570 /* UserDataModel.swift in Sources */,
+				95C645C629015A06000CD570 /* ProgressBarHelper.swift in Sources */,
 				95C6459B28FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift in Sources */,
 				951D197328D485E000671FAD /* ColorExtension.swift in Sources */,
 				95C6457228FFC4DC000CD570 /* ProfileEditModalView.swift in Sources */,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Assets.xcassets/Custom colors/Background.colorset/Contents.json	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "246",
+          "green" : "242",
+          "red" : "242"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "30",
+          "green" : "28",
+          "red" : "28"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
--- a/GeoQuiz/Components/ColorExtension.swift	Wed Oct 19 10:04:17 2022 +0200
+++ b/GeoQuiz/Components/ColorExtension.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -40,4 +40,8 @@
     static var royalLightBlue: Color {
         Color("RoyalLightBlue")
     }
+    
+    static var customBackground: Color {
+        Color("Background")
+    }
 }
--- a/GeoQuiz/Components/ProfileEditModalView.swift	Wed Oct 19 10:04:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-//
-//  ProfileEditModalView.swift
-//  GeoQuiz
-//
-//  Created by Dennis Concepción Martín on 19/10/22.
-//
-
-import SwiftUI
-import PhotosUI
-
-struct ProfileEditModalView: View {
-    @ObservedObject var user: User
-    @Environment(\.dismiss) var dismiss
-    
-    @State private var selectedItem: PhotosPickerItem? = nil
-    
-    var body: some View {
-        NavigationStack {
-            Form {
-                Section {
-                    HStack {
-                        Spacer()
-                        ZStack {
-                            UserImage(uiImage: user.data.uiImage)
-                                .onChange(of: selectedItem) { newItem in
-                                    Task {
-                                        if let data = try? await newItem?.loadTransferable(type: Data.self) {
-                                            user.data.imageData = data
-                                        }
-                                    }
-                                }
-                            
-                            PhotosPicker(
-                                selection: $selectedItem,
-                                matching: .images,
-                                photoLibrary: .shared()) {
-                                    EmptyView()
-                                }
-                        }
-                        
-                        Spacer()
-                    }
-                } header: {
-                    Text("Profile image")
-                }
-                
-                Section {
-                    TextField("Enter a username", text: $user.data.username)
-                } header: {
-                    Text("Username")
-                }
-            }
-            .navigationTitle("Edit profile")
-            .navigationBarTitleDisplayMode(.inline)
-            .toolbar {
-                ToolbarItem(placement: .navigationBarTrailing) {
-                    Button("Done") {
-                        dismiss()
-                    }
-                }
-            }
-        }
-    }
-}
-
-struct ProfileEditModalView_Previews: PreviewProvider {
-    static var previews: some View {
-        ProfileEditModalView(user: User())
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Components/ProgressBarHelper.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,38 @@
+//
+//  ProgressBarHelper.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 20/10/22.
+//
+
+import SwiftUI
+
+struct ProgressBar: View {
+    let pctScore: Double
+    let gradient: Gradient
+    
+    var body: some View {
+        GeometryReader { geo in
+            ZStack(alignment: .leading) {
+                Capsule()
+                    .foregroundColor(.customBackground)
+                    .frame(height: 6)
+                
+                Capsule()
+                    .fill(
+                        LinearGradient(
+                            gradient: gradient,
+                            startPoint: .trailing, endPoint: .leading
+                        )
+                    )
+                    .frame(width: geo.size.width * pctScore, height: 6)
+            }
+        }
+    }
+}
+
+struct ProgressBar_Previews: PreviewProvider {
+    static var previews: some View {
+        ProgressBar(pctScore: 0.3, gradient: .main)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Components/RecentGameHelper.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,76 @@
+//
+//  RecentGameHelper.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 19/10/22.
+//
+
+import SwiftUI
+
+struct RecentGame: View {
+    let game: PlayedGame
+    let name: String
+    let gradient: Gradient
+    let symbol: String
+    
+    var body: some View {
+        HStack(alignment: .center, spacing: 15) {
+            RoundedRectangle(cornerRadius: 5)
+                .fill(
+                    LinearGradient(
+                        gradient: gradient,
+                        startPoint: .top, endPoint: .bottom
+                    )
+                )
+                .frame(width: 35, height: 35)
+                .overlay(
+                    Image(systemName: symbol)
+                        .font(.headline)
+                        .foregroundColor(.white)
+                        .padding(5)
+                )
+            
+            VStack(alignment: .leading) {
+                Text(name)
+                    .font(.headline)
+                
+                Text("\(game.date, format: .dateTime)")
+                    .font(.callout)
+                    .foregroundColor(.secondary)
+            }
+            
+            Spacer()
+            
+            Text("\(game.score, format: .number) ⭐️")
+            
+        }
+        .padding()
+        .background(
+            RoundedRectangle(cornerRadius: 20)
+                .foregroundColor(.white)
+        )
+    }
+    
+    init(game: PlayedGame) {
+        self.game = game
+        
+        switch game.type {
+        case .guessTheFlag:
+            self.name = GuessTheFlagInfo.name
+            self.gradient = GuessTheFlagInfo.gradient
+            self.symbol = GuessTheFlagInfo.symbol
+        case .guessTheCapital:
+            self.name = GuessTheCapitalInfo.name
+            self.gradient = GuessTheCapitalInfo.gradient
+            self.symbol = GuessTheCapitalInfo.symbol
+        case .guessTheCountry:
+            self.name = GuessTheCountryInfo.name
+            self.gradient = GuessTheCountryInfo.gradient
+            self.symbol = GuessTheCountryInfo.symbol
+        case .guessThePopulation:
+            self.name = GuessThePopulationInfo.name
+            self.gradient = GuessThePopulationInfo.gradient
+            self.symbol = GuessThePopulationInfo.symbol
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Components/UserProfileComponent.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,43 @@
+//
+//  UserProfileComponent.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 20/10/22.
+//
+
+import SwiftUI
+
+struct UserProfile: View {
+    @ObservedObject var user: User
+    @ObservedObject var storeKitRC: StoreKitRC
+    
+    var body: some View {
+        HStack(spacing: 20) {
+            UserImage(uiImage: user.data.uiImage)
+            
+            VStack(alignment: .leading, spacing: 8) {
+                Text(user.data.username)
+                    .font(.title)
+                    .fontWeight(.semibold)
+                
+                if storeKitRC.isActive {
+                    Text("Premium user ⭐️")
+                        .foregroundColor(.secondary)
+                }
+            }
+            
+            Spacer()
+        }
+        .padding()
+        .background(
+            RoundedRectangle(cornerRadius: 20)
+                .foregroundColor(.white)
+        )
+    }
+}
+
+struct UserProfile_Previews: PreviewProvider {
+    static var previews: some View {
+        UserProfile(user: User(), storeKitRC: StoreKitRC())
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Components/UserProgressComponent.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,82 @@
+//
+//  GameProgressHelper.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 20/10/22.
+//
+
+import SwiftUI
+
+struct UserProgress: View {
+    private let games: [(key: String, value: GameProgress)]
+    
+    var body: some View {
+        VStack(alignment: .leading) {
+            VStack(spacing: 10) {
+                ForEach(games, id: \.key) { game in
+                    HStack {
+                        Text(game.key)
+                            .font(.headline)
+                        
+                        Spacer()
+                        
+                        Text("\(game.value.highestScore) of \(game.value.numberOfQuestions)")
+                            .font(.caption)
+                            .foregroundColor(.secondary)
+                    }
+                    
+                    ProgressBar(pctScore: game.value.pctScore, gradient: game.value.gradient)
+                    
+                    if game.key != games.last!.key {
+                        Divider()
+                    }
+                }
+            }
+        }
+        .padding()
+        .background(
+            RoundedRectangle(cornerRadius: 20)
+                .foregroundColor(.white)
+        )
+    }
+    
+    init(playedGames: FetchedResults<PlayedGame>) {
+        let flagGames = playedGames.filter { $0.type == .guessTheFlag }
+        let capitalGames = playedGames.filter { $0.type == .guessTheCapital }
+        let countryGames = playedGames.filter { $0.type == .guessTheCountry }
+        let populationGames = playedGames.filter { $0.type == .guessThePopulation }
+        
+        self.games = [
+            GuessTheFlagInfo.name: GameProgress(
+                numberOfQuestions: GuessTheFlagInfo.numberOfQuestions,
+                highestScore: Int(flagGames.max { $0.score < $1.score }?.score ?? 0),
+                gradient: GuessTheFlagInfo.gradient
+            ),
+            GuessTheCapitalInfo.name: GameProgress(
+                numberOfQuestions: GuessTheCapitalInfo.numberOfQuestions,
+                highestScore: Int(capitalGames.max { $0.score < $1.score }?.score ?? 0),
+                gradient: GuessTheCapitalInfo.gradient
+            ),
+            GuessTheCountryInfo.name: GameProgress(
+                numberOfQuestions: GuessTheCountryInfo.numberOfQuestions,
+                highestScore: Int(countryGames.max { $0.score < $1.score }?.score ?? 0),
+                gradient: GuessTheCountryInfo.gradient
+            ),
+            GuessThePopulationInfo.name: GameProgress(
+                numberOfQuestions: GuessThePopulationInfo.numberOfQuestions,
+                highestScore: Int(populationGames.max { $0.score < $1.score }?.score ?? 0),
+                gradient: GuessThePopulationInfo.gradient
+            )
+        ].sorted { $0.value.pctScore > $1.value.pctScore }
+    }
+    
+    private struct GameProgress {
+        let numberOfQuestions: Int
+        let highestScore: Int
+        var pctScore: Double {
+            Double(highestScore) / Double(numberOfQuestions)
+        }
+        
+        let gradient: Gradient
+    }
+}
--- a/GeoQuiz/ContentView.swift	Wed Oct 19 10:04:17 2022 +0200
+++ b/GeoQuiz/ContentView.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -35,10 +35,10 @@
                         path.append(.guessTheFlag)
                     } label: {
                         GameButton(
-                            gradient: .main,
-                            level: "Level 1",
-                            symbol: "flag.fill",
-                            name: "Guess the flag"
+                            gradient: GuessTheFlagInfo.gradient,
+                            level: GuessTheFlagInfo.level,
+                            symbol: GuessTheFlagInfo.symbol,
+                            name: GuessTheFlagInfo.name
                         )
                     }
                     
@@ -50,10 +50,10 @@
                         }
                     } label: {
                         GameButton(
-                            gradient: .secondary,
-                            level: "Level 2",
-                            symbol: storeKitRC.isActive ? "building.2.fill": "lock.fill",
-                            name: "Guess the capital"
+                            gradient: GuessTheCapitalInfo.gradient,
+                            level: GuessTheCapitalInfo.level,
+                            symbol: storeKitRC.isActive ? GuessTheCapitalInfo.symbol: "lock.fill",
+                            name: GuessTheCapitalInfo.name
                         )
                     }
                     
@@ -65,10 +65,10 @@
                         }
                     } label: {
                         GameButton(
-                            gradient: .tertiary,
-                            level: "Level 3",
-                            symbol: storeKitRC.isActive ? "globe.americas.fill": "lock.fill",
-                            name: "Guess the country"
+                            gradient: GuessTheCountryInfo.gradient,
+                            level: GuessTheCountryInfo.level,
+                            symbol: storeKitRC.isActive ? GuessTheCountryInfo.symbol: "lock.fill",
+                            name: GuessTheCountryInfo.name
                         )
                     }
                     
@@ -80,10 +80,10 @@
                         }
                     } label: {
                         GameButton(
-                            gradient: .quaternary,
-                            level: "Level 4",
-                            symbol: storeKitRC.isActive ? "person.fill": "lock.fill",
-                            name: "Guess the population"
+                            gradient: GuessThePopulationInfo.gradient,
+                            level: GuessThePopulationInfo.level,
+                            symbol: storeKitRC.isActive ? GuessThePopulationInfo.symbol: "lock.fill",
+                            name: GuessThePopulationInfo.name
                         )
                     }
 
@@ -150,5 +150,6 @@
 struct ContentView_Previews: PreviewProvider {
     static var previews: some View {
         ContentView()
+            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
     }
 }
--- a/GeoQuiz/GeoQuizApp.swift	Wed Oct 19 10:04:17 2022 +0200
+++ b/GeoQuiz/GeoQuizApp.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -10,7 +10,7 @@
 
 @main
 struct GeoQuizApp: App {
-    @StateObject private var dataController = DataController()
+    @StateObject private var persistenceController = PersistenceController()
     
     init() {
         Purchases.configure(withAPIKey: "appl_BymTxroeoaWiXAraaFjcPlHlqbf")
@@ -19,7 +19,7 @@
     var body: some Scene {
         WindowGroup {
             ContentView()
-                .environment(\.managedObjectContext, dataController.container.viewContext)
+                .environment(\.managedObjectContext, persistenceController.container.viewContext)
         }
     }
 }
--- a/GeoQuiz/Logic/DataController.swift	Wed Oct 19 10:04:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-//
-//  DataController.swift
-//  GeoQuiz
-//
-//  Created by Dennis Concepción Martín on 19/10/22.
-//
-
-import CoreData
-import Foundation
-
-class DataController: ObservableObject {
-    let container = NSPersistentContainer(name: "GeoQuiz")
-    
-    init() {
-        container.loadPersistentStores { description, error in
-            if let error = error {
-                print("Core Data failed to load: \(error.localizedDescription)")
-            }
-        }
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Logic/GameInfoProtocol+Extension.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,75 @@
+//
+//  GameInfoProtocol+Structs.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 20/10/22.
+//
+
+import Foundation
+import SwiftUI
+
+@objc
+public enum GameType: Int16 {
+    case guessTheFlag, guessTheCapital, guessTheCountry, guessThePopulation
+}
+
+protocol GameInfo {
+    static var type: GameType { get }
+    static var level: String { get }
+    static var name: String { get }
+    static var symbol: String { get }
+    static var gradient: Gradient { get }
+    static var numberOfQuestions: Int { get }
+}
+
+class GuessTheFlagInfo: GameInfo {
+    static let type: GameType = .guessTheFlag
+    static let level = "Level 1"
+    static let name = "Guess the flag"
+    static let symbol = "flag.fill"
+    static let gradient: Gradient = .main
+    
+    static var numberOfQuestions: Int {
+        let data: CountryData = load("countries.json")
+        return data.countries.count
+    }
+}
+
+class GuessTheCapitalInfo: GameInfo {
+    static let type: GameType = .guessTheFlag
+    static let level = "Level 2"
+    static let name = "Guess the capital"
+    static let symbol = "building.2.fill"
+    static let gradient: Gradient = .secondary
+    
+    static var numberOfQuestions: Int {
+        let data: CountryData = load("countries.json")
+        return data.countries.count
+    }
+}
+
+class GuessTheCountryInfo: GameInfo {
+    static let type: GameType = .guessTheFlag
+    static let level = "Level 3"
+    static let name = "Guess the country"
+    static let symbol = "globe.americas.fill"
+    static let gradient: Gradient = .tertiary
+    
+    static var numberOfQuestions: Int {
+        let data: CityData = load("cities.json")
+        return data.cities.count
+    }
+}
+
+class GuessThePopulationInfo: GameInfo {
+    static let type: GameType = .guessTheFlag
+    static let level = "Level 4"
+    static let name = "Guess the population"
+    static let symbol = "person.fill"
+    static let gradient: Gradient = .quaternary
+    
+    static var numberOfQuestions: Int {
+        let data: CityData = load("cities.json")
+        return data.cities.count
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Logic/GameStatsClass.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,12 @@
+//
+//  GameStatsClass.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 20/10/22.
+//
+
+import Foundation
+
+class GameStats {
+    
+}
--- a/GeoQuiz/Logic/GameTypeEnum.swift	Wed Oct 19 10:04:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-//
-//  GameTypeEnum.swift
-//  GeoQuiz
-//
-//  Created by Dennis Concepción Martín on 14/10/22.
-//
-
-import Foundation
-
-@objc
-public enum GameType: Int16 {
-    case guessTheFlag, guessTheCapital, guessTheCountry, guessThePopulation
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/Logic/PersistenceController.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,55 @@
+//
+//  PersistenceController.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 19/10/22.
+//
+
+import CoreData
+import Foundation
+
+class PersistenceController: ObservableObject {
+    
+    // Create mock data for previews
+    static var preview: PersistenceController = {
+        let result = PersistenceController()
+        let viewContext = result.container.viewContext
+        
+//        for _ in 0..<10 {
+//            let playedGame = PlayedGame(context: viewContext)
+//            playedGame.id = UUID()
+//            playedGame.type = GameType(rawValue: Int16.random(in: 0...3))!
+//            playedGame.score = Int32.random(in: 0...50)
+//            playedGame.date = 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 viewContext.save()
+        } catch {
+            let nsError = error as NSError
+            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+        }
+        return result
+        
+    }()
+    
+    // Initialize container
+    let container = NSPersistentContainer(name: "GeoQuiz")
+    
+    init() {
+        container.loadPersistentStores { description, error in
+            if let error = error {
+                print("Core Data failed to load: \(error.localizedDescription)")
+            }
+        }
+        
+        container.viewContext.automaticallyMergesChangesFromParent = true
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/GeoQuiz/ProfileEditModalView.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -0,0 +1,70 @@
+//
+//  ProfileEditModalView.swift
+//  GeoQuiz
+//
+//  Created by Dennis Concepción Martín on 19/10/22.
+//
+
+import SwiftUI
+import PhotosUI
+
+struct ProfileEditModalView: View {
+    @ObservedObject var user: User
+    @Environment(\.dismiss) var dismiss
+    
+    @State private var selectedItem: PhotosPickerItem? = nil
+    
+    var body: some View {
+        NavigationStack {
+            Form {
+                Section {
+                    HStack {
+                        Spacer()
+                        ZStack {
+                            UserImage(uiImage: user.data.uiImage)
+                                .onChange(of: selectedItem) { newItem in
+                                    Task {
+                                        if let data = try? await newItem?.loadTransferable(type: Data.self) {
+                                            user.data.imageData = data
+                                        }
+                                    }
+                                }
+                            
+                            PhotosPicker(
+                                selection: $selectedItem,
+                                matching: .images,
+                                photoLibrary: .shared()) {
+                                    EmptyView()
+                                }
+                        }
+                        
+                        Spacer()
+                    }
+                } header: {
+                    Text("Profile image")
+                }
+                
+                Section {
+                    TextField("Enter a username", text: $user.data.username)
+                } header: {
+                    Text("Username")
+                }
+            }
+            .navigationTitle("Edit profile")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button("Done") {
+                        dismiss()
+                    }
+                }
+            }
+        }
+    }
+}
+
+struct ProfileEditModalView_Previews: PreviewProvider {
+    static var previews: some View {
+        ProfileEditModalView(user: User())
+    }
+}
--- a/GeoQuiz/ProfileModalView.swift	Wed Oct 19 10:04:17 2022 +0200
+++ b/GeoQuiz/ProfileModalView.swift	Thu Oct 20 13:49:42 2022 +0200
@@ -16,95 +16,65 @@
     @Environment(\.managedObjectContext) var moc
     
     @FetchRequest(sortDescriptors: [
-        SortDescriptor(\.date),
+        SortDescriptor(\.date, order: .reverse),
     ]) var playedGames: FetchedResults<PlayedGame>
     
     @State private var showingEditModalView = false
     
     var body: some View {
-        NavigationView {
-            Form {
-                Section {
-                    HStack(spacing: 20) {
-                        UserImage(uiImage: user.data.uiImage)
+            NavigationView {
+                ScrollView {
+                    VStack(alignment: .leading, spacing: 15) {
+                        UserProfile(user: user, storeKitRC: storeKitRC)
+                        
+                        UserProgress(playedGames: playedGames)
                         
-                        VStack(alignment: .leading, spacing: 8) {
-                            Text(user.data.username)
-                                .font(.title)
-                                .fontWeight(.semibold)
-                            
-                            if storeKitRC.isActive {
-                                Text("Premium user ⭐️")
-                                    .foregroundColor(.secondary)
-                            }
+                        ForEach(playedGames) { playedGame in
+                            RecentGame(game: playedGame)
+                        }
+                        .onDelete(perform: deleteGame)
+                    }
+                    .padding()
+                }
+                .background(.customBackground)
+                .navigationTitle("Profile")
+                .navigationBarTitleDisplayMode(.inline)
+                .toolbar {
+                    ToolbarItem(placement: .cancellationAction) {
+                        Button {
+                            dismiss()
+                        } label: {
+                            Label("Exit", systemImage: "multiply")
+                        }
+                    }
+                    
+                    ToolbarItem(placement: .navigationBarTrailing) {
+                        Button("Edit") {
+                            showingEditModalView = true
                         }
                     }
                 }
                 
-                Section {
-                    VStack(alignment: .leading) {
-                        Text("Game 1")
-                        Capsule()
-                            .frame(height: 6)
-                    }
-                    
-                    VStack(alignment: .leading) {
-                        Text("Game 1")
-                        Capsule()
-                            .frame(height: 6)
-                    }
-                    VStack(alignment: .leading) {
-                        Text("Game 1")
-                        Capsule()
-                            .frame(height: 6)
-                    }
-                    VStack(alignment: .leading) {
-                        Text("Game 1")
-                        Capsule()
-                            .frame(height: 6)
-                    }
-                } header: {
-                    Text("Progress")
-                }
-                
-                Section {
-                    ForEach(playedGames) { playedGame in
-                        HStack {
-                            Text("\(playedGame.id)")
-                            Text("\(playedGame.date)")
-                        }
-                    }
-                } header: {
-                    Text("Recent games")
+                .sheet(isPresented: $showingEditModalView) {
+                    ProfileEditModalView(user: user)
                 }
             }
-            .navigationTitle("Profile")
-            .navigationBarTitleDisplayMode(.inline)
-            .toolbar {
-                ToolbarItem(placement: .cancellationAction) {
-                    Button {
-                        dismiss()
-                    } label: {
-                        Label("Exit", systemImage: "multiply")
-                    }
-                }
-                
-                ToolbarItem(placement: .navigationBarTrailing) {
-                    Button("Edit") {
-                        showingEditModalView = true
-                    }
-                }
-            }
-            
-            .sheet(isPresented: $showingEditModalView) {
-                ProfileEditModalView(user: user)
-            }
+        
+    }
+    
+    private func deleteGame(at offsets: IndexSet) {
+        for offset in offsets {
+            let game = playedGames[offset]
+            moc.delete(game)
         }
+        
+        try? moc.save()
     }
 }
 
 struct ProfileView_Previews: PreviewProvider {
     static var previews: some View {
         ProfileModalView(user: User(), storeKitRC: StoreKitRC())
+            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
     }
 }