如何告诉SwiftUI视图绑定到嵌套的ObservableObjects

时间:2019-10-16 05:28:47

标签: ios swift swiftui combine

我有一个SwiftUI视图,其中包含一个名为appModel的EnvironmentObject。然后,它在其appModel.submodel.count方法中读取值body。我希望这会将我的视图绑定到count上的属性submodel,以便在属性更新时重新呈现,但这似乎不会发生。

这是一个错误吗?如果没有,那么将视图绑定到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来解决此问题,但我想避免这样做。

11 个答案:

答案 0 :(得分:6)

嵌套模型在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 { (_) in
            self.objectWillChange.send()
        }
    } 
}

基本上,您的AppModelSubmodel捕获了事件并将其发送到视图

编辑:

如果您不需要SubModel上课,则可以尝试以下类似方法:

struct Submodel{
    var count = 0
}

class AppModel: ObservableObject {
    @Published var submodel: Submodel = Submodel()
}

答案 1 :(得分:1)

所有三个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提供合并解决方案。

答案 2 :(得分:1)

我最近在我的博客上写了这个: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 中获取发布者,并在修改 value 类上的属性 SomeElement 时将更新发送到本地发布者。您可以将其扩展为使用 CombineLatest 从多个属性或主题的任意数量的变体发布流。

不过,这不是一个“就做”的解决方案,因为这种模式的逻辑结论是在您增加视图层次结构之后,您最终将获得订阅的视图的潜在大量样本将失效和重绘的发布者,可能会导致过度、全面的重绘和相对较差的更新性能。我建议您查看是否可以将视图重构为特定于某个类,并将其与该类匹配,以最大限度地减少 SwiftUI 视图失效的“爆炸半径”。

答案 3 :(得分:0)

我有一个解决方案,我认为它比订阅儿童(视图)模型更为优雅。很奇怪,我没有解释它为什么起作用的原因。

解决方案

定义一个继承自ObservableObject的基类,并定义一个简单调用notifyWillChange()的方法objectWillChange.send()。然后,任何派生类都将覆盖notifyWillChange()并调用父级的notifyWillChange()方法。 必须在方法中包装objectWillChange.send(),否则对@Published属性的更改不会导致任何View的更新。它可能与检测@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)
        }
    }
}

答案 4 :(得分:0)

AppModel中的var子模型不需要属性包装器@Published。 @Published的目的是发出新值和objectWillChange。 但是变量永远不会改变,只会被初始化一次。

订阅者通过接收器objectWillChange结构将子模型中的更改传播到视图anyCancellable和ObservableObject-protocol并导致视图重绘。

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

答案 5 :(得分:0)

我是这样做的:

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

答案 6 :(得分:0)

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/

答案 7 :(得分:0)

有关解决方案,请参阅以下帖子:[arthurhammer.de/2020/03/combine-optional-flatmap][1]。这是与 $ 发布者以结合方式解决问题。

假设 class Foto 有一个注解结构体和注解发布者,它们发布一个注解结构体。在 Foto.sample(orientation: .Portrait) 中,注释结构通过注释发布者异步“加载”。普通的香草组合......但要将其放入视图和视图模型中,请使用:

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

答案 8 :(得分:-1)

它看起来像错误。当我将xcode更新到最新版本时,当绑定到嵌套的ObservableObjects时它可以正常工作

答案 9 :(得分:-1)

嵌套的 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()
      }
  }
}

答案 10 :(得分:-2)

@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/