GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

Performance Struggles With SwiftData and List

Forums > SwiftUI

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:

  1. 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?
  2. 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")
                }
            }
        }
    }

   

  1. Use .task { } instead of onAppear to not block the main thread.. you can then use a SwiftData background context to pre-load. You can also use a ProgressView to show loading state in the UI. https://www.hackingwithswift.com/quick-start/swiftdata/how-to-create-a-background-context

  2. Instead of a List use a LazyVStack + ScrollView https://developer.apple.com/documentation/swiftui/creating-performant-scrollable-stacks

   

@shoom  

He @redwolfplayer . Did you found solution? Because I faced up with the same problem. When I populate context it works very slow. If I close app and run again - everyting is ok.

   

Hacking with Swift is sponsored by try! Swift Tokyo.

SPONSORED Ready to dive into the world of Swift? try! Swift Tokyo is the premier iOS developer conference will be happened in April 9th-11th, where you can learn from industry experts, connect with fellow developers, and explore the latest in Swift and iOS development. Don’t miss out on this opportunity to level up your skills and be part of the Swift community!

Get your ticket here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.