diff --git a/iOS/MonsterCards.xcodeproj/project.pbxproj b/iOS/MonsterCards.xcodeproj/project.pbxproj index 8aa3923..7c62193 100644 --- a/iOS/MonsterCards.xcodeproj/project.pbxproj +++ b/iOS/MonsterCards.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ E20209FC25D8E19100EFE733 /* MonsterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */; }; E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */; }; E210B83F25B42DAB0083EAC5 /* MCAdvantagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */; }; + E216B791260C1FE800FB205F /* LanguageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B790260C1FE800FB205F /* LanguageViewModel.swift */; }; + E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B798260C2DF200FB205F /* EditLanguages.swift */; }; + E216B79E260C396F00FB205F /* EditLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B79D260C396F00FB205F /* EditLanguage.swift */; }; E2182E6425B22F8A00DFAEF8 /* Monster+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */; }; E24ACE502607326E009BF703 /* EditBasicInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */; }; E24ACE562607EE94009BF703 /* EditArmor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE552607EE94009BF703 /* EditArmor.swift */; }; @@ -77,6 +80,9 @@ E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonsterViewModel.swift; sourceTree = ""; }; E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCProficiencyPicker.swift; sourceTree = ""; }; E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAdvantagePicker.swift; sourceTree = ""; }; + E216B790260C1FE800FB205F /* LanguageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageViewModel.swift; sourceTree = ""; }; + E216B798260C2DF200FB205F /* EditLanguages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguages.swift; sourceTree = ""; }; + E216B79D260C396F00FB205F /* EditLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguage.swift; sourceTree = ""; }; E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Monster+CoreDataClass.swift"; sourceTree = ""; }; E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditBasicInfo.swift; sourceTree = ""; }; E24ACE552607EE94009BF703 /* EditArmor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditArmor.swift; sourceTree = ""; }; @@ -180,15 +186,15 @@ E2570FB725B1AC520055B23B /* MonsterCards */ = { isa = PBXGroup; children = ( - E2570FB825B1AC520055B23B /* MonsterCardsApp.swift */, - E2D473FB25B5328800CB36D7 /* Helpers */, - E257101225B1B2790055B23B /* Models */, - E2570FEB25B1ADA90055B23B /* Views */, E2570FBC25B1AC550055B23B /* Assets.xcassets */, - E2570FC125B1AC550055B23B /* Persistence.swift */, + E2D473FB25B5328800CB36D7 /* Helpers */, E2570FC625B1AC550055B23B /* Info.plist */, + E257101225B1B2790055B23B /* Models */, E2570FC325B1AC550055B23B /* MonsterCards.xcdatamodeld */, + E2570FB825B1AC520055B23B /* MonsterCardsApp.swift */, + E2570FC125B1AC550055B23B /* Persistence.swift */, E2570FBE25B1AC550055B23B /* Preview Content */, + E2570FEB25B1ADA90055B23B /* Views */, ); path = MonsterCards; sourceTree = ""; @@ -228,12 +234,14 @@ E24ACE5F2607F45E009BF703 /* EditAbilityScores.swift */, E24ACE552607EE94009BF703 /* EditArmor.swift */, E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */, - E2CB0DE526088CE400142591 /* EditStrings.swift */, + E216B79D260C396F00FB205F /* EditLanguage.swift */, + E216B798260C2DF200FB205F /* EditLanguages.swift */, E2B5285825B3028700AAA69E /* EditMonster.swift */, E24ACE642607F55D009BF703 /* EditSavingThrows.swift */, E2CB0DB226080C0500142591 /* EditSkill.swift */, E24ACE692607F715009BF703 /* EditSkills.swift */, E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */, + E2CB0DE526088CE400142591 /* EditStrings.swift */, E2570FFE25B1AE180055B23B /* Library.swift */, E2CB0DB726081A2F00142591 /* MCAbilityScorePicker.swift */, E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */, @@ -251,12 +259,13 @@ E257101225B1B2790055B23B /* Models */ = { isa = PBXGroup; children = ( - E2CB0DE0260887ED00142591 /* StringViewModel.swift */, E20209E625D8DEB600EFE733 /* Enums */, + E216B790260C1FE800FB205F /* LanguageViewModel.swift */, E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */, E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */, E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */, E20209F925D8E19100EFE733 /* SkillViewModel.swift */, + E2CB0DE0260887ED00142591 /* StringViewModel.swift */, ); path = Models; sourceTree = ""; @@ -402,10 +411,12 @@ E20209FB25D8E19100EFE733 /* SkillViewModel.swift in Sources */, E24ACE602607F45E009BF703 /* EditAbilityScores.swift in Sources */, E2570FC225B1AC550055B23B /* Persistence.swift in Sources */, + E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */, E2570FBB25B1AC520055B23B /* ContentView.swift in Sources */, E24ACE502607326E009BF703 /* EditBasicInfo.swift in Sources */, E2570FC525B1AC550055B23B /* MonsterCards.xcdatamodeld in Sources */, E2182E6425B22F8A00DFAEF8 /* Monster+CoreDataClass.swift in Sources */, + E216B791260C1FE800FB205F /* LanguageViewModel.swift in Sources */, E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */, E2570FF025B1ADC10055B23B /* Search.swift in Sources */, E257100925B1B2480055B23B /* MonsterDetail.swift in Sources */, @@ -420,6 +431,7 @@ E24ACE6A2607F715009BF703 /* EditSkills.swift in Sources */, E20209FC25D8E19100EFE733 /* MonsterViewModel.swift in Sources */, E2570FFF25B1AE180055B23B /* Library.swift in Sources */, + E216B79E260C396F00FB205F /* EditLanguage.swift in Sources */, E2BD703125B3BBB90058ED69 /* MCStepperField.swift in Sources */, E2CB0DE626088CE400142591 /* EditStrings.swift in Sources */, E2CB0DB326080C0500142591 /* EditSkill.swift in Sources */, diff --git a/iOS/MonsterCards/Models/LanguageViewModel.swift b/iOS/MonsterCards/Models/LanguageViewModel.swift new file mode 100644 index 0000000..de6cad9 --- /dev/null +++ b/iOS/MonsterCards/Models/LanguageViewModel.swift @@ -0,0 +1,79 @@ +// +// LanguageViewModel.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import Foundation + +// TODO: split this into separate Model and ViewModel classes later. +public class LanguageViewModel : NSObject, ObservableObject, Comparable, Identifiable, NSSecureCoding { + public static var supportsSecureCoding = true + + public func encode(with coder: NSCoder) { + coder.encode(self.name, forKey: "name") + coder.encode(self.speaks, forKey: "speaks") + } + + public required init?(coder: NSCoder) { + self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String + self.speaks = coder.decodeBool(forKey: "speaks") + } + + public static func < (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool { + lhs.name < rhs.name + } + + public static func == (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool { + lhs.name == rhs.name && + lhs.speaks == rhs.speaks + } + + @Published var name: String + @Published var speaks: Bool + + init( + _ name: String = "", + _ speaks: Bool = true + ) { + self.name = name + self.speaks = speaks + } +} + +// TODO: figure out how to add this to the set of known transformers so it will work with transformer set to NSSecureUnarchiveFromDataTransformerName +@objc(LanguageViewModelValueTransformer) +public final class LanguageViewModelValueTransformer: ValueTransformer { + override public class func transformedValueClass() -> AnyClass { + return NSArray.self + } + + override public class func allowsReverseTransformation() -> Bool { + return true + } + + override public func transformedValue(_ value: Any?) -> Any? { + guard let language = value as? NSArray else { return nil } + + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: language, requiringSecureCoding: true) + return data + } catch { + assertionFailure("Failed to transform `LanguageViewModel` to `Data`") + return nil + } + } + + override public func reverseTransformedValue(_ value: Any?) -> Any? { + guard let data = value as? NSData else { return nil } + + do { + let language = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: LanguageViewModel.self, from: data as Data) + return language + } catch { + assertionFailure("Failed to transform `Data` to `LanguageViewModel`") + return nil + } + } +} diff --git a/iOS/MonsterCards/Models/Monster+CoreDataClass.swift b/iOS/MonsterCards/Models/Monster+CoreDataClass.swift index c485a98..fc79c01 100644 --- a/iOS/MonsterCards/Models/Monster+CoreDataClass.swift +++ b/iOS/MonsterCards/Models/Monster+CoreDataClass.swift @@ -698,8 +698,51 @@ public class Monster: NSManagedObject { var languagesDescription: String { get { - let sortedLanguages = self.languagesArray.sorted() - return StringHelper.oxfordJoin(sortedLanguages, ", ", ", and ", " and ") + let spokenLanguages = (self.languages ?? []) + .filter({ $0.speaks }) + .map({$0.name}) + .sorted() + let understoodLanguages = (self.languages ?? []) + .filter({ !$0.speaks }) + .map({$0.name}) + .sorted() + + let understandsButText = understandsBut?.isEmpty ?? false + ? "" + : String(format: " but %@", understandsBut!) + + let telepathyText = telepathy > 0 + ? String(format: ", telepathy %d ft.", telepathy) + : "" + + if (spokenLanguages.count > 0) { + if (understoodLanguages.count > 0) { + return String( + format:"%@ and understands %@%@%@", + StringHelper.oxfordJoin(spokenLanguages), + StringHelper.oxfordJoin(understoodLanguages), + understandsButText, + telepathyText) + } else { + return String( + format: "%@%@%@", + StringHelper.oxfordJoin(spokenLanguages), + understandsButText, + telepathyText) + } + } else { + if (understoodLanguages.count > 0) { + return String( + format: "understands %@%@%@", + StringHelper.oxfordJoin(understoodLanguages), + understandsButText, + telepathyText) + } else if (telepathy > 0){ + return String(format: "telepathy %d ft.", telepathy) + } else { + return "" + } + } } } diff --git a/iOS/MonsterCards/Models/MonsterViewModel.swift b/iOS/MonsterCards/Models/MonsterViewModel.swift index ab893d6..b9fa6b9 100644 --- a/iOS/MonsterCards/Models/MonsterViewModel.swift +++ b/iOS/MonsterCards/Models/MonsterViewModel.swift @@ -54,6 +54,9 @@ class MonsterViewModel: ObservableObject { @Published var damageVulnerabilities: [StringViewModel] @Published var conditionImmunities: [StringViewModel] @Published var senses: [StringViewModel] + @Published var languages: [LanguageViewModel] + @Published var telepathy: Int64 + @Published var understandsBut: String init(_ rawMonster: Monster? = nil) { self.name = "" @@ -100,6 +103,9 @@ class MonsterViewModel: ObservableObject { self.damageVulnerabilities = [] self.conditionImmunities = [] self.senses = [] + self.languages = [] + self.telepathy = 0 + self.understandsBut = "" if (rawMonster != nil) { self.copyFromMonster(monster: rawMonster!) @@ -145,6 +151,8 @@ class MonsterViewModel: ObservableObject { self.charismaScore = monster.charismaScore self.charismaSavingThrowAdvantage = monster.charismaSavingThrowAdvantageEnum self.charismaSavingThrowProficiency = monster.charismaSavingThrowProficiencyEnum + self.telepathy = monster.telepathy + self.understandsBut = monster.understandsBut ?? "" self.skills = (monster.skills?.allObjects.map {SkillViewModel(($0 as! Skill))})!.sorted() self.damageImmunities = (monster.damageImmunities ?? []) @@ -166,6 +174,9 @@ class MonsterViewModel: ObservableObject { self.senses = (monster.senses ?? []) .map {StringViewModel($0)} .sorted() + + self.languages = (monster.languages ?? []) + .sorted() } func copyToMonster(monster: Monster) { @@ -207,6 +218,8 @@ class MonsterViewModel: ObservableObject { monster.charismaScore = charismaScore monster.charismaSavingThrowAdvantageEnum = charismaSavingThrowAdvantage monster.charismaSavingThrowProficiencyEnum = charismaSavingThrowProficiency + monster.telepathy = telepathy + monster.understandsBut = understandsBut // Remove missing skills from raw monster monster.skills?.forEach {s in @@ -233,5 +246,6 @@ class MonsterViewModel: ObservableObject { monster.damageResistances = damageResistances.map {$0.name} monster.damageVulnerabilities = damageVulnerabilities.map {$0.name} monster.senses = senses.map {$0.name} + monster.languages = languages.map {LanguageViewModel($0.name, $0.speaks)} } } diff --git a/iOS/MonsterCards/MonsterCards.xcdatamodeld/MonsterCards.xcdatamodel/contents b/iOS/MonsterCards/MonsterCards.xcdatamodeld/MonsterCards.xcdatamodel/contents index 43c88b7..26464f1 100644 --- a/iOS/MonsterCards/MonsterCards.xcdatamodeld/MonsterCards.xcdatamodel/contents +++ b/iOS/MonsterCards/MonsterCards.xcdatamodeld/MonsterCards.xcdatamodel/contents @@ -36,6 +36,7 @@ + @@ -47,6 +48,7 @@ + @@ -64,7 +66,7 @@ - + \ No newline at end of file diff --git a/iOS/MonsterCards/Persistence.swift b/iOS/MonsterCards/Persistence.swift index b55fb61..6cc6b14 100644 --- a/iOS/MonsterCards/Persistence.swift +++ b/iOS/MonsterCards/Persistence.swift @@ -38,19 +38,6 @@ struct PersistenceController { } container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { - -// NSPersistentStoreCoordinator.destroyPersistentStore(storeDes) - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ fatalError("Unresolved error \(error), \(error.userInfo)") } }) diff --git a/iOS/MonsterCards/Views/EditLanguage.swift b/iOS/MonsterCards/Views/EditLanguage.swift new file mode 100644 index 0000000..f6f352d --- /dev/null +++ b/iOS/MonsterCards/Views/EditLanguage.swift @@ -0,0 +1,28 @@ +// +// EditLanguage.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import SwiftUI + +struct EditLanguage: View { + @ObservedObject var viewModel: LanguageViewModel + + var body: some View { + MCTextField( + label: "Name", + value: $viewModel.name) + .autocapitalization(.none) + + Toggle("Speaks", isOn: $viewModel.speaks) + } +} + +struct EditLanguage_Previews: PreviewProvider { + static var previews: some View { + let viewModel = LanguageViewModel() + EditLanguage(viewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditLanguages.swift b/iOS/MonsterCards/Views/EditLanguages.swift new file mode 100644 index 0000000..f35f3f0 --- /dev/null +++ b/iOS/MonsterCards/Views/EditLanguages.swift @@ -0,0 +1,48 @@ +// +// EditLanguages.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import SwiftUI + +struct EditLanguages: View { + @ObservedObject var viewModel: MonsterViewModel + + var body: some View { + let sortedLanguages = viewModel.languages.sorted() + List { + MCTextField( + label: "Understands But", + value: $viewModel.understandsBut) + MCStepperField(label: "Telepathy", prefix: "", step: 5, suffix: " ft.", value: $viewModel.telepathy) + ForEach(sortedLanguages/*viewModel.languages*/) { language in + NavigationLink(language.name, destination: EditLanguage(viewModel: language)) + } + + } + .toolbar(content: { + Button( + action: { + let newLanguage = LanguageViewModel("English") + viewModel.languages.append(newLanguage) + viewModel.languages = viewModel.languages.sorted() + }, + label: { + Image(systemName: "plus") + } + ) + }) + .onAppear(perform: { + viewModel.languages = viewModel.languages.sorted() + }) + } +} + +struct EditLanguages_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditLanguages(viewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditMonster.swift b/iOS/MonsterCards/Views/EditMonster.swift index 3eb7ac4..1b5771b 100644 --- a/iOS/MonsterCards/Views/EditMonster.swift +++ b/iOS/MonsterCards/Views/EditMonster.swift @@ -65,6 +65,10 @@ struct EditMonster: View { NavigationLink( "Senses", destination: EditStrings(viewModel: monsterViewModel, path: \.senses, title: "Senses")) + + NavigationLink( + "Languages", + destination: EditLanguages(viewModel: monsterViewModel)) } } diff --git a/iOS/MonsterCards/Views/MonsterDetail.swift b/iOS/MonsterCards/Views/MonsterDetail.swift index 0b0c921..5ff035b 100644 --- a/iOS/MonsterCards/Views/MonsterDetail.swift +++ b/iOS/MonsterCards/Views/MonsterDetail.swift @@ -302,6 +302,11 @@ struct MonsterDetail_Previews: PreviewProvider { monster.wisdomSavingThrowProficiencyEnum = ProficiencyType.proficient monster.charismaSavingThrowAdvantageEnum = AdvantageType.disadvantage monster.charismaSavingThrowProficiencyEnum = ProficiencyType.none + monster.telepathy = 1 + monster.languages = [ + LanguageViewModel("English", true), + LanguageViewModel("French", false) + ] return Group { MonsterDetail(monster: monster)