# HG changeset patch # User Dennis C. M. # Date 1666266582 -7200 # Node ID 1011e56b78323f97049d5500c3ed92dc3bdcfb5b # Parent f1967f8cc67be82cdb65be0fafd12c20c912c42e implement user profile diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz.xcodeproj/project.pbxproj --- 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 = ""; }; 95C6456D28FE8C04000CD570 /* UserImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserImageHelper.swift; sourceTree = ""; }; 95C6457128FFC4DC000CD570 /* ProfileEditModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditModalView.swift; sourceTree = ""; }; - 95C6457328FFC8E0000CD570 /* DataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataController.swift; sourceTree = ""; }; + 95C6457328FFC8E0000CD570 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 95C6457628FFC934000CD570 /* GeoQuiz.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = GeoQuiz.xcdatamodel; sourceTree = ""; }; 95C6459828FFE5A3000CD570 /* PlayedGame+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayedGame+CoreDataClass.swift"; sourceTree = ""; }; 95C6459928FFE5A3000CD570 /* PlayedGame+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayedGame+CoreDataProperties.swift"; sourceTree = ""; }; + 95C6459C290003E1000CD570 /* RecentGameHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentGameHelper.swift; sourceTree = ""; }; + 95C645BD29011EF9000CD570 /* UserProgressComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProgressComponent.swift; sourceTree = ""; }; + 95C645BF29011F8E000CD570 /* GameStatsClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStatsClass.swift; sourceTree = ""; }; + 95C645C129014442000CD570 /* GameInfoProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameInfoProtocol+Extension.swift"; sourceTree = ""; }; + 95C645C32901552B000CD570 /* UserProfileComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileComponent.swift; sourceTree = ""; }; + 95C645C529015A06000CD570 /* ProgressBarHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarHelper.swift; sourceTree = ""; }; 95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAlertHelper.swift; sourceTree = ""; }; - 95CC404828F98503001F74E1 /* GameTypeEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameTypeEnum.swift; sourceTree = ""; }; 95E6188428DDDB5C003359ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 95FA409928D9876B00129B60 /* GuessTheFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheFlagView.swift; sourceTree = ""; }; 95FA409B28D9881100129B60 /* CountryGameClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryGameClass.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Assets.xcassets/Custom colors/Background.colorset/Contents.json --- /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 + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Components/ColorExtension.swift --- 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") + } } diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Components/ProfileEditModalView.swift --- 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()) - } -} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Components/ProgressBarHelper.swift --- /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) + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Components/RecentGameHelper.swift --- /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 + } + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Components/UserProfileComponent.swift --- /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()) + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Components/UserProgressComponent.swift --- /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) { + 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 + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/ContentView.swift --- 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) } } diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/GeoQuizApp.swift --- 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) } } } diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Logic/DataController.swift --- 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)") - } - } - } -} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Logic/GameInfoProtocol+Extension.swift --- /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 + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Logic/GameStatsClass.swift --- /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 { + +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Logic/GameTypeEnum.swift --- 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 -} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/Logic/PersistenceController.swift --- /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 + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/ProfileEditModalView.swift --- /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()) + } +} diff -r f1967f8cc67b -r 1011e56b7832 GeoQuiz/ProfileModalView.swift --- 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 @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) } }