具有动态数据崩溃的 SwiftUI 分层选择器
SwiftUI hierarchical Picker with dynamic data crashes
我刚开始使用 SwiftUI,我在使用动态数据管理多个 Pickers 时遇到了一些困难。
在这种情况下,有两个选择器,用于 Country 和 City。
当我尝试从一个城市比另一个国家多的国家/地区切换选择器时,应用程序会崩溃:
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()
}
}
}
诀窍是 "recreate" "slave" 选择器,当你 select 一个不同的国家
在您的示例中 selection 由用户更改状态变量,但 SwiftUI 将仅重新创建取决于此状态变量的视图。 SwiftUI 不知道为什么要重新创建第二个选择器视图。我做到了 "manually",通过调用它的 .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
}
这是"full" single View iOS app,小心,它不会运行在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()
}
}
在这里你可以看到结果
更新
如果当前城市 selection 在不同国家 selection 之间持续存在,那就更好了。
让我们尝试更新我们的模型和逻辑。
首先添加存储
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
}
答案很简单。 "recreating" 第二个 Picker 将重置其内部状态,并且由于其 selection 已绑定到我们的模型,因此它将重置为初始状态。诀窍是在 SwiftUI 重新创建它之后推迟 属性 的更新。
我刚开始使用 SwiftUI,我在使用动态数据管理多个 Pickers 时遇到了一些困难。 在这种情况下,有两个选择器,用于 Country 和 City。 当我尝试从一个城市比另一个国家多的国家/地区切换选择器时,应用程序会崩溃:
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()
}
}
}
诀窍是 "recreate" "slave" 选择器,当你 select 一个不同的国家
在您的示例中 selection 由用户更改状态变量,但 SwiftUI 将仅重新创建取决于此状态变量的视图。 SwiftUI 不知道为什么要重新创建第二个选择器视图。我做到了 "manually",通过调用它的 .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
}
这是"full" single View iOS app,小心,它不会运行在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()
}
}
在这里你可以看到结果
更新
如果当前城市 selection 在不同国家 selection 之间持续存在,那就更好了。
让我们尝试更新我们的模型和逻辑。
首先添加存储
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
}
答案很简单。 "recreating" 第二个 Picker 将重置其内部状态,并且由于其 selection 已绑定到我们的模型,因此它将重置为初始状态。诀窍是在 SwiftUI 重新创建它之后推迟 属性 的更新。