SwiftUI 分层选取器与动态数据崩溃

标签 swift dynamic swiftui picker

我刚刚开始使用 SwiftUI,在管理多个具有动态数据的选取器时遇到一些困难。 在这种情况下,有两个选择器,分别用于国家/地区城市。 当我尝试从城市数量多的国家/地区切换选择器时,应用程序会崩溃:

Fatal error: Index out of range

知道如何解决这个问题吗?

App Screenshot

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
}

struct ContentView: View {

    @State var selectedCountry = 0
    @State var selectedCity = 0

    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")])]

    var body: some View {
        VStack {
            Picker(selection: $selectedCountry,label: Text("")){
                ForEach(0 ..< countries.count){ index in
                    Text(self.countries[index].name)
                }
            }.labelsHidden()
            .clipped()
            Picker(selection: $selectedCity,label: Text("")){
                ForEach(0 ..< countries[selectedCountry].cities.count){ index in
                    Text(self.countries[self.selectedCountry].cities[index].name)
                }
            }.labelsHidden()
            .clipped()
        }
    }
}

最佳答案

诀窍是当您选择不同的国家/地区时“重新创建”“从属”选择器

在您的示例中,用户所做的选择更改了状态变量,但 SwiftUI 将仅重新创建取决于此状态变量的 View 。 SwiftUI 不知道为什么要重新创建第二个选取器 View 。我“手动”完成了它,通过调用它的 .id() 以防必须完成(国家已更改)

Apple 向我们提供了有关 View.id() 的哪些信息..

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {

    /// Returns a view whose identity is explicitly bound to the proxy
    /// value `id`. When `id` changes the identity of the view (for
    /// example, its state) is reset.
    @inlinable public func id<ID>(_ id: ID) -> some View where ID : Hashable

}

这是“完整”的单 View iOS 应用程序,请小心,它不会在 Playground 中运行

//
//  ContentView.swift
//  tmp034
//
//  Created by Ivo Vacek on 05/02/2020.
//  Copyright © 2020 Ivo Vacek. NO rights reserved.
//

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 {
            selectedCity = 0
            id = UUID()
            print("country changed")
        }
    }
    @Published var id: UUID = UUID()
    @Published var selectedCity: Int = 0
    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
        }
    }
}

struct ContentView: View {
    @ObservedObject var model = Model()
    var body: some View {

        return VStack {
            Picker(selection: $model.selectedContry, label: Text("")){
                ForEach(0 ..< model.countryNemes.count){ index in
                    Text(self.model.countryNemes[index])
                }
            }.labelsHidden()
            .clipped()
            Picker(selection: $model.selectedCity, label: Text("")){
                ForEach(0 ..< model.cityNamesCount){ index in
                    Text(self.model.cityNames[index])
                }
            }
            // !! changing views id force SwiftUI to recreate it !!
            .id(model.id)

            .labelsHidden()
            .clipped()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

在这里你可以看到结果 enter image description here

更新

如果当前的城市选择能够在不同的国家/地区选择之间保持不变,那就更好了。

让我们尝试更新我们的模型和逻辑。

首先添加存储

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

接下来使用新版本更新模型

@Published var selectedContry: Int = 0 {
        willSet {
            print("country changed", newValue, citySelections[newValue] ?? 0)
            selectedCity = citySelections[newValue] ?? 0
            id = UUID()
        }
    }
@Published var selectedCity: Int = 0 {
        willSet {
            DispatchQueue.main.async { [newValue] in
                print("city changed", newValue)
                self.citySelections[self.selectedContry] = newValue
            }
        }
    }

还有胡拉!!!现在好多了! 也许你会问为什么

DispatchQueue.main.async { [newValue] in
                    print("city changed", newValue)
                    self.citySelections[self.selectedContry] = newValue
                }

答案很简单。 “重新创建”第二个选取器将重置其内部状态,并且由于其选择绑定(bind)到我们的模型,因此它将重置为其初始状态。诀窍是在 SwiftUI 重新创建该属性后推迟更新该属性。

enter image description here

关于SwiftUI 分层选取器与动态数据崩溃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60082411/

相关文章:

java 多态性不起作用,我错过了什么?

ios - FirebaseUI 和 SwiftUI 登录

ios - 重载单一平等在Swift中

ios - UICollectionViewCell 无共同祖先错误(不重复)

python - 如何根据 page1 上的选择在 page2 上生成动态 MultipleChoiceField 选项?

c# - 如何处理动态表名

ios - 当ObservedObject更改时,在SwiftUI View 中不会调用onReceive

SwiftUI:路径.anchorPreference

ios - 当我长按 UITableView 自定义标题单元格时应用程序崩溃

ios - 匹配 Swift 中对象的数据类型