SwiftUI hierarchical Picker with dynamic data

2020-07-22 17:17发布

问题:

I am trying to use multiple pickers with dynamic data in SwiftUI (XCode 11.3.1). The app sometimes crashes and sometimes freezes or shows the wrong data in the picker both in the simulator and on a real device running iOS 13.3.1. I tried the suggestions in the answers to this question with no success. What am I doing wrong?

import SwiftUI

struct DbItem : Identifiable {
    var id: Int
    var name: String
}

final class DbStore : ObservableObject {

    let countries: [DbItem] = [DbItem(id: 0, name: "USA"), DbItem(id: 1, name: "France")]
    let citymap:[Int:[DbItem]] = [0:[DbItem(id: 10, name: "New York"), DbItem(id: 11, name: "Los Angeles"), DbItem(id: 12, name: "Dallas"), DbItem(id: 13, name: "Chicago")], 1:[DbItem(id: 20, name: "Paris"), DbItem(id: 21, name: "Nice"), DbItem(id: 22, name: "Lille")]]

    @Published var cities = [DbItem]()
    @Published var country : Int = -1 {
        willSet {
            if newValue >= 0 {
                self.id = UUID()
                DispatchQueue.main.async { [newValue] in
                    self.cities = self.citymap[newValue]!
                }
            }
        }
    }
    @Published var city : Int = -1 {
        didSet {
        }
    }
}

struct ContentView: View {
    @EnvironmentObject private var store: DbStore

    var body: some View {
        NavigationView {
            Form {
                VStack {
                    Picker(selection: $store.country,
                           label: Text("Country: ")
                    ) {
                        ForEach(store.countries) { country in
                            Text(country.name)
                        }
                    }
                    Picker(selection: $store.city,
                           label: Text("City: ")
                    ) {
                        ForEach(store.cities) { city in
                            Text(city.name)
                        }
                    }
                    .disabled(store.country < 0)
                }
            }
        }
    }
}

回答1:

Using the same code as provided in link mentioned in your question I made some small changes to adopt the code for your needs

struct ContentView: View {
    @ObservedObject var model = Model()
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Header").font(.title)) {
                    Picker(selection: $model.selectedContry, label: Text("Country")){
                        ForEach(0 ..< model.countryNemes.count){ index in
                            Text(self.model.countryNemes[index])
                        }
                    }
                    Picker(selection: $model.selectedCity, label: Text("City")){
                        ForEach(0 ..< model.cityNamesCount){ index in
                            Text(self.model.cityNames[index])
                        }
                        }
                    .id(model.id)
                }
            }.navigationBarTitle("Navigation Title")
        }
    }
}

Please see, that there is no VStack but Section in the Form! The result works as expected. (the rest of code is without any changes). Try the code on real device (due the known "back button" bug in simulator)

In case you have some trouble with the rest of code, here it is

import Foundation
import SwiftUI

struct Country: Identifiable {
    var id: Int = 0
    var name: String
    var cities: [City]
}

struct City: Identifiable {
    var id: Int = 0
    var name: String
}

class Model: ObservableObject {
    let countries: [Country] = [Country(id: 0, name: "USA", cities: [City(id: 0, name: "New York"),City(id: 1, name: "Los Angeles"),City(id: 2, name: "Dallas"),City(id: 3, name: "Chicago")]),Country(id: 1, name: "France", cities: [City(id: 0, name: "Paris")])]

    @Published var selectedContry: Int = 0 {
        willSet {
            print("country changed", newValue, citySelections[newValue] ?? 0)
            selectedCity = citySelections[newValue] ?? 0
            id = UUID()
        }
    }
    @Published var id: UUID = UUID()
    @Published var selectedCity: Int = 0 {
        willSet {
            DispatchQueue.main.async { [newValue] in
                print("city changed", newValue)
                self.citySelections[self.selectedContry] = newValue
            }
        }
    }
    var countryNemes: [String] {
        countries.map { (country) in
            country.name
        }
    }
    var cityNamesCount: Int {
        cityNames.count
    }
    var cityNames: [String] {
        countries[selectedContry].cities.map { (city) in
            city.name
        }
    }

    private var citySelections: [Int: Int] = [:]
}