如何使 Picker onChange 更新选定的结构?

How to make Picker onChange update a selected struct?

提问人:Boris Ryabov 提问时间:7/15/2023 最后编辑:burnsiBoris Ryabov 更新时间:7/23/2023 访问量:58

问:

我有两个结构。其中一个结构有一个字段,用于存储另一个结构的实例。反过来,具有与之关联的结构的 ID 列表。IdeaOptionOptionIdea

我创建了一个 Picker,它将这个想法的 id 添加到 Option 中的关联想法列表中。Idea.Option

这会引发此错误。This throws this error.

代码如下

//ContentView
import SwiftUI

struct ContentView: View {
    
    var options: [Option] = [
        Option(name: "option1", assositatedIdea: []),
        Option(name: "option2", assositatedIdea: []),
        Option(name: "option3", assositatedIdea: [])
    ]
    
    @Binding var ideas: Idea
    
    
    var body: some View {
        Picker("Option: \(ideas.option.name)", selection: $ideas.option) {
            ForEach(options) {option in
                Text(option.name).tag(option)
            }
        }.onChange(of: ideas.option) {newOption in
            ideas.option.addAssosiatedIdea(ideaId: ideas.id)
        }
        
    }
}

struct Option: Hashable, Identifiable, Equatable {
    let id: UUID
    var name: String
    var assositatedIdea: [UUID] // Stores IDs of structs of Ideas
    
    init(id: UUID = UUID(), name: String, assositatedIdea: [UUID]) {
        self.id = id
        self.name = name
        self.assositatedIdea = assositatedIdea
        
    }
    
    mutating func addAssosiatedIdea(ideaId: UUID) {
        
        
        if (assositatedIdea.contains(ideaId)) {
            return
        }
        self.assositatedIdea.append(ideaId)
    }

}

extension Option {
    static var emptyOption: Option {
        Option(name: "", assositatedIdea: [])
    }
}

struct Idea {
    let id: UUID
    var name: String
    var option: Option
    
    init(id: UUID = UUID(), name: String, option: Option) {
        self.id = id
        self.name = name
        self.option = option
        
    }

}
// test1App.swift 
import SwiftUI

@main
struct test1App: App {
    
    @State var ideas: Idea = Idea(name: "big brain idea", option: Option.emptyOption)
    var body: some Scene {
        WindowGroup {
            ContentView(ideas: $ideas)
        }
    }
}

我希望 Picker 预览新选项。但是,Picker 会引用并显示 中的第一个选项。options

我做了一些打印调试,发现实际上 Idea 更新了其字段。

swift swiftui-picker

评论


答:

0赞 Benzy Neez 7/15/2023 #1

解释

首先,这是对错误的解释。

  • 结构的 id 类型为 。OptionUUID
  • 因此,每个人都有一个唯一但不可预测的 ID。Option
  • 您正在使用 的数组创建 ,该数组是视图的本地数组。PickerOptionContentView
  • 您正在将 绑定到 。Picker$ideas.option
  • 绑定值不会与数组中的任何值匹配,因为它肯定有自己的唯一 ID。Option
  • 这就是为什么给出错误“选择 [Option] 无效且没有关联的标签”的原因。Picker

两个可能不匹配的另一个原因是,每个都有一个数组。这些是关联的 .OptionOptionUUIDIdea

快速修复

  1. 解决这些问题的快速方法是确保两个本应相同的选项实际上相等。因此,您可以使用简单的 .为了忽略关联的 id,您还应该实现自己的相等函数:UUIDInt
struct Option: Hashable, Identifiable, Equatable {
    let id: Int // <- not UUID
    var name: String
    var assositatedIdea: [UUID] // Stores IDs of structs of Ideas

    init(id: Int, name: String, assositatedIdea: [UUID]) {
        self.id = id
        self.name = name
        self.assositatedIdea = assositatedIdea
    }

