Hi All - trying to get something figured out for my macOS app I'm working on. The app is a pokemon tracker which I was really hoping would be an easy first app...unfortunately it's turned into anything but.
The goal is to click checkboxes to save whether or not the pokemon has been caught / collected. The issues I'm running into are:
- When the app first starts I'm loading all the Pokemon into SwiftData through a JSON file...this obviously takes a lot of time and hangs the app right on first launch which isn't fun, but I can't find a different way to do it?
- Loading the list of pokemon is slow and scrolling is very stuttery....I've tried a million different ways and you can probably see remnants of some of these ways in my code but I can't seem to make this any better.
I left out some of the inits and other general Model code. Suffice to say the app runs and "works" as it exists today
Any help that's possible would be really appreciated. This is my first SwiftUI app and I'm fairly embarassed by how difficult it's being...
@Model
class PokeList {
var name: String
var view: String
var stdPokemon: [PokeListData] = []
var femPokemon: [PokeListData] = []
var formPokemon: [PokeListData] = []
var regionPokemon: [PokeListData] = []
var stdPokemonShiny: [PokeListData] = []
var femPokemonShiny: [PokeListData] = []
var formPokemonShiny: [PokeListData] = []
var regionPokemonShiny: [PokeListData] = []
}
@Model
class PokeListData {
var isShiny: Bool
var collected: Bool
var preEvolveCollected: Bool
var order: Int
var pokemon: Pokemon
}
@Model
class Pokemon {
var national: Int
var name: String
var generation: Int
var priority: Int
var typeOne: String
var typeTwo: String?
var gender: String?
var form: String?
var regionalForm: String?
}
// in my ContentView.onAppear I call these functions if no data exists
func createPokemonCoreData() {
do {
try modelContext.delete(model: Pokemon.self)
try modelContext.delete(model: PokeList.self)
guard let url = Bundle.main.url(forResource: "pokemon", withExtension: "json") else {
fatalError("Failed to find pokemon.json")
}
let data = try Data(contentsOf: url)
let pokemon = try JSONDecoder().decode(PokeData.self, from: data)
for mon in pokemon.data {
let priority = mon.priority == nil ? 1 : Int(mon.priority!)
let newMon = Pokemon(national: Int(mon.national), name: mon.name, generation: Int(mon.generation), priority: priority, typeOne: mon.typeOne, typeTwo: mon.typeTwo, gender: mon.gender, form: mon.form, regionalForm: mon.regionalForm)
modelContext.insert(newMon)
}
} catch {
debugPrint(error)
print("Failed to create data: \(error.localizedDescription)")
}
}
func createNationalDexList() {
let natDexList = PokeList(name: "National Dex")
modelContext.insert(natDexList)
//let pokeSorted = pokemon.sorted(by: sortByNationalPriority)
var counter = 0
for mon in pokemon {
if mon.gender != nil && mon.priority != 1 {
natDexList.femPokemon.append(PokeListData(isShiny: false, order: counter, pokemon: mon))
natDexList.femPokemonShiny.append(PokeListData(isShiny: true, order: counter, pokemon: mon))
} else if mon.form != nil && mon.priority != 1 {
natDexList.formPokemon.append(PokeListData(isShiny: false, order: counter, pokemon: mon))
natDexList.formPokemonShiny.append(PokeListData(isShiny: true, order: counter, pokemon: mon))
} else if mon.regionalForm != nil && mon.priority != 1 {
natDexList.regionPokemon.append(PokeListData(isShiny: false, order: counter, pokemon: mon))
natDexList.regionPokemonShiny.append(PokeListData(isShiny: true, order: counter, pokemon: mon))
} else {
natDexList.stdPokemon.append(PokeListData(isShiny: false, order: counter, pokemon: mon))
natDexList.stdPokemonShiny.append(PokeListData(isShiny: true, order: counter, pokemon: mon))
}
counter += 1
}
}
// My main highlevel view
var body: some View {
ScrollViewReader { scroll in
VStack {
HStack {
if(selectedPokeList.stdPokemon.count > 0) {
Button("Std Poke") {
scroll.scrollTo("Standard")
}
}
if(selectedPokeList.femPokemon.count > 0) {
Button("Fem Poke") {
scroll.scrollTo("Female")
}
}
if(selectedPokeList.formPokemon.count > 0) {
Button("Form Poke") {
scroll.scrollTo("Forms")
}
}
if(selectedPokeList.regionPokemon.count > 0) {
Button("Regional Poke") {
scroll.scrollTo("Regional")
}
}
}
HStack {
if(selectedPokeList.stdPokemonShiny.count > 0) {
Button("Std Poke Shiny") {
scroll.scrollTo("Standard Shiny")
}
}
if(selectedPokeList.femPokemonShiny.count > 0) {
Button("Fem Poke Shiny") {
scroll.scrollTo("Female Shiny")
}
}
if(selectedPokeList.formPokemonShiny.count > 0) {
Button("Form Poke Shiny") {
scroll.scrollTo("Forms Shiny")
}
}
if(selectedPokeList.regionPokemonShiny.count > 0) {
Button("Regional Poke Shiny") {
scroll.scrollTo("Regional Shiny")
}
}
}
if selectedPokeList.view == "List" {
List {
if(selectedPokeList.stdPokemon.count > 0) {
Section(header: Text("Standard").font(.title)) {
ForEach(selectedPokeList.stdPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Standard")
}
if(selectedPokeList.femPokemon.count > 0) {
Section(header: Text("Female").font(.title)) {
ForEach(selectedPokeList.femPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Female")
}
if(selectedPokeList.formPokemon.count > 0) {
Section(header: Text("Forms").font(.title)) {
ForEach(selectedPokeList.formPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Forms")
}
if(selectedPokeList.regionPokemon.count > 0) {
Section(header: Text("Regional").font(.title)) {
ForEach(selectedPokeList.regionPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Regional")
}
if(selectedPokeList.stdPokemonShiny.count > 0) {
Section(header: Text("Standard Shiny").font(.title)) {
ForEach(selectedPokeList.stdPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Standard Shiny")
}
if(selectedPokeList.femPokemonShiny.count > 0) {
Section(header: Text("Female Shiny").font(.title)) {
ForEach(selectedPokeList.femPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Female Shiny")
}
if(selectedPokeList.formPokemonShiny.count > 0) {
Section(header: Text("Forms Shiny").font(.title)) {
ForEach(selectedPokeList.formPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Forms Shiny")
}
if(selectedPokeList.regionPokemonShiny.count > 0) {
Section(header: Text("Regional Shiny").font(.title)) {
ForEach(selectedPokeList.regionPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeRowView(pokeData: mon)
}
}
.id("Regional Shiny")
}
}
} else if selectedPokeList.view == "Grid" {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150, maximum: 250))]) {
if(selectedPokeList.stdPokemon.count > 0) {
Section(header: Text("Standard").font(.title)) {
ForEach(selectedPokeList.stdPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Standard")
}
if(selectedPokeList.femPokemon.count > 0) {
Section(header: Text("Female").font(.title)) {
ForEach(selectedPokeList.femPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Female")
}
if(selectedPokeList.formPokemon.count > 0) {
Section(header: Text("Forms").font(.title)) {
ForEach(selectedPokeList.formPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Forms")
}
if(selectedPokeList.regionPokemon.count > 0) {
Section(header: Text("Regional").font(.title)) {
ForEach(selectedPokeList.regionPokemon.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Regional")
}
if(selectedPokeList.stdPokemonShiny.count > 0) {
Section(header: Text("Standard Shiny").font(.title)) {
ForEach(selectedPokeList.stdPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Standard Shiny")
}
if(selectedPokeList.femPokemonShiny.count > 0) {
Section(header: Text("Female Shiny").font(.title)) {
ForEach(selectedPokeList.femPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Female Shiny")
}
if(selectedPokeList.formPokemonShiny.count > 0) {
Section(header: Text("Form Shiny").font(.title)) {
ForEach(selectedPokeList.formPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Forms Shiny")
}
if(selectedPokeList.regionPokemonShiny.count > 0) {
Section(header: Text("Regional Shiny").font(.title)) {
ForEach(selectedPokeList.regionPokemonShiny.sorted(by: sortByCounter), id: \.self) { mon in
PokeGridView(pokeData: mon)
}
}
.id("Regional Shiny")
}
}
}
}
}
}
// my individual pokemon views
var body: some View {
HStack {
Image(pokeData.pokemon.getImageName(isShiny: pokeData.isShiny))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
VStack {
Text(pokeData.pokemon.name)
.font(.title)
if let form = pokeData.pokemon.form {
Text(form)
.font(.title2)
}
if let gender = pokeData.pokemon.gender {
Text(gender)
.font(.title2)
}
if let regional = pokeData.pokemon.regionalForm {
Text(regional)
.font(.title2)
}
}
.padding(.horizontal)
VStack {
Text("Type One: \(pokeData.pokemon.typeOne)")
if let typeTwo = pokeData.pokemon.typeTwo {
Text("Type Two: \(typeTwo)")
}
if pokeData.isShiny {
Spacer()
Image(systemName: "sparkles")
}
}
.padding(.horizontal)
VStack{
Toggle(isOn: $pokeData.collected) {
Text("Collected")
}
Toggle(isOn: $pokeData.preEvolveCollected) {
Text("Pre-Evolution Collected")
}
}
.padding(.horizontal)
}
}
var body: some View {
VStack {
Image(pokeData.pokemon.getImageName(isShiny: pokeData.isShiny))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
Text(pokeData.pokemon.name)
.font(.title)
if let form = pokeData.pokemon.form {
Text(form)
.font(.title2)
}
if let gender = pokeData.pokemon.gender {
Text(gender)
.font(.title2)
}
if let regional = pokeData.pokemon.regionalForm {
Text(regional)
.font(.title2)
}
Spacer()
Text("\(pokeData.pokemon.typeOne)\(pokeData.pokemon.typeTwo != nil ? pokeData.pokemon.typeTwo! : "")")
HStack {
Toggle(isOn: $pokeData.collected) {
Image(systemName: "person.circle")
}
Toggle(isOn: $pokeData.preEvolveCollected) {
Image(systemName: "person.2.fill")
}
}
}
}