如何告诉 SwiftUI 视图绑定到嵌套的 ObservableObjects
Posted
技术标签:
【中文标题】如何告诉 SwiftUI 视图绑定到嵌套的 ObservableObjects【英文标题】:How to tell SwiftUI views to bind to nested ObservableObjects 【发布时间】:2020-02-12 19:41:12 【问题描述】:我有一个 SwiftUI 视图,它接收一个名为 appModel
的 EnvironmentObject。然后它在其body
方法中读取值appModel.submodel.count
。我希望这会将我的视图绑定到submodel
上的属性count
,以便在属性更新时重新渲染,但这似乎不会发生。
这是一个错误吗?如果没有,在 SwiftUI 中将视图绑定到环境对象的嵌套属性的惯用方式是什么?
具体来说,我的模型是这样的……
class Submodel: ObservableObject
@Published var count = 0
class AppModel: ObservableObject
@Published var submodel: Submodel = Submodel()
而我的观点是这样的……
struct ContentView: View
@EnvironmentObject var appModel: AppModel
var body: some View
Text("Count: \(appModel.submodel.count)")
.onTapGesture
self.appModel.submodel.count += 1
当我运行应用程序并单击标签时,count
属性确实增加了,但标签没有更新。
我可以通过将appModel.submodel
作为属性传递给ContentView
来解决此问题,但我希望尽可能避免这样做。
【问题讨论】:
我也在这样设计我的应用程序。在过去的应用程序开发中,我通常有一个全局 App 对象。有没有人认为这种将超级“App”类作为环境变量的设计将成为标准做法?我也在考虑使用多个 EnvironmentObject,但这很难维护。 【参考方案1】:您可以在您的顶视图中创建一个与您的***类中的函数或已发布 var 相同的 var。然后传递它并将其绑定到每个子视图。如果它在任何子视图中发生变化,那么顶视图将被更新。
代码结构:
struct Expense : Identifiable
var id = UUID()
var name: String
var type: String
var cost: Double
var isDeletable: Bool
class Expenses: ObservableObject
@Published var name: String
@Published var items: [Expense]
init()
name = "John Smith"
items = [
Expense(name: "Lunch", type: "Business", cost: 25.47, isDeletable: true),
Expense(name: "Taxi", type: "Business", cost: 17.0, isDeletable: true),
Expense(name: "Sports Tickets", type: "Personal", cost: 75.0, isDeletable: false)
]
func totalExpenses() -> Double
class ExpenseTracker: ObservableObject
@Published var name: String
@Published var expenses: Expenses
init()
name = "My name"
expenses = Expenses()
func getTotalExpenses() -> Double
观看次数:
struct MainView: View
@ObservedObject var myTracker: ExpenseTracker
@State var totalExpenses: Double = 0.0
var body: some View
NavigationView
Form
Section (header: Text("Main"))
HStack
Text("name:")
Spacer()
TextField("", text: $myTracker.name)
.multilineTextAlignment(.trailing)
.keyboardType(.default)
NavigationLink(destination: ContentView(myExpenses: myTracker.expenses, totalExpenses: $totalExpenses),
label:
Text("View Expenses")
)
Section (header: Text("Results"))
HStack
Text("Total Expenses")
Spacer()
Text("\(totalExpenses, specifier: "%.2f")")
.navigationTitle("My Expense Tracker")
.font(.subheadline)
.onAppear
totalExpenses = myTracker.getTotalExpenses()
struct ContentView: View
@ObservedObject var myExpenses:Expenses
@Binding var totalExpenses: Double
@State var selectedExpenseItem:Expense? = nil
var body: some View
NavigationView
Form
List
ForEach(myExpenses.items) item in
HStack
Text("\(item.name)")
Spacer()
Button(action:
self.selectedExpenseItem = item
)
Text("View")
.deleteDisabled(item.isDeletable)
.onDelete(perform: removeItem)
HStack
Text("Total Expenses:")
Spacer()
Text("\(myExpenses.totalExpenses(), specifier: "%.2f")")
.navigationTitle("Expenses")
.toolbar
Button
let newExpense = Expense(name: "Enter name", type: "Expense item", cost: 10.00, isDeletable: false)
self.myExpenses.items.append(newExpense)
self.totalExpenses = myExpenses.totalExpenses()
label:
Image(systemName: "plus")
.fullScreenCover(item: $selectedExpenseItem) myItem in
ItemDetailView(item: myItem, myExpenses: myExpenses, totalExpenses: $totalExpenses)
func removeItem(at offsets: IndexSet)
self.myExpenses.items.remove(atOffsets: offsets)
self.totalExpenses = myExpenses.totalExpenses()
【讨论】:
【参考方案2】:如果您需要在此处嵌套可观察对象,这是我能找到的最佳方法。
class ChildModel: ObservableObject
@Published
var count = 0
class ParentModel: ObservableObject
@Published
private var childWillChange: Void = ()
private(set) var child = ChildModel()
init()
child.objectWillChange.assign(to: &$childWillChange)
您无需订阅子对象的 objectWillChange 发布者并触发父对象的发布者,而是将值分配给已发布的属性,父对象的 objectWillChange 会自动触发。
【讨论】:
【参考方案3】:请参阅以下帖子以获取解决方案:[arthurhammer.de/2020/03/combine-optional-flatmap][1]。这是在与 $ 出版商联合解决问题。
假设class Foto
有一个注解结构和注解发布者,它们发布一个注解结构。在 Foto.sample(orientation: .Portrait) 中,注释结构通过注释发布者异步“加载”。普通的香草组合......但要将其放入 View 和 ViewModel,请使用:
class DataController: ObservableObject
@Published var foto: Foto
@Published var annotation: LCPointAnnotation
@Published var annotationFromFoto: LCPointAnnotation
private var cancellables: Set<AnyCancellable> = []
init()
self.foto = Foto.sample(orientation: .Portrait)
self.annotation = LCPointAnnotation()
self.annotationFromFoto = LCPointAnnotation()
self.foto.annotationPublisher
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.assign(to: \.annotation, on: self)
.store(in: &cancellables)
$foto
.flatMap $0.$annotation
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.assign(to: \.annotationFromFoto, on: self)
.store(in: &cancellables)
注:[1]:https://arthurhammer.de/2020/03/combine-optional-flatmap/
注意 flatMap 上面的 $annotation,它是一个发布者!
public class Foto: ObservableObject, FotoProperties, FotoPublishers
/// use class not struct to update asnyc properties!
/// Source image data
@Published public var data: Data
@Published public var annotation = LCPointAnnotation.defaultAnnotation
......
public init(data: Data)
guard let _ = UIImage(data: data),
let _ = CIImage(data: data) else
fatalError("Foto - init(data) - invalid Data to generate CIImage or UIImage")
self.data = data
self.annotationPublisher
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.sink resultAnnotation in
self.annotation = resultAnnotation
print("Foto - init annotation = \(self.annotation)")
.store(in: &cancellables)
【讨论】:
【参考方案4】:Sorin Lica 的方案可以解决这个问题,但是这会在处理复杂视图时导致代码异味。
似乎更好的建议是仔细查看您的观点,并对其进行修改以形成更多、更有针对性的观点。构造您的视图,以便每个视图显示一个对象结构的单个级别,将视图与符合ObservableObject
的类匹配。在上述情况下,您可以创建一个视图来显示 Submodel
(或什至几个视图),以显示您想要显示的属性。将属性元素传递给该视图,让它为您跟踪发布者链。
struct SubView: View
@ObservableObject var submodel: Submodel
var body: some View
Text("Count: \(submodel.count)")
.onTapGesture
self.submodel.count += 1
struct ContentView: View
@EnvironmentObject var appModel: AppModel
var body: some View
SubView(submodel: appModel.submodel)
这种模式意味着制作更多、更小和集中的视图,并让 SwiftUI 内部的引擎进行相关跟踪。这样您就不必处理簿记了,您的视图也可能会变得相当简单。
您可以在此帖子中查看更多详细信息:https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
【讨论】:
这个页面的答案是黄金。谢谢你。它不仅解释了问题,而且比整个传递 objectWillChange 上游地狱更优雅,如前所述,这将导致许多不必要的 UI 更新。 rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui 这可能是“SwiftUI 方式”。【参考方案5】:@Published
不是为引用类型设计的,因此将它添加到AppModel
属性是一个编程错误,即使编译器或运行时没有抱怨。直观的是添加@ObservedObject
,如下所示,但遗憾的是,这无声无息:
class AppModel: ObservableObject
@ObservedObject var submodel: SubModel = SubModel()
我不确定是否允许嵌套 ObservableObjects
是 SwiftUI 有意为之,还是将来要填补的空白。按照其他答案中的建议连接父对象和子对象非常混乱且难以维护。 SwiftUI 的想法似乎是将视图拆分为较小的视图并将子对象传递给子视图:
struct ContentView: View
@EnvironmentObject var appModel: AppModel
var body: some View
SubView(model: appModel.submodel)
struct SubView: View
@ObservedObject var model: SubModel
var body: some View
Text("Count: \(model.count)")
.onTapGesture
model.count += 1
class SubModel: ObservableObject
@Published var count = 0
class AppModel: ObservableObject
var submodel: SubModel = SubModel()
子模型突变在传递到子视图时实际上会传播!
但是,没有什么可以阻止另一个开发人员从父视图调用 appModel.submodel.count
,这很烦人,没有编译器警告,甚至没有一些 Swift 方法来强制不这样做。
来源:https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
【讨论】:
这个页面的答案是黄金。谢谢你。它不仅解释了问题,而且比整个传递 objectWillChange 上游地狱更优雅,如前所述,这将导致许多不必要的 UI 更新。 rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui【参考方案6】:我是这样做的:
import Combine
extension ObservableObject
func propagateWeakly<InputObservableObject>(
to inputObservableObject: InputObservableObject
) -> AnyCancellable where
InputObservableObject: ObservableObject,
InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
objectWillChange.propagateWeakly(to: inputObservableObject)
extension Publisher where Failure == Never
public func propagateWeakly<InputObservableObject>(
to inputObservableObject: InputObservableObject
) -> AnyCancellable where
InputObservableObject: ObservableObject,
InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
sink [weak inputObservableObject] _ in
inputObservableObject?.objectWillChange.send()
所以在调用端:
class TrackViewModel
private let playbackViewModel: PlaybackViewModel
private var propagation: Any?
init(playbackViewModel: PlaybackViewModel)
self.playbackViewModel = playbackViewModel
propagation = playbackViewModel.propagateWeakly(to: self)
...
Here's a gist.
【讨论】:
【参考方案7】:嵌套的ObservableObject
模型还不能工作。
但是,您可以通过手动订阅每个模型来使其工作。 The answer gave a simple example of this.
我想补充一点,您可以通过扩展使这个手动过程更加精简和可读:
class Submodel: ObservableObject
@Published var count = 0
class AppModel: ObservableObject
@Published var submodel = Submodel()
@Published var submodel2 = Submodel2() // the code for this is not defined and is for example only
private var cancellables: Set<AnyCancellable> = []
init()
// subscribe to changes in `Submodel`
submodel
.subscribe(self)
.store(in: &cancellables)
// you can also subscribe to other models easily (this solution scales well):
submodel2
.subscribe(self)
.store(in: &cancellables)
这是扩展名:
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher
func subscribe<T: ObservableObject>(
_ observableObject: T
) -> AnyCancellable where T.ObjectWillChangePublisher == ObservableObjectPublisher
return objectWillChange
// Publishing changes from background threads is not allowed.
.receive(on: DispatchQueue.main)
.sink [weak observableObject] (_) in
observableObject?.objectWillChange.send()
【讨论】:
【参考方案8】:我最近在我的博客上写了这篇文章:Nested Observable Objects。如果您真的想要 ObservableObjects 的层次结构,该解决方案的要点是创建您自己的*** Combine Subject 以符合 ObservableObject protocol,然后将您想要触发更新的任何逻辑封装到命令式代码中更新该主题。
例如,如果您有两个“嵌套”类,例如
class MainThing : ObservableObject
@Published var element : SomeElement
init(element : SomeElement)
self.element = element
class SomeElement : ObservableObject
@Published var value : String
init(value : String)
self.value = value
然后您可以将***类(在本例中为MainThing
)扩展为:
class MainThing : ObservableObject
@Published var element : SomeElement
var cancellable : AnyCancellable?
init(element : SomeElement)
self.element = element
self.cancellable = self.element.$value.sink(
receiveValue: [weak self] _ in
self?.objectWillChange.send()
)
它从嵌入的ObservableObject
中获取发布者,并在修改SomeElement
类上的属性value
时将更新发送到本地发布者。您可以扩展它以使用 CombineLatest 从多个属性或主题的任意数量的变体发布流。
但这不是一个“随便做”的解决方案,因为这种模式的逻辑结论是在您扩展视图层次结构之后,您最终会得到订阅的视图的大量样本那个将失效和重绘的发布者,可能会导致过度、全面的重绘和相对较差的更新性能。我建议你看看你是否可以将你的视图重构为特定于一个类,并将它与那个类匹配,以保持 SwiftUI 视图失效的“爆炸半径”最小化。
【讨论】:
最后(以及博文中)的建议绝对是金句。我正在陷入链接objectWillChange
调用的兔子洞,但我只需要重构一个视图即可获取@ObservedObject
...谢谢@heckj :)【参考方案9】:
AppModel 中的 var 子模型不需要属性包装器 @Published。 @Published 的目的是发出新值和 objectWillChange。 但是变量永远不会改变,只会启动一次。
订阅者 anyCancellable 和 ObservableObject-protocol 通过 sink-objectWillChange 构造将子模型中的更改传播到视图,并导致视图重绘。
class SubModel: ObservableObject
@Published var count = 0
class AppModel: ObservableObject
let submodel = SubModel()
var anyCancellable: AnyCancellable? = nil
init()
anyCancellable = submodel.objectWillChange.sink [weak self] (_) in
self?.objectWillChange.send()
【讨论】:
【参考方案10】:嵌套模型在 SwiftUI 中还不能工作,但你可以这样做
class SubModel: ObservableObject
@Published var count = 0
class AppModel: ObservableObject
@Published var submodel: SubModel = SubModel()
var anyCancellable: AnyCancellable? = nil
init()
anyCancellable = submodel.objectWillChange.sink [weak self] (_) in
self?.objectWillChange.send()
基本上您的AppModel
会从SubModel
捕获事件并将其进一步发送到View
。
编辑:
如果您不需要 SubModel
上课,那么您也可以尝试这样的事情:
struct SubModel
var count = 0
class AppModel: ObservableObject
@Published var submodel: SubModel = SubModel()
【讨论】:
谢谢,这很有帮助!当你说“嵌套模型在 SwiftUI 中还不能工作”时,你确定它们是计划好的吗? 我不确定,但我认为它应该可以工作,我也在我的项目中使用了类似的东西,所以如果我能找到更好的方法,我会进行编辑 @SorinLicaSubmodel
应该是 ObservableObject
类型吗?
我想补充一点,AnyCancellable 类型是在组合框架中定义的。我猜 99% 的人都知道这一点,我不得不用谷歌搜索...
在我的情况下,我有一个带有活动更改的 ObservableObject 列表,如果我会在嵌套对象的更改上下沉,当我只需要刷新一行时,这将触发重新加载整个列表。所以我会冻结【参考方案11】:
我有一个我认为比订阅子(视图)模型更优雅的解决方案。这很奇怪,我没有解释它为什么起作用。
解决方案
定义一个继承自ObservableObject
的基类,并定义一个简单调用objectWillChange.send()
的方法notifyWillChange()
。然后任何派生类将覆盖notifyWillChange()
并调用父类的notifyWillChange()
方法。
需要在方法中包装objectWillChange.send()
,否则对@Published
属性的更改不会导致任何View
s 更新。它可能与如何检测@Published
更改有关。我相信 SwiftUI/Combine 在后台使用反射......
我对 OP 的代码做了一些细微的补充:
count
包含在一个方法调用中,该方法调用在计数器递增之前调用notifyWillChange()
。这是传播更改所必需的。
AppModel
包含另外一个@Published
属性title
,用于导航栏的标题。这表明@Published
同时适用于父对象和子对象(在下面的示例中,模型初始化后 2 秒更新)。
代码
基础模型
class BaseViewModel: ObservableObject
func notifyWillUpdate()
objectWillChange.send()
型号
class Submodel: BaseViewModel
@Published var count = 0
class AppModel: BaseViewModel
@Published var title: String = "Hello"
@Published var submodel: Submodel = Submodel()
override init()
super.init()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) [weak self] in
guard let self = self else return
self.notifyWillChange() // XXX: objectWillChange.send() doesn't work!
self.title = "Hello, World"
func increment()
notifyWillChange() // XXX: objectWillChange.send() doesn't work!
submodel.count += 1
override func notifyWillChange()
super.notifyWillChange()
objectWillChange.send()
视图
struct ContentView: View
@EnvironmentObject var appModel: AppModel
var body: some View
NavigationView
Text("Count: \(appModel.submodel.count)")
.onTapGesture
self.appModel.increment()
.navigationBarTitle(appModel.title)
【讨论】:
【参考方案12】:所有三个 ViewModel 都可以通信和更新
// First ViewModel
class FirstViewModel: ObservableObject
var facadeViewModel: FacadeViewModels
facadeViewModel.firstViewModelUpdateSecondViewModel()
// Second ViewModel
class SecondViewModel: ObservableObject
// FacadeViewModels Combine Both
import Combine // so you can update thru nested Observable Objects
class FacadeViewModels: ObservableObject
lazy var firstViewModel: FirstViewModel = FirstViewModel(facadeViewModel: self)
@Published var secondViewModel = secondViewModel()
var anyCancellable = Set<AnyCancellable>()
init()
firstViewModel.objectWillChange.sink
self.objectWillChange.send()
.store(in: &anyCancellable)
secondViewModel.objectWillChange.sink
self.objectWillChange.send()
.store(in: &anyCancellable)
func firstViewModelUpdateSecondViewModel()
//Change something on secondViewModel
secondViewModel
感谢 Sorin 提供的组合解决方案。
【讨论】:
【参考方案13】:它看起来像错误。当我将 xcode 更新到最新版本时,它在绑定到嵌套的 ObservableObjects 时可以正常工作
【讨论】:
你能澄清一下你目前使用的 xcode 版本吗?我目前有 Xcode 11.0 并遇到此问题。我在升级到 11.1 时遇到了麻烦,它不会像 80% 那样完成。以上是关于如何告诉 SwiftUI 视图绑定到嵌套的 ObservableObjects的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI如何让绑定到同一个状态的多个TextField呈现出不同输入行为