外部模型对象的 SwiftUI 数据流和 UI 更新,其值发生变化但 id 相同
Posted
技术标签:
【中文标题】外部模型对象的 SwiftUI 数据流和 UI 更新,其值发生变化但 id 相同【英文标题】:SwiftUI data flow and UI update for foreign model object with changing values but same id 【发布时间】:2020-07-10 12:59:22 【问题描述】:我有一些第三方库的对象(“事物”)数组,我想在 SwiftUI 视图中显示它们。 这些“事物”对象可以通过 id 识别和散列,但是当重新加载一组新事物时,它们的内容可能已经改变(比如说那个事物的“状态”或“文本”,尽管它又是同一个事物)。所以 id 保持不变,但 Thing 的内容可以改变。 问题是,当我得到一个新的事物数组时,SwiftUI 不会更新 UI。我认为这是因为事物被它们的 id 再次“识别”为相同的事物。 我无法更改 Thing,因为它来自第三方库。
现在我只是简单地将 Thing 包装到另一个类中,突然它就可以工作了!但我想了解为什么这行得通,以及它是否是已定义的行为,而不仅仅是巧合或“运气”。
谁能解释一下幕后发生了什么?特别是 DirectThingView 和 WrappedThingView 之间导致 SwiftUI 为后者而不是前者更新 UI 的主要区别是什么?
或者有什么建议可以更好地解决这个问题?
这是显示所有内容的示例代码: 它在两列中显示事物;第一列使用 DirectThingView,第二列使用 WrappedThingView。如果您点击“重新加载”按钮,事物数组将被更改的事物填充,但只有右列的 UI 正确更新值;左列始终保持其初始状态。
//
// TestView.swift
//
// Created by Manfred Schwind on 10.07.20.
// Copyright © 2020 mani.de. All rights reserved.
//
import SwiftUI
// The main model contains an array of "Things",
// every Thing has an id and contains a text.
// For testing purposes, every other time a Thing gets instantiated, its text contains either "A" or "B".
// Problem here: the "current" text of a Thing with the same id can change, when Things are reloaded.
class TestViewModel: ObservableObject
@Published var things = [Thing(id: 1), Thing(id: 2), Thing(id: 3)]
struct TestView: View
@ObservedObject var viewModel = TestViewModel()
var body: some View
VStack (spacing: 30)
HStack (spacing: 40)
// We try to display the current Thing array in the UI
// The views in the first column directly store the Thing:
// Problem here: the UI does not update for changed Things ...
VStack
Text("Direct")
ForEach(self.viewModel.things, id: \.self) thing in
DirectThingView(viewModel: thing)
// The views in the second column store the Thin wrapped into another class:
// In this case, the problem magically went away!
VStack
Text("Wrapped")
ForEach(self.viewModel.things, id: \.self) thing in
WrappedThingView(viewModel: thing)
Button(action:
// change the Thing array in the TestViewModel, this causes the UI to update:
self.viewModel.things = [Thing(id: 1), Thing(id: 2), Thing(id: 3)]
)
Text("Reload")
struct DirectThingView: View
// first approach just stores the passed Thing directly internally:
private let viewModel: Thing
init(viewModel: Thing)
self.viewModel = viewModel
var body: some View
Text(self.viewModel.text)
struct WrappedThingView: View
// second approach stores the passed Thing wrapped into another Object internally:
private let viewModel: WrappedThing
init(viewModel: Thing)
// take the Thing like in the first approach, but internally store it wrapped:
self.viewModel = WrappedThing(childModel: viewModel)
var body: some View
Text(self.viewModel.childModel.text)
// If type of WrappedThing is changed from class to struct, then the problem returns!
private class WrappedThing
let childModel: Thing
init(childModel: Thing)
self.childModel = childModel
// Thing has do be Identifiable and Hashable for ForEach to work properly:
class Thing: Identifiable, Hashable
// Identifiable:
let id: Int
// The text contains either "A" or "B", in alternating order on every new Thing instantiation
var text: String
init(id: Int)
self.id = id
struct Holder
static var flip: Bool = false
self.text = Holder.flip ? "B" : "A"
Holder.flip = !Holder.flip
// Hashable:
public func hash(into hasher: inout Hasher)
hasher.combine(self.id)
// Equatable (part of Hashable):
public static func == (lhs: Thing, rhs: Thing) -> Bool
return lhs.id == rhs.id
#if DEBUG
struct TestView_Previews: PreviewProvider
static var previews: some View
TestView()
#endif
非常感谢您!
【问题讨论】:
【参考方案1】:我自己找到了答案。问题是这里的 Equatable of Thing 的实现。如上所述,同一事物(但内容不同)的旧版本和新版本被认为是相同的。但是 SwiftUI 区分了“身份”和“平等”,这必须正确实现。
在上面的代码中, Identifiable 和 Hashable 都可以,但 Equatable 必须更改为更精确。所以例如这解决了问题:
// Thing has do be Identifiable and Hashable for ForEach to work properly:
class Thing: Identifiable, Hashable
// Identifiable:
let id: Int
// The text contains either "A" or "B", in alternating order on every new Thing instantiation
var text: String
init(id: Int)
self.id = id
struct Holder
static var flip: Bool = false
self.text = Holder.flip ? "B" : "A"
Holder.flip = !Holder.flip
// Hashable:
public func hash(into hasher: inout Hasher)
// we are lazy and just use the id here, "collisions" are then separated by func ==
hasher.combine(self.id)
// Equatable (part of Hashable):
public static func == (lhs: Thing, rhs: Thing) -> Bool
// We are lazy again (in reality Thing has many properties) and we consider
// two Things to be the equal ONLY when they point to the same address.
// So we get the "same but different" semantic that we want, when we are
// getting a new version of the Thing.
// (Same in the sense of identity, different in the sense of equality)
return lhs === rhs
【讨论】:
以上是关于外部模型对象的 SwiftUI 数据流和 UI 更新,其值发生变化但 id 相同的主要内容,如果未能解决你的问题,请参考以下文章
如何为不同环境的 SwiftUI App 生命周期应用程序运行 UI 测试?