Add 'iOS/' from commit '938f0fb75860d3637b998bdd0c27dcffd9fc9451'

git-subtree-dir: iOS
git-subtree-mainline: c4bb775af4
git-subtree-split: 938f0fb758
This commit is contained in:
Tom Hicks
2025-06-30 12:55:22 -07:00
135 changed files with 7654 additions and 0 deletions

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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")
}
}

View 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)
}
}

View 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")
}
}

View 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))
}
}
}

View 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)
}
}

View 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))
}
}

View 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))
}
}

View 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)
)
}
}

View 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))
}
}

View 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))
}
}

View 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))
}
}

View 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"))
}
}

View 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)
}
}
}

View 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)")
}
}
}

View 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)
}
}

View 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(""))
}
}