Add 'iOS/' from commit '938f0fb75860d3637b998bdd0c27dcffd9fc9451'
git-subtree-dir: iOS git-subtree-mainline:c4bb775af4git-subtree-split:938f0fb758
This commit is contained in:
21
iOS/MonsterCards/Views/Collections.swift
Normal file
21
iOS/MonsterCards/Views/Collections.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Collections.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/15/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Collections: View {
|
||||
// @State var allCollections: [MonsterCollection] = []
|
||||
var body: some View {
|
||||
Text("Collections")
|
||||
}
|
||||
}
|
||||
|
||||
struct Collections_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Collections().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
80
iOS/MonsterCards/Views/ContentView.swift
Normal file
80
iOS/MonsterCards/Views/ContentView.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/15/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ImportInfo {
|
||||
var monster: MonsterViewModel = MonsterViewModel()
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@State private var importInfo = ImportInfo()
|
||||
@State private var isShowingImportDialog = false
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
Search()
|
||||
.tabItem {
|
||||
Image(systemName: "magnifyingglass")
|
||||
Text("Search")
|
||||
}
|
||||
Dashboard()
|
||||
.tabItem {
|
||||
Image(systemName: "rectangle.3.offgrid.fill")
|
||||
Text("Dashboard")
|
||||
|
||||
}
|
||||
Collections()
|
||||
.tabItem {
|
||||
Image(systemName: "tray.full.fill")
|
||||
Text("Collections")
|
||||
}
|
||||
Library()
|
||||
.tabItem {
|
||||
Image(systemName: "book.fill")
|
||||
Text("Library")
|
||||
}
|
||||
}
|
||||
.onOpenURL(perform: beginImportingMonster)
|
||||
.sheet(isPresented: $isShowingImportDialog) {
|
||||
ImportMonster(monster: $importInfo.monster, isOpen: $isShowingImportDialog)
|
||||
}
|
||||
}
|
||||
|
||||
func beginImportingMonster(url: URL) {
|
||||
|
||||
// TOOD: only do this if the file name ends in .json or .monster
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
let isAccessing = url.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
if (isAccessing) {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
let data = try Data(contentsOf: url)
|
||||
let monsterDTO = try decoder.decode(MonsterDTO.self, from: data)
|
||||
// TODO: check for some minimal set of properties to ensure this is the expected json schema
|
||||
self.importInfo.monster = MonsterImportHelper.import5ESBMonster(monsterDTO)
|
||||
// TODO: throw or set an err here and don't set isShowingImportDialog to true if the file didn't match any of our supported monster schemas.
|
||||
self.isShowingImportDialog = true
|
||||
} catch let error as NSError {
|
||||
// TODO: show an error message to the user that we were unable to open the file and maybe why.
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
20
iOS/MonsterCards/Views/Dashboard.swift
Normal file
20
iOS/MonsterCards/Views/Dashboard.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Dashboard.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/15/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Dashboard: View {
|
||||
var body: some View {
|
||||
Text("Dashboard")
|
||||
}
|
||||
}
|
||||
|
||||
struct Dashboard_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Dashboard().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
42
iOS/MonsterCards/Views/EditAbilityScores.swift
Normal file
42
iOS/MonsterCards/Views/EditAbilityScores.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// EditAbilityScores.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditAbilityScores: View {
|
||||
@ObservedObject var monsterViewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
List {MCStepperField(
|
||||
label: "STR",
|
||||
value: $monsterViewModel.strengthScore)
|
||||
MCStepperField(
|
||||
label: "DEX",
|
||||
value: $monsterViewModel.dexterityScore)
|
||||
MCStepperField(
|
||||
label: "CON",
|
||||
value: $monsterViewModel.constitutionScore)
|
||||
MCStepperField(
|
||||
label: "INT",
|
||||
value: $monsterViewModel.intelligenceScore)
|
||||
MCStepperField(
|
||||
label: "WIS",
|
||||
value: $monsterViewModel.wisdomScore)
|
||||
MCStepperField(
|
||||
label: "CHA",
|
||||
value: $monsterViewModel.charismaScore)
|
||||
}
|
||||
.navigationTitle("Ability Scores")
|
||||
}
|
||||
}
|
||||
|
||||
struct EditAbilityScores_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditAbilityScores(monsterViewModel: viewModel)
|
||||
}
|
||||
}
|
||||
45
iOS/MonsterCards/Views/EditArmor.swift
Normal file
45
iOS/MonsterCards/Views/EditArmor.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// EditArmor.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditArmor: View {
|
||||
@ObservedObject var monsterViewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
// Armor Type select bound to monster.armorTypeEnum
|
||||
MCArmorTypePicker(
|
||||
label: "Armor Type",
|
||||
value: $monsterViewModel.armorType)
|
||||
|
||||
// Toggle bound to monster.hasShield?
|
||||
Toggle(
|
||||
"Has Shield",
|
||||
isOn: $monsterViewModel.hasShield)
|
||||
|
||||
// Number with -/+ buttons bound to monster.naturalArmorBonus
|
||||
MCStepperField(
|
||||
label: "Natural Armor Bonus",
|
||||
value: $monsterViewModel.naturalArmorBonus)
|
||||
|
||||
// Editable Text field bound to monster.customArmorText?
|
||||
MCTextField(
|
||||
label: "Custom Armor",
|
||||
value: $monsterViewModel.customArmor)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
.navigationTitle("Armor")
|
||||
}
|
||||
}
|
||||
|
||||
struct EditArmor_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditArmor(monsterViewModel: viewModel)
|
||||
}
|
||||
}
|
||||
71
iOS/MonsterCards/Views/EditBasicInfo.swift
Normal file
71
iOS/MonsterCards/Views/EditBasicInfo.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// EditBasicInfo.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditBasicInfo: View {
|
||||
|
||||
@ObservedObject var monsterViewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
// Editable Text field bound to monster.name
|
||||
MCTextField(
|
||||
label: "Name",
|
||||
value: $monsterViewModel.name)
|
||||
.autocapitalization(.words)
|
||||
|
||||
// Editable Text field bound to monster.size
|
||||
MCTextField(
|
||||
label: "Size",
|
||||
value: $monsterViewModel.size)
|
||||
.autocapitalization(.words)
|
||||
|
||||
// Editable Text field bound to monster.type
|
||||
MCTextField(
|
||||
label: "Type",
|
||||
value: $monsterViewModel.type)
|
||||
.autocapitalization(.none)
|
||||
|
||||
// Editable Text field bound to monster.subType
|
||||
MCTextField(
|
||||
label: "Subtype",
|
||||
value: $monsterViewModel.subType)
|
||||
.autocapitalization(.none)
|
||||
|
||||
// Editable Text field bound to monster.alignment
|
||||
MCTextField(
|
||||
label: "Alignment",
|
||||
value: $monsterViewModel.alignment)
|
||||
.autocapitalization(.none)
|
||||
|
||||
// Number with -/+ buttons bound to monster.hitDice
|
||||
MCStepperField(
|
||||
label: "Hit Dice",
|
||||
value: $monsterViewModel.hitDice)
|
||||
|
||||
// Toggle bound to monster.hasCustomHP?
|
||||
Toggle(
|
||||
"Has Custom HP",
|
||||
isOn:$monsterViewModel.hasCustomHP)
|
||||
|
||||
// Editable Text field bound to monster.customHpText?
|
||||
MCTextField(
|
||||
label: "Custom HP",
|
||||
value: $monsterViewModel.customHP)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
.navigationTitle("Basic Info")
|
||||
}
|
||||
}
|
||||
|
||||
struct EditBasicInfo_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel.init(nil)
|
||||
EditBasicInfo(monsterViewModel: viewModel)
|
||||
}
|
||||
}
|
||||
41
iOS/MonsterCards/Views/EditChallengeRating.swift
Normal file
41
iOS/MonsterCards/Views/EditChallengeRating.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// EditChallengeRating.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditChallengeRating: View {
|
||||
@ObservedObject var viewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
let isUsingCustomProficiencyBonus = viewModel.challengeRating == ChallengeRating.custom
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
MCChallengeRatingPicker(
|
||||
label: "Rating",
|
||||
value: $viewModel.challengeRating)
|
||||
|
||||
MCTextField(
|
||||
label: "Custom Text",
|
||||
value: $viewModel.customChallengeRating)
|
||||
.disabled(!isUsingCustomProficiencyBonus)
|
||||
|
||||
MCStepperField(
|
||||
label: "Custom Proficiency Bonus",
|
||||
value: $viewModel.customProficiencyBonus)
|
||||
.disabled(!isUsingCustomProficiencyBonus)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct EditChallengeRating_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditChallengeRating(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
33
iOS/MonsterCards/Views/EditLanguage.swift
Normal file
33
iOS/MonsterCards/Views/EditLanguage.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// EditLanguage.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditLanguage: View {
|
||||
@ObservedObject var viewModel: LanguageViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
MCTextField(
|
||||
label: "Name",
|
||||
value: $viewModel.name)
|
||||
.autocapitalization(.none)
|
||||
|
||||
Toggle("Speaks", isOn: $viewModel.speaks)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct EditLanguage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = LanguageViewModel()
|
||||
EditLanguage(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
58
iOS/MonsterCards/Views/EditLanguages.swift
Normal file
58
iOS/MonsterCards/Views/EditLanguages.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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))
|
||||
}
|
||||
.onDelete(perform: { indexSet in
|
||||
for index in indexSet {
|
||||
viewModel.languages.remove(at: index)
|
||||
}
|
||||
})
|
||||
}
|
||||
.toolbar(content: {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
220
iOS/MonsterCards/Views/EditMonster.swift
Normal file
220
iOS/MonsterCards/Views/EditMonster.swift
Normal file
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// EditMonster.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/16/21.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
struct EditMonster: View {
|
||||
// TODO: Add challengeRating/challengeRatingEnum and customChallengeRating maybe in basicInfo
|
||||
// TODO: Add a way to edit the monster being blind. Probably a header section to the senses section.
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
var monster: Monster
|
||||
|
||||
@StateObject private var monsterViewModel: MonsterViewModel = MonsterViewModel()
|
||||
@State private var hasInitializedViewModel = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Group {
|
||||
NavigationLink(
|
||||
"Basic Info",
|
||||
destination: EditBasicInfo(monsterViewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Armor",
|
||||
destination: EditArmor(monsterViewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Speed",
|
||||
destination: EditSpeed(monsterViewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Ability Scores",
|
||||
destination: EditAbilityScores(monsterViewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Saving Throws",
|
||||
destination: EditSavingThrows(monsterViewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Skills",
|
||||
destination: EditSkills(monsterViewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Condition Immunities",
|
||||
destination: EditStrings(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.conditionImmunities,
|
||||
title: "Condition Immunities"))
|
||||
|
||||
NavigationLink(
|
||||
"Damage Immunities",
|
||||
destination: EditStrings(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.damageImmunities,
|
||||
title: "Damage Immunities"))
|
||||
|
||||
NavigationLink(
|
||||
"Damage Resistances",
|
||||
destination: EditStrings(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.damageResistances,
|
||||
title: "Damage Resistances"))
|
||||
|
||||
NavigationLink(
|
||||
"Damage Vulnerabilities",
|
||||
destination: EditStrings(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.damageVulnerabilities,
|
||||
title: "Damage Vulnerabilities"))
|
||||
}
|
||||
Group {
|
||||
NavigationLink(
|
||||
"Senses",
|
||||
destination: EditStrings(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.senses,
|
||||
title: "Senses"))
|
||||
|
||||
NavigationLink(
|
||||
"Languages",
|
||||
destination: EditLanguages(viewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Challenge Rating",
|
||||
destination: EditChallengeRating(viewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Abilities",
|
||||
destination: EditTraits(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.abilities,
|
||||
title: "Abilities"))
|
||||
|
||||
NavigationLink(
|
||||
"Actions",
|
||||
destination: EditTraits(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.actions,
|
||||
title: "Actions"))
|
||||
|
||||
NavigationLink(
|
||||
"Reactions",
|
||||
destination: EditTraits(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.reactions,
|
||||
title: "Reactions"))
|
||||
|
||||
NavigationLink(
|
||||
"Legendary Actions",
|
||||
destination: EditTraits(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.legendaryActions,
|
||||
title: "Legendary Actions"))
|
||||
|
||||
NavigationLink(
|
||||
"Lair Actions",
|
||||
destination: EditTraits(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.lairActions,
|
||||
title: "Lair Actions"))
|
||||
|
||||
NavigationLink(
|
||||
"Regional Actions",
|
||||
destination: EditTraits(
|
||||
viewModel: monsterViewModel,
|
||||
path: \.regionalActions,
|
||||
title: "Regional Actions"))
|
||||
}
|
||||
|
||||
}
|
||||
.onAppear(perform: copyMonsterToLocal)
|
||||
.toolbar(content: {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button("Save", action: saveMonster)
|
||||
}
|
||||
})
|
||||
.navigationTitle(monsterViewModel.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func dismissView() {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
|
||||
private func saveMonster() {
|
||||
copyLocalToMonster()
|
||||
|
||||
do {
|
||||
// Save core data context
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// 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.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
dismissView()
|
||||
}
|
||||
|
||||
private func copyMonsterToLocal() {
|
||||
if (!hasInitializedViewModel) {
|
||||
monsterViewModel.copyFromMonster(monster: monster)
|
||||
hasInitializedViewModel = true
|
||||
}
|
||||
}
|
||||
|
||||
private func copyLocalToMonster() {
|
||||
monsterViewModel.copyToMonster(monster: monster)
|
||||
}
|
||||
}
|
||||
|
||||
struct EditMonster_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let monster = Monster.init(context: context)
|
||||
|
||||
monster.name = "Steve"
|
||||
monster.size = "Medium"
|
||||
monster.type = "humanoid"
|
||||
monster.subtype = "human"
|
||||
monster.alignment = "LG"
|
||||
monster.hitDice = 6
|
||||
monster.hasCustomHP = true
|
||||
monster.customHP = "12 (1d10)+2"
|
||||
monster.walkSpeed = 5
|
||||
monster.burrowSpeed = 10
|
||||
monster.climbSpeed = 15
|
||||
monster.flySpeed = 20
|
||||
monster.swimSpeed = 25
|
||||
monster.canHover = true
|
||||
monster.hasCustomSpeed = false
|
||||
monster.customSpeed = "walk: 5 ft."
|
||||
monster.strengthScore = 8
|
||||
monster.dexterityScore = 10
|
||||
monster.constitutionScore = 12
|
||||
monster.intelligenceScore = 14
|
||||
monster.wisdomScore = 16
|
||||
monster.charismaScore = 18
|
||||
monster.strengthSavingThrowAdvantage = AdvantageType.none.rawValue
|
||||
monster.strengthSavingThrowProficiency = ProficiencyType.none.rawValue
|
||||
monster.dexteritySavingThrowAdvantage = AdvantageType.advantage.rawValue
|
||||
monster.dexteritySavingThrowProficiency = ProficiencyType.proficient.rawValue
|
||||
monster.constitutionSavingThrowAdvantage = AdvantageType.disadvantage.rawValue
|
||||
monster.constitutionSavingThrowProficiency = ProficiencyType.expertise.rawValue
|
||||
monster.intelligenceSavingThrowAdvantage = AdvantageType.none.rawValue
|
||||
monster.intelligenceSavingThrowProficiency = ProficiencyType.expertise.rawValue
|
||||
monster.wisdomSavingThrowAdvantage = AdvantageType.advantage.rawValue
|
||||
monster.wisdomSavingThrowProficiency = ProficiencyType.proficient.rawValue
|
||||
monster.charismaSavingThrowAdvantage = AdvantageType.disadvantage.rawValue
|
||||
monster.charismaSavingThrowProficiency = ProficiencyType.none.rawValue
|
||||
|
||||
return EditMonster(monster: monster).environment(\.managedObjectContext, context)
|
||||
}
|
||||
}
|
||||
80
iOS/MonsterCards/Views/EditSavingThrows.swift
Normal file
80
iOS/MonsterCards/Views/EditSavingThrows.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// EditSavingThrows.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditSavingThrows: View {
|
||||
@ObservedObject var monsterViewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
// TODO: Add a version of this layout for wider screens where these VStacks with HStacks
|
||||
VStack {
|
||||
MCAdvantagePicker(
|
||||
label: "Strength Advantage",
|
||||
value: $monsterViewModel.strengthSavingThrowAdvantage)
|
||||
|
||||
MCProficiencyPicker(
|
||||
label: "Strength Proficiency",
|
||||
value: $monsterViewModel.strengthSavingThrowProficiency)
|
||||
}
|
||||
VStack {
|
||||
MCAdvantagePicker(
|
||||
label: "Dexterity Advantage",
|
||||
value: $monsterViewModel.dexteritySavingThrowAdvantage)
|
||||
|
||||
MCProficiencyPicker(
|
||||
label: "Dexterity Proficiency",
|
||||
value: $monsterViewModel.dexteritySavingThrowProficiency)
|
||||
}
|
||||
VStack {
|
||||
MCAdvantagePicker(
|
||||
label: "Constitution Advantage",
|
||||
value: $monsterViewModel.constitutionSavingThrowAdvantage)
|
||||
|
||||
MCProficiencyPicker(
|
||||
label: "Constitution Proficiency",
|
||||
value: $monsterViewModel.constitutionSavingThrowProficiency)
|
||||
}
|
||||
VStack {
|
||||
MCAdvantagePicker(
|
||||
label: "Intelligence Advantage",
|
||||
value: $monsterViewModel.intelligenceSavingThrowAdvantage)
|
||||
|
||||
MCProficiencyPicker(
|
||||
label: "Intelligence Proficiency",
|
||||
value: $monsterViewModel.intelligenceSavingThrowProficiency)
|
||||
}
|
||||
VStack {
|
||||
MCAdvantagePicker(
|
||||
label: "Wisdom Advantage",
|
||||
value: $monsterViewModel.wisdomSavingThrowAdvantage)
|
||||
|
||||
MCProficiencyPicker(
|
||||
label: "Wisdom Proficiency",
|
||||
value: $monsterViewModel.wisdomSavingThrowProficiency)
|
||||
}
|
||||
VStack {
|
||||
MCAdvantagePicker(
|
||||
label: "Charisma Advantage",
|
||||
value: $monsterViewModel.charismaSavingThrowAdvantage)
|
||||
|
||||
MCProficiencyPicker(
|
||||
label: "Charisma Proficiency",
|
||||
value: $monsterViewModel.charismaSavingThrowProficiency)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Saving Throws")
|
||||
}
|
||||
}
|
||||
|
||||
struct EditSavingThrows_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditSavingThrows(monsterViewModel: viewModel)
|
||||
}
|
||||
}
|
||||
42
iOS/MonsterCards/Views/EditSkill.swift
Normal file
42
iOS/MonsterCards/Views/EditSkill.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// EditSkill.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditSkill: View {
|
||||
@ObservedObject var skillViewModel: SkillViewModel
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
MCTextField(
|
||||
label: "Name",
|
||||
value: $skillViewModel.name)
|
||||
.autocapitalization(.words)
|
||||
|
||||
MCAbilityScorePicker(
|
||||
label: "Ability Score",
|
||||
value: $skillViewModel.abilityScore)
|
||||
|
||||
// TODO: Add a version of this layout for wider screens where these two are in an HStack
|
||||
MCAdvantagePicker(
|
||||
label: "Advantage",
|
||||
value: $skillViewModel.advantage)
|
||||
|
||||
MCProficiencyPicker(
|
||||
label: "Proficiency",
|
||||
value: $skillViewModel.proficiency)
|
||||
}
|
||||
.navigationTitle("Skill")
|
||||
}
|
||||
}
|
||||
|
||||
struct EditSkill_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = SkillViewModel()
|
||||
EditSkill(skillViewModel: viewModel)
|
||||
}
|
||||
}
|
||||
49
iOS/MonsterCards/Views/EditSkills.swift
Normal file
49
iOS/MonsterCards/Views/EditSkills.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// EditSkills.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditSkills: View {
|
||||
@ObservedObject var monsterViewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(monsterViewModel.skills) { skill in
|
||||
NavigationLink(skill.name, destination: EditSkill(skillViewModel: skill))
|
||||
}
|
||||
.onDelete(perform: { indexSet in
|
||||
for index in indexSet {
|
||||
monsterViewModel.skills.remove(at: index)
|
||||
}
|
||||
})
|
||||
}
|
||||
.toolbar(content: {
|
||||
Button(
|
||||
action: {
|
||||
let newSkill = SkillViewModel()
|
||||
newSkill.name = ""
|
||||
newSkill.proficiency = .proficient
|
||||
monsterViewModel.skills.append(newSkill)
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
)
|
||||
})
|
||||
.navigationTitle("Skills")
|
||||
.onAppear(perform: {
|
||||
monsterViewModel.skills = monsterViewModel.skills.sorted()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct EditSkills_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditSkills(monsterViewModel: viewModel)
|
||||
}
|
||||
}
|
||||
75
iOS/MonsterCards/Views/EditSpeed.swift
Normal file
75
iOS/MonsterCards/Views/EditSpeed.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// EditSpeed.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditSpeed: View {
|
||||
@ObservedObject var monsterViewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
// Number bound to monster.walkSpeed
|
||||
MCStepperField(
|
||||
label: "Base",
|
||||
step: 5,
|
||||
suffix: " ft.",
|
||||
value: $monsterViewModel.walkSpeed)
|
||||
|
||||
// Number bound to monster.burrowSpeed
|
||||
MCStepperField(
|
||||
label: "Burrow",
|
||||
step: 5,
|
||||
suffix: " ft.",
|
||||
value: $monsterViewModel.burrowSpeed)
|
||||
|
||||
// Number bound to monster.climbSpeed
|
||||
MCStepperField(
|
||||
label: "Climb",
|
||||
step: 5,
|
||||
suffix: " ft.",
|
||||
value: $monsterViewModel.climbSpeed)
|
||||
|
||||
// Number bound to monster.flySpeed
|
||||
MCStepperField(
|
||||
label: "Fly",
|
||||
step: 5,
|
||||
suffix: " ft.",
|
||||
value: $monsterViewModel.flySpeed)
|
||||
|
||||
// Toggle bound to monster.canHover
|
||||
Toggle(
|
||||
"Can Hover",
|
||||
isOn: $monsterViewModel.canHover)
|
||||
|
||||
// Number bound to monster.swimSpeed
|
||||
MCStepperField(
|
||||
label: "Swim",
|
||||
step: 5,
|
||||
suffix: " ft.",
|
||||
value: $monsterViewModel.swimSpeed)
|
||||
|
||||
// Toggle bound to monster.hasCustomSpeed
|
||||
Toggle(
|
||||
"Has Custom Speed",
|
||||
isOn: $monsterViewModel.hasCustomSpeed)
|
||||
|
||||
// Editable Text field bound to monster.customSpeedText
|
||||
MCTextField(
|
||||
label: "Custom Speed",
|
||||
value: $monsterViewModel.customSpeed)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
.navigationTitle("Speed")
|
||||
}
|
||||
}
|
||||
|
||||
struct EditSpeed_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditSpeed(monsterViewModel: viewModel)
|
||||
}
|
||||
}
|
||||
60
iOS/MonsterCards/Views/EditStrings.swift
Normal file
60
iOS/MonsterCards/Views/EditStrings.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// EditStrings.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/22/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditStrings: View {
|
||||
@ObservedObject var viewModel: MonsterViewModel
|
||||
var path: ReferenceWritableKeyPath<MonsterViewModel, [StringViewModel]>
|
||||
var title: String
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(viewModel[keyPath: path]) { damageType in
|
||||
TextField(
|
||||
"",
|
||||
text: Binding<String>(
|
||||
get: {damageType.name},
|
||||
set: {damageType.name = $0}
|
||||
)
|
||||
)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
.onDelete(perform: { indexSet in
|
||||
for index in indexSet {
|
||||
viewModel[keyPath: path].remove(at: index)
|
||||
}
|
||||
})
|
||||
}
|
||||
.toolbar(content: {
|
||||
Button(
|
||||
action: {
|
||||
let newString = StringViewModel()
|
||||
viewModel[keyPath: path].append(newString)
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
)
|
||||
})
|
||||
.onAppear(perform: {
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
})
|
||||
.navigationTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
struct EditStrings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditStrings(
|
||||
viewModel: viewModel,
|
||||
path: \.damageImmunities,
|
||||
title: "Damage Types")
|
||||
}
|
||||
}
|
||||
34
iOS/MonsterCards/Views/EditTrait.swift
Normal file
34
iOS/MonsterCards/Views/EditTrait.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// EditTrait.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/25/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditTrait: View {
|
||||
@ObservedObject var viewModel: AbilityViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
MCTextField(
|
||||
label: "Name",
|
||||
value: $viewModel.name)
|
||||
|
||||
Text("Description")
|
||||
.font(.caption2)
|
||||
|
||||
TextEditor(text: $viewModel.abilityDescription)
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct EditTrait_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = AbilityViewModel()
|
||||
EditTrait(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
64
iOS/MonsterCards/Views/EditTraits.swift
Normal file
64
iOS/MonsterCards/Views/EditTraits.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// EditTraits.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/25/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditTraits: View {
|
||||
@ObservedObject var viewModel: MonsterViewModel
|
||||
var path: ReferenceWritableKeyPath<MonsterViewModel, [AbilityViewModel]>
|
||||
var title: String
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(viewModel[keyPath: path]) { ability in
|
||||
NavigationLink(
|
||||
ability.name,
|
||||
destination: EditTrait(viewModel: ability))
|
||||
}
|
||||
.onDelete(perform: { indexSet in
|
||||
for index in indexSet {
|
||||
viewModel[keyPath: path].remove(at: index)
|
||||
}
|
||||
})
|
||||
.onMove(perform: { indices, newOffset in
|
||||
viewModel[keyPath: path].move(fromOffsets: indices, toOffset: newOffset)
|
||||
|
||||
})
|
||||
}
|
||||
.toolbar(content: {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
|
||||
Button(
|
||||
action: {
|
||||
let newAbility = AbilityViewModel()
|
||||
viewModel[keyPath: path].append(newAbility)
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.onAppear(perform: {
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
})
|
||||
.navigationTitle(title)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct EditTraits_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditTraits(
|
||||
viewModel: viewModel,
|
||||
path: \.abilities,
|
||||
title: "Abilities")
|
||||
}
|
||||
}
|
||||
80
iOS/MonsterCards/Views/ImportMonster.swift
Normal file
80
iOS/MonsterCards/Views/ImportMonster.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// ImportMonster.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 4/1/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImportMonster: View {
|
||||
@Binding var monster: MonsterViewModel
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@Binding var isOpen: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
HStack {
|
||||
Button("Cancel", action: cancelImport)
|
||||
Spacer()
|
||||
Button("Add to Library", action: saveNewMonster)
|
||||
}
|
||||
MonsterDetailView(viewModel: monster)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func cancelImport() {
|
||||
isOpen = false
|
||||
}
|
||||
|
||||
func saveNewMonster() {
|
||||
print("Saving monster: \(monster.name)")
|
||||
withAnimation {
|
||||
let newMonster = Monster(context: viewContext)
|
||||
monster.copyToMonster(monster: newMonster)
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
isOpen = false
|
||||
} catch {
|
||||
// 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.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImportMonster_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let monster = MonsterViewModel()
|
||||
monster.name = "Steve"
|
||||
monster.size = "Medium"
|
||||
monster.type = "dwarf"
|
||||
monster.alignment = "chaotic good"
|
||||
monster.armorType = .none
|
||||
monster.hasShield = true
|
||||
monster.hitDice = 4
|
||||
monster.strengthScore = 20
|
||||
monster.dexterityScore = 14
|
||||
monster.constitutionScore = 18
|
||||
monster.intelligenceScore = 8
|
||||
monster.wisdomScore = 8
|
||||
monster.charismaScore = 15
|
||||
monster.walkSpeed = 40
|
||||
monster.challengeRating = .four
|
||||
monster.languages = [LanguageViewModel("Common", true), LanguageViewModel("Giant", true)]
|
||||
monster.actions = [AbilityViewModel("Greataxe, +3", "__Badass Attack:___ Hits the other dude on a _3_ or above and does a ton of damage")]
|
||||
|
||||
return
|
||||
VStack{
|
||||
Text("Hello, World!")
|
||||
}
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
ImportMonster(monster: .constant(monster), isOpen: .constant(true))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
77
iOS/MonsterCards/Views/Library.swift
Normal file
77
iOS/MonsterCards/Views/Library.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Library.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/15/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Library: View {
|
||||
// TODO: add an import button that searches https://api.open5e.com/monsters/ and lets you import a monster from there
|
||||
// TODO: add an import button that lets you browse for a tetra cube monster file and import it
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@FetchRequest(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \Monster.name, ascending: true),
|
||||
],
|
||||
animation: .default)
|
||||
var allMonsters: FetchedResults<Monster>
|
||||
|
||||
var body: some View {
|
||||
NavigationView{
|
||||
List {
|
||||
ForEach(allMonsters) { monster in
|
||||
NavigationLink(destination: MonsterDetailWrapper(monster: monster)) {
|
||||
Text(monster.name ?? "")
|
||||
}
|
||||
}
|
||||
.onDelete(perform: { indexSet in
|
||||
for index in indexSet {
|
||||
let monster = allMonsters[index]
|
||||
viewContext.delete(monster)
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// 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.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.navigationTitle("Library")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar(content: {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: addMonster) {
|
||||
Image(systemName:"plus")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func addMonster() {
|
||||
withAnimation {
|
||||
let newItem = Monster(context: viewContext)
|
||||
newItem.name = "Unnamed Monster"
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// 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.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Library_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
return Library().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
35
iOS/MonsterCards/Views/MCAbilityScorePicker.swift
Normal file
35
iOS/MonsterCards/Views/MCAbilityScorePicker.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// MCAbilityScorePicker.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 2/15/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MCAbilityScorePicker: View {
|
||||
var label: String = ""
|
||||
var value: Binding<AbilityScore>
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
Picker(
|
||||
selection: value,
|
||||
label: Text(value.wrappedValue.displayName)) {
|
||||
ForEach(AbilityScore.allCases) {abilityScore in
|
||||
Text(abilityScore.displayName).tag(abilityScore)
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MCAbilityScorePicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MCAbilityScorePicker(
|
||||
label: "Ability Score",
|
||||
value: .constant(AbilityScore.strength))
|
||||
}
|
||||
}
|
||||
39
iOS/MonsterCards/Views/MCAdvantagePicker.swift
Normal file
39
iOS/MonsterCards/Views/MCAdvantagePicker.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// MCAdvantagePicker.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/17/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MCAdvantagePicker: View {
|
||||
var label: String = ""
|
||||
var value: Binding<AdvantageType>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
Picker(
|
||||
selection: value,
|
||||
label: Text(label)) {
|
||||
ForEach(AdvantageType.allCases) { advType in
|
||||
Text(advType.displayName).tag(advType)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
// .pickerStyle(SegmentedPickerStyle())
|
||||
// .pickerStyle(WheelPickerStyle())
|
||||
// .pickerStyle(DefaultPickerStyle())
|
||||
// .pickerStyle(InlinePickerStyle())
|
||||
// .pickerStyle(MenuPickerStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MCAdvantagePicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MCAdvantagePicker(value: .constant(AdvantageType.none))
|
||||
}
|
||||
}
|
||||
36
iOS/MonsterCards/Views/MCArmorTypePicker.swift
Normal file
36
iOS/MonsterCards/Views/MCArmorTypePicker.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// MCArmorTypePicker.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 2/6/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MCArmorTypePicker: View {
|
||||
var label: String = ""
|
||||
var value: Binding<ArmorType>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
Picker(
|
||||
selection: value,
|
||||
label: Text(value.wrappedValue.displayName)) {
|
||||
ForEach(ArmorType.allCases) {armorType in
|
||||
Text(armorType.displayName).tag(armorType)
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MCArmorTypePicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MCArmorTypePicker(
|
||||
value: .constant(ArmorType.none)
|
||||
)
|
||||
}
|
||||
}
|
||||
35
iOS/MonsterCards/Views/MCChallengeRatingPicker.swift
Normal file
35
iOS/MonsterCards/Views/MCChallengeRatingPicker.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// MCChallengeRatingPicker.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MCChallengeRatingPicker: View {
|
||||
var label: String = ""
|
||||
var value: Binding<ChallengeRating>
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
Picker(
|
||||
selection: value,
|
||||
label: Text(value.wrappedValue.displayName)) {
|
||||
ForEach(ChallengeRating.allCases) {abilityScore in
|
||||
Text(abilityScore.displayName).tag(abilityScore)
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MCChallengeRatingPicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MCChallengeRatingPicker(
|
||||
label: "Rating",
|
||||
value: .constant(ChallengeRating.ten))
|
||||
}
|
||||
}
|
||||
34
iOS/MonsterCards/Views/MCProficiencyPicker.swift
Normal file
34
iOS/MonsterCards/Views/MCProficiencyPicker.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// MCProficiencyPicker.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/17/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MCProficiencyPicker: View {
|
||||
var label: String = ""
|
||||
var value: Binding<ProficiencyType>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
Picker(
|
||||
selection: value,
|
||||
label: Text(label)) {
|
||||
ForEach(ProficiencyType.allCases) { profType in
|
||||
Text(profType.displayName).tag(profType)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MCProficiencyPicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MCProficiencyPicker(value: .constant(ProficiencyType.none))
|
||||
}
|
||||
}
|
||||
37
iOS/MonsterCards/Views/MCStepperField.swift
Normal file
37
iOS/MonsterCards/Views/MCStepperField.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// MCStepperField.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/16/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MCStepperField: View {
|
||||
var label: String = ""
|
||||
var prefix: String = ""
|
||||
var step: Int = 1
|
||||
var suffix: String = ""
|
||||
var value: Binding<Int64>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
Stepper(
|
||||
value: value,
|
||||
step: step
|
||||
) {
|
||||
Text("\(prefix)\(value.wrappedValue)\(suffix)" as String)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MCStepperField_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MCStepperField(
|
||||
label: "Hit Dice",
|
||||
value: .constant(4))
|
||||
}
|
||||
}
|
||||
26
iOS/MonsterCards/Views/MCTextField.swift
Normal file
26
iOS/MonsterCards/Views/MCTextField.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// MCTextField.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/16/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MCTextField: View {
|
||||
var label: String
|
||||
var value: Binding<String>
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
TextField(label, text: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MCTextField_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MCTextField(label: "Name", value: .constant("Ted"))
|
||||
}
|
||||
}
|
||||
374
iOS/MonsterCards/Views/MonsterDetailView.swift
Normal file
374
iOS/MonsterCards/Views/MonsterDetailView.swift
Normal file
@@ -0,0 +1,374 @@
|
||||
//
|
||||
// MonsterDetail.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/15/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MarkdownUI
|
||||
|
||||
struct LabeledField<Content: View>: View {
|
||||
@Environment(\.horizontalSizeClass) var sizeClass
|
||||
|
||||
let content: Content
|
||||
let label: String
|
||||
|
||||
@inlinable public init(
|
||||
_ label: String,
|
||||
@ViewBuilder content: () -> Content) {
|
||||
self.content = content()
|
||||
self.label = label
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if (sizeClass == .compact) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.fontWeight(.bold)
|
||||
content
|
||||
}
|
||||
} else {
|
||||
HStack(alignment: .top) {
|
||||
Text(label)
|
||||
.fontWeight(.bold)
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SectionDivider: View {
|
||||
var body: some View {
|
||||
Image("section-divider") // divider
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
struct SmallAbilityScore: View {
|
||||
var label: String
|
||||
var score: Int64
|
||||
var modifier: Int
|
||||
|
||||
public init(
|
||||
_ label: String,
|
||||
_ score: Int64,
|
||||
_ modifier: Int) {
|
||||
self.label = label
|
||||
self.score = score
|
||||
self.modifier = modifier
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(label)
|
||||
.fontWeight(.bold)
|
||||
Text(String(format: "%d (%+d)", score, modifier))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct BasicInfoView: View {
|
||||
@ObservedObject var monster: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
let monsterMeta = monster.meta
|
||||
let monsterArmorClassDescription = monster.armorClassDescription
|
||||
let monsterHitPoints = monster.hitPoints
|
||||
let monsterSpeed = monster.speed
|
||||
|
||||
if (!monster.name.isEmpty) {
|
||||
Text(monster.name)
|
||||
.font(.largeTitle)
|
||||
}
|
||||
|
||||
// meta: "(large humanoid (elf) lawful evil"
|
||||
if (!monsterMeta.isEmpty) {
|
||||
Text(monsterMeta)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
SectionDivider()
|
||||
|
||||
// AC
|
||||
if (!monsterArmorClassDescription.isEmpty) {
|
||||
LabeledField("Armor Class") {
|
||||
Text(monsterArmorClassDescription)// armor class
|
||||
}
|
||||
}
|
||||
|
||||
// HP
|
||||
if (!monsterHitPoints.isEmpty) {
|
||||
LabeledField("Hit Points") {
|
||||
Text(monsterHitPoints) // hit points
|
||||
}
|
||||
}
|
||||
|
||||
// Speed
|
||||
if (!monsterSpeed.isEmpty) {
|
||||
LabeledField("Speed") {
|
||||
Text(monsterSpeed) // speed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AbilityScoresView: View {
|
||||
@ObservedObject var monster: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
SectionDivider()
|
||||
|
||||
// Ability Scores
|
||||
HStack {
|
||||
SmallAbilityScore("STR", monster.strengthScore, monster.strengthModifier)
|
||||
SmallAbilityScore("DEX", monster.dexterityScore, monster.dexterityModifier)
|
||||
SmallAbilityScore("CON", monster.constitutionScore, monster.constitutionModifier)
|
||||
SmallAbilityScore("INT", monster.intelligenceScore, monster.intelligenceModifier)
|
||||
SmallAbilityScore("WIS", monster.wisdomScore, monster.wisdomModifier)
|
||||
SmallAbilityScore("CHA", monster.charismaScore, monster.charismaModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResistancesAndImmunitiesView: View {
|
||||
@ObservedObject var monster: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
let monsterDamageVulnerabilitiesDescription = monster.damageVulnerabilitiesDescription
|
||||
let monsterDamageResistancesDescription = monster.damageResistancesDescription
|
||||
let monsterDamageImmunitiesDescription = monster.damageImmunitiesDescription
|
||||
let monsterConditionImmunitiesDescription = monster.conditionImmunitiesDescription
|
||||
let monsterSensesDescription = monster.sensesDescription
|
||||
|
||||
// Damage Vulnerabilities
|
||||
if (!monsterDamageVulnerabilitiesDescription.isEmpty) {
|
||||
LabeledField("Damage Vulnerabilities") {
|
||||
Text(monsterDamageVulnerabilitiesDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Damage Resistances
|
||||
if (!monsterDamageResistancesDescription.isEmpty) {
|
||||
LabeledField("Damage Resistances") {
|
||||
Text(monsterDamageResistancesDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Damage Immunities
|
||||
if (!monsterDamageImmunitiesDescription.isEmpty) {
|
||||
LabeledField("Damage Immunities") {
|
||||
Text(monsterDamageImmunitiesDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Condition Immunities
|
||||
if (!monsterConditionImmunitiesDescription.isEmpty) {
|
||||
LabeledField("Condition Immunities") {
|
||||
Text(monsterConditionImmunitiesDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Senses
|
||||
if (!monsterSensesDescription.isEmpty) {
|
||||
LabeledField("Senses") {
|
||||
Text(monsterSensesDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SavingThrowsAndSkillsView: View {
|
||||
@ObservedObject var monster: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
let savingThrowsDescription = monster.savingThrowsDescription
|
||||
let skillsDescription = monster.skillsDescription
|
||||
|
||||
// Saving Throws
|
||||
if (!savingThrowsDescription.isEmpty) {
|
||||
LabeledField("Saving Throws") {
|
||||
Text(savingThrowsDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (!skillsDescription.isEmpty) {
|
||||
LabeledField("Skills") {
|
||||
Text(skillsDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TraitList: View {
|
||||
var title: String
|
||||
var traits: [AbilityViewModel]
|
||||
var viewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
ForEach(traits) { action in
|
||||
VStack {
|
||||
Markdown(Document(action.renderedText(viewModel)))
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct MonsterDetailView: View {
|
||||
let kTextColor = Color(hex: 0x982818)
|
||||
|
||||
@ObservedObject var viewModel: MonsterViewModel
|
||||
|
||||
var body: some View {
|
||||
let monsterLanguagesDescription = viewModel.languagesDescription
|
||||
let monsterChallengeRatingDescription = viewModel.challengeRatingDescription
|
||||
|
||||
ScrollView {
|
||||
// TODO: Consider adding an inmage here at the top
|
||||
VStack (alignment: .leading) {
|
||||
// TODO: Find a way to hide unnecessarry dividiers.
|
||||
// if sections 0, 1, 2, and 3 are present there should be a divider between each of them
|
||||
// if section 1 is not present there should be one and only one divider between sections 0 and 2 as well as the one between 2 and 3
|
||||
// if sections 1 and 2 are not present there should be a single divider between sections 0 and 3
|
||||
|
||||
BasicInfoView(monster: viewModel)
|
||||
AbilityScoresView(monster: viewModel)
|
||||
SectionDivider()
|
||||
SavingThrowsAndSkillsView(monster: viewModel)
|
||||
ResistancesAndImmunitiesView(monster: viewModel)
|
||||
Group {
|
||||
// Languages
|
||||
if (!monsterLanguagesDescription.isEmpty) {
|
||||
LabeledField("Languages") {
|
||||
Text(monsterLanguagesDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Challenge Rating
|
||||
if (!monsterChallengeRatingDescription.isEmpty) {
|
||||
LabeledField("Challenge") {
|
||||
Text(monsterChallengeRatingDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Proficiency Bonus
|
||||
LabeledField("Proficiency Bonus") {
|
||||
Text(String(viewModel.proficiencyBonus))
|
||||
}
|
||||
|
||||
// Abilities
|
||||
if (viewModel.abilities.count > 0) {
|
||||
ForEach(viewModel.abilities) { ability in
|
||||
VStack {
|
||||
Markdown(Document(ability.renderedText(viewModel)))
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
if (viewModel.actions.count > 0) {
|
||||
TraitList(
|
||||
title:"Actions",
|
||||
traits: viewModel.actions,
|
||||
viewModel: viewModel)
|
||||
}
|
||||
|
||||
// Reactions
|
||||
if (viewModel.reactions.count > 0) {
|
||||
TraitList(
|
||||
title:"Reactions",
|
||||
traits: viewModel.reactions,
|
||||
viewModel: viewModel)
|
||||
}
|
||||
|
||||
// Legendary Actions
|
||||
if (viewModel.legendaryActions.count > 0) {
|
||||
TraitList(
|
||||
title:"Legendary Actions",
|
||||
traits: viewModel.legendaryActions,
|
||||
viewModel: viewModel)
|
||||
}
|
||||
|
||||
// Lair Actions
|
||||
if (viewModel.lairActions.count > 0) {
|
||||
TraitList(
|
||||
title: "Lair Actions",
|
||||
traits: viewModel.lairActions,
|
||||
viewModel: viewModel)
|
||||
}
|
||||
|
||||
// Regional Actions
|
||||
if (viewModel.regionalActions.count > 0) {
|
||||
TraitList(
|
||||
title: "Regional Actions",
|
||||
traits: viewModel.regionalActions,
|
||||
viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.foregroundColor(kTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MonsterDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let monster = MonsterViewModel.init()
|
||||
monster.name = "Steve"
|
||||
monster.size = "Medium"
|
||||
monster.type = "humanoid"
|
||||
monster.subType = "human"
|
||||
monster.alignment = "LG"
|
||||
monster.hitDice = 6
|
||||
monster.hasCustomHP = true
|
||||
monster.customHP = "12 (1d10)+2"
|
||||
monster.walkSpeed = 5
|
||||
monster.burrowSpeed = 10
|
||||
monster.climbSpeed = 15
|
||||
monster.flySpeed = 20
|
||||
monster.swimSpeed = 25
|
||||
monster.canHover = true
|
||||
monster.hasCustomSpeed = false
|
||||
monster.customSpeed = "walk: 5 ft."
|
||||
monster.strengthScore = 8
|
||||
monster.dexterityScore = 10
|
||||
monster.constitutionScore = 12
|
||||
monster.intelligenceScore = 14
|
||||
monster.wisdomScore = 16
|
||||
monster.charismaScore = 18
|
||||
monster.strengthSavingThrowAdvantage = AdvantageType.none
|
||||
monster.strengthSavingThrowProficiency = ProficiencyType.none
|
||||
monster.dexteritySavingThrowAdvantage = AdvantageType.advantage
|
||||
monster.dexteritySavingThrowProficiency = ProficiencyType.proficient
|
||||
monster.constitutionSavingThrowAdvantage = AdvantageType.disadvantage
|
||||
monster.constitutionSavingThrowProficiency = ProficiencyType.expertise
|
||||
monster.intelligenceSavingThrowAdvantage = AdvantageType.none
|
||||
monster.intelligenceSavingThrowProficiency = ProficiencyType.expertise
|
||||
monster.wisdomSavingThrowAdvantage = AdvantageType.advantage
|
||||
monster.wisdomSavingThrowProficiency = ProficiencyType.proficient
|
||||
monster.charismaSavingThrowAdvantage = AdvantageType.disadvantage
|
||||
monster.charismaSavingThrowProficiency = ProficiencyType.none
|
||||
monster.telepathy = 1
|
||||
monster.languages = [
|
||||
LanguageViewModel("English", true),
|
||||
LanguageViewModel("French", false)
|
||||
]
|
||||
|
||||
return Group {
|
||||
MonsterDetailView(viewModel: monster)
|
||||
}
|
||||
}
|
||||
}
|
||||
89
iOS/MonsterCards/Views/MonsterDetailWrapper.swift
Normal file
89
iOS/MonsterCards/Views/MonsterDetailWrapper.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// MonsterDetailWrapper.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 4/7/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MonsterDetailWrapper: View {
|
||||
let kTextColor = Color(hex: 0x982818)
|
||||
|
||||
@ObservedObject var monster: Monster
|
||||
@StateObject private var viewModel = MonsterViewModel()
|
||||
|
||||
var body: some View {
|
||||
|
||||
MonsterDetailView(viewModel: viewModel)
|
||||
.onAppear(perform: {
|
||||
viewModel.copyFromMonster(monster: monster)
|
||||
})
|
||||
.toolbar(content: {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
NavigationLink("Edit", destination: EditMonster(monster: monster))
|
||||
}
|
||||
})
|
||||
.navigationTitle(monster.name ?? "")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func editMonster() {
|
||||
print("Edit Monster pressed")
|
||||
}
|
||||
}
|
||||
|
||||
struct MonsterDetailWrapper_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let monster = Monster.init(context: context)
|
||||
monster.name = "Steve"
|
||||
monster.size = "Medium"
|
||||
monster.type = "humanoid"
|
||||
monster.subtype = "human"
|
||||
monster.alignment = "LG"
|
||||
monster.hitDice = 6
|
||||
monster.hasCustomHP = true
|
||||
monster.customHP = "12 (1d10)+2"
|
||||
monster.walkSpeed = 5
|
||||
monster.burrowSpeed = 10
|
||||
monster.climbSpeed = 15
|
||||
monster.flySpeed = 20
|
||||
monster.swimSpeed = 25
|
||||
monster.canHover = true
|
||||
monster.hasCustomSpeed = false
|
||||
monster.customSpeed = "walk: 5 ft."
|
||||
monster.strengthScore = 8
|
||||
monster.dexterityScore = 10
|
||||
monster.constitutionScore = 12
|
||||
monster.intelligenceScore = 14
|
||||
monster.wisdomScore = 16
|
||||
monster.charismaScore = 18
|
||||
monster.strengthSavingThrowAdvantageEnum = AdvantageType.none
|
||||
monster.strengthSavingThrowProficiencyEnum = ProficiencyType.none
|
||||
monster.dexteritySavingThrowAdvantageEnum = AdvantageType.advantage
|
||||
monster.dexteritySavingThrowProficiencyEnum = ProficiencyType.proficient
|
||||
monster.constitutionSavingThrowAdvantageEnum = AdvantageType.disadvantage
|
||||
monster.constitutionSavingThrowProficiencyEnum = ProficiencyType.expertise
|
||||
monster.intelligenceSavingThrowAdvantageEnum = AdvantageType.none
|
||||
monster.intelligenceSavingThrowProficiencyEnum = ProficiencyType.expertise
|
||||
monster.wisdomSavingThrowAdvantageEnum = AdvantageType.advantage
|
||||
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 {
|
||||
MonsterDetailWrapper(monster: monster)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.previewDevice("iPod touch (7th generation)")
|
||||
MonsterDetailWrapper(monster: monster)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.previewDevice("iPad Pro (11-inch) (2nd generation)")
|
||||
}
|
||||
}
|
||||
}
|
||||
75
iOS/MonsterCards/Views/Search.swift
Normal file
75
iOS/MonsterCards/Views/Search.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Search.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/15/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Search: View {
|
||||
@State private var searchText = ""
|
||||
|
||||
@Environment(\.managedObjectContext) var managedObjectContext
|
||||
@FetchRequest(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \Monster.name, ascending: true),
|
||||
],
|
||||
animation: .default)
|
||||
var allMonsters: FetchedResults<Monster>
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
SearchBar(text: $searchText)
|
||||
.padding(.top, -30)
|
||||
|
||||
ForEach(
|
||||
allMonsters.filter(
|
||||
{
|
||||
// TODO: consider splitting search text into words and if each word appears in any of these fields return true e.g, "large demon" would match large in size and demon in type.
|
||||
// TODO: add tags and search by tags
|
||||
// TODO: add a display of what fields matched on each item in the results
|
||||
// TODO: make the criteria configurable from this screen
|
||||
// TODO: find a way to add challenge rating as a search criteria
|
||||
|
||||
if (searchText.isEmpty) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (StringHelper.safeContainsCaseInsensitive($0.name, searchText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (StringHelper.safeContainsCaseInsensitive($0.size, searchText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (StringHelper.safeContainsCaseInsensitive($0.type, searchText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (StringHelper.safeContainsCaseInsensitive($0.subtype, searchText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (StringHelper.safeContainsCaseInsensitive($0.alignment, searchText)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})) { monster in
|
||||
NavigationLink(destination: MonsterDetailWrapper(monster: monster)) {
|
||||
Text(monster.name ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct Search_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Search().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
63
iOS/MonsterCards/Views/SearchBar.swift
Normal file
63
iOS/MonsterCards/Views/SearchBar.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// SearchBar.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 1/15/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct SearchBar: UIViewRepresentable {
|
||||
|
||||
@Binding var text: String
|
||||
|
||||
class Coordinator: NSObject, UISearchBarDelegate {
|
||||
@Binding var text: String
|
||||
|
||||
init(text: Binding<String>) {
|
||||
_text = text
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
text = searchText
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> SearchBar.Coordinator {
|
||||
return Coordinator(text: $text)
|
||||
}
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
|
||||
let searchBar = UISearchBar(frame: .zero)
|
||||
searchBar.delegate = context.coordinator
|
||||
searchBar.autocapitalizationType = .none
|
||||
return searchBar
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
|
||||
uiView.text = text
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func containsCaseInsensitive(_ string: String) -> Bool {
|
||||
return self.localizedCaseInsensitiveContains(string)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
func endEditing() {
|
||||
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
struct SearchBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchBar(text: .constant(""))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user