带有动态数据崩溃的SwiftUI分层选择器 [英] SwiftUI hierarchical Picker with dynamic data crashes

查看:91
本文介绍了带有动态数据崩溃的SwiftUI分层选择器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我刚刚开始使用SwiftUI,并且在管理带有动态数据的多个Picker时遇到了一些困难. 在这种情况下,有两个选择器,分别用于国家/地区城市. 当我尝试从一个城市比另一个城市多的国家/地区切换选择器时,该应用将崩溃:

I just started working on SwiftUI and I have some difficulty to manage several Pickers with dynamic data. In that case there's two Pickers, for Country and City. When I try to switch the Picker from a country with more cities than the other, the app would crash :

致命错误:索引超出范围

Fatal error: Index out of range

有什么主意我该如何解决?

Any idea how I could fix that ?

应用屏幕截图

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

推荐答案

诀窍是在选择其他国家/地区时重新创建"从属"选择器

The trick is to "recreate" the "slave" picker when you select a different country

在您的示例中,用户选择了更改状态变量,但是SwiftUI将仅重新创建依赖于此状态变量的视图. SwiftUI不知道为什么要重新创建第二个Picker View.我通过手动调用它的.id()来做到这一点,以防万一必须这样做(更改了信使)

In your example selection made by user change the state variable, but SwiftUI will recreate only view which is depending on this state variable. SwiftUI don't have any idea why to recreate the second Picker View. I did it "manually", by calling its .id() in case it must be done (the coutry was changed)

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

What information Apple gives us about 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中运行

This is "full" single View iOS app, be careful, it will not run in 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()
    }
}

在这里您可以看到结果

here you can see the result

更新

如果当前的城市选择在不同的国家选择之间保持不变,那就更好了.

It could be even better if the current city selection will persist between different country selections.

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

Lets try to update our model and logic.

首先添加存储空间

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

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

And HURRA!!! Now it is much more better! Maybe you ask why

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

答案很简单. 重新创建"第二个Picker将重置其内部状态,并且由于其选择已绑定到我们的模型,因此它将被重置为其初始状态.诀窍是在SwiftUI重新创建后,推迟更新此属性.

The answer is simple. "recreating" second Picker will reset its internal state, and because its selection is bind to our model, it will be reset to its initial state. The trick is postpone update of this property AFTER SwiftUI recreate it.

这篇关于带有动态数据崩溃的SwiftUI分层选择器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