    static func ==(lhs: Option, rhs: Option) -> Bool {
        lhs.id == rhs.id
    }
  1. 空选项的 id 可以为 0:
    static var emptyOption: Option {
        Option(id: 0, name: "", assositatedIdea: [])
    }
  1. 即使进行了这些更改,选取器仍会抱怨绑定持有的选项与任何标记都不匹配,因为你使用的是空选项进行创建。因此,在可选择选项列表中包含一个空选项:ideas
    var options: [Option] = [
        Option(id: 0, name: "empty", assositatedIdea: []), // <- ADDED
        Option(id: 1, name: "option1", assositatedIdea: []),
        Option(id: 2, name: "option2", assositatedIdea: []),
        Option(id: 3, name: "option3", assositatedIdea: [])
    ]

替代模型

上面的更改使它正常工作,但它仍然有点混乱。特别是,保存 ID 集合的方式意味着这两个结构是相互依赖的。OptionIdea

以下是一些关于如何使数据模型和实现更简洁的建议:

  • 如果可能,使用枚举来定义可能的选项(如果选项是固定的并且事先知道,则有效)。
  • 尽可能使用代替。letvar
  • 与其让选项知道引用它们的想法,不如使用另一种新类型来模拟这种关系。
  • 与其收集想法的 ID (UUID),不如收集实际的想法。
  • Idea可能根本不需要 ID。

下面说明了如何以这种方式实现它。对 和 关联之间的关系进行建模的新类型是 ,它是一种类类型。该类是这些类的映射的包装器。OptionIdeasOptionAndIdeasAllOptionsAndIdeas

enum Option: Int, Identifiable {
    case empty = 0
    case option1 = 1
    case option2 = 2
    case option3 = 3

    var id: Int {
        self.rawValue
    }

    var asString: String {
        "\(self)"
    }
}

struct Idea: Equatable {
    let option: Option
    let name: String
}

class OptionAndIdeas {
    let option: Option
    var associatedIdeas: [Idea]

    init(option: Option, associatedIdeas: [Idea] = []) {
        self.option = option
        self.associatedIdeas = associatedIdeas
    }

    func addAssociatedIdea(idea: Idea) {
        if !associatedIdeas.contains(idea) {
            associatedIdeas.append(idea)
        }
    }

    func removeAssociatedIdea(idea: Idea) {
        if let index = associatedIdeas.firstIndex(of: idea) {
            associatedIdeas.remove(at: index)
        }
    }
}

class AllOptionAndIdeas {

    /// A map of OptionAndIdeas, keyed by option
    private var allOptionsAndIdeas = [Option: OptionAndIdeas]()

    func switchIdeaToOption(idea: Idea, option: Option) -> Idea {

        // Remove the idea from its previous association
        if let optionAndIdeas = allOptionsAndIdeas[idea.option] {
            optionAndIdeas.removeAssociatedIdea(idea: idea)
        }
        // Re-create the idea, associating it with the new option
        let result = Idea(option: option, name: idea.name)

        // Update the associations between options and ideas
        let optionAndIdeas = allOptionsAndIdeas[option] ?? OptionAndIdeas(option: option)
        optionAndIdeas.addAssociatedIdea(idea: result)
        allOptionsAndIdeas[option] = optionAndIdeas

        return result
    }
}

struct PickerExample: View {

    private let options: [Option] = [ .empty, .option1, .option2, .option3 ]

    @Binding private var idea: Idea
    private let allOptionsAndIdeas: AllOptionAndIdeas
    @State private var selectedOption: Option

    init(idea: Binding<Idea>, allOptionsAndIdeas: AllOptionAndIdeas) {
        self._idea = idea
        self.allOptionsAndIdeas = allOptionsAndIdeas
        self._selectedOption = State(initialValue: idea.wrappedValue.option)
    }

    var body: some View {
        Picker("Option", selection: $selectedOption) {
            ForEach(options) { option in
                Text(option.asString).tag(option)
            }
        }
        .onChange(of: selectedOption) { newOption in

            // Switch to the selected option
            idea = allOptionsAndIdeas.switchIdeaToOption(idea: idea, option: newOption)
        }
    }
}

struct ContentView: View {
    private let allOptionsAndIdeas = AllOptionAndIdeas()
    @State private var bigBrainIdea = Idea(option: .empty, name: "big brain idea")

    var body: some View {
        PickerExample(idea: $bigBrainIdea, allOptionsAndIdeas: allOptionsAndIdeas)
    }
}

评论

0赞 Boris Ryabov 7/16/2023
由于在我的项目中,两者都是用户制作的,因此我决定将这两者绑定为可选的数据结构。绑定将在 App init 上进行。谢谢你的提示!IdeaOption