Swift Combine-如何订阅嵌套的可观察对象

时间:2020-11-04 17:57:59

标签: swift observable combine

这是this question的抽象版本。在应用程序中,这些嵌套的Observable Objects实际上是在视图中使用的(因此,我宁愿使用Observable Objects而不是直接的Publishers)。但是,我希望能够简单地订阅视图模型以对其进行测试。该协议在那里,所以我可以在测试中模拟嵌套。

这是基本设置:

protocol NestedProtocol: AnyObject {
  var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
  @Published var string = ""
}
class Parent: ObservableObject {
  @Published var nested: NestedProtocol
  init(nested: NestedProtocol) {
    self.nested = nested
  }
}

var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)

parent.$nested.sink { newValue in
  print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)

然后此命令 nested.string = "foo1"输出“ NEW VALUE”,它应作为nested的初始值。我希望它输出“ NEW VALUE foo1”。 (TIL发布的变量似乎是当前值的发布者。)

  1. 我当然可以做
nested.string = "foo1"
parent.nested = nested

我会得到“ NEW VALUE foo1”,但这太臭了。

  1. 我尝试过
protocol NestedProtocol: ObservableObject {
  var string: String { get set }
}
class Nested<T>: ObservableObject where T: NestedProtocol {
...

但是在现实生活中,我想嵌套声明一些静态常量,这在泛型类型中是不允许的。所以那行不通。

  1. 从引用的问题/答案中,我还尝试了以下组合:
Parent
init() {
  nested.objectWillChange.sink { [weak self] (_) in
    self?.objectWillChange.send()
  }.store(in: sinkHole)
}

Nested
init() {
  string.sink { [weak self] (_) in
    self?.objectWillChange.send()
  }.store(in: sinkHole)
}

没有骰子。这些方法被调用了,但外层接收器仍只是返回“ NEW VALUE”

  1. 我也尝试致电
parent.nested.string = "foo1"

所以现在我要修改父级,这应该可以,对吗?不对。

2 个答案:

答案 0 :(得分:0)

这花了我几个小时,在尝试以各种方式修改协议时偶然发现了它。

第1课:

protocol NestedProtocol: AnyObject {
  var string: String { get set }
}

应该是

protocol NestedProtocol {
  var string: String { get set }
}

为什么?我不确定。显然,如果父级不能假定已发布的对象是一个类,那么它会更密切地监视对其的修改?我的直觉告诉了我完全相反的一面,但是它表明了我有多信任我的直觉。

第2课: 确实,我的第四个想法是正确的,您需要在嵌套对象修改中命名父对象:

nested.string = "foo1"

应该是

parent.nested.string = "foo1"

同样,它们都是类,所以这与我的理解有些冲突,但是我不知道@Published下发生的所有魔术。

最终的完整版本如下:

protocol NestedProtocol {
  var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
  @Published var string = ""
}
class Parent: ObservableObject {
  @Published var nested: NestedProtocol
  init(nested: NestedProtocol) {
    self.nested = nested
  }
}

var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)

parent.$nested.sink { newValue in
  print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)

nested.string = "foo1"
parent.nested.string = "foo2"

返回 “新值” “新值foo2”

答案 1 :(得分:0)

这里有很多东西可以打开。

首先,您可能会知道,如果属性是值类型,例如structString,然后将其标记为@Published即可:

class Outer {
   @Published var str: String = "default"
}

let outer = Outer()
outer.$str.sink { print($0) }
outer.str = "changed"

将输出:

default
changed

但是,您的问题是关于嵌套的可观察对象(它是引用类型)。因此,以上内容不适用于引用类型。

但是在您的示例中,您使用的是协议作为存在的协议(即代替最终实例),并且正如您指出的那样,它没有从AnyObject继承而来,实际上它的行为就像一个值类型:

protocol InnerProtocol {
   var str: String { get set }
}
class Inner: InnerProtocol {
   @Published var str: String = "default"
}
class Outer {
   @Published var inner: InnerProtocol
   init(_ inner: InnerProtocol) { self.inner = inner }
}

let inner = Inner()
let outer = Outer(inner)
outer.$inner.sink { print($0.str) }
outer.inner.str = "changed"

这还将输出:

default
changed

看起来像您想要的,但是实际上它并没有真正“观察”嵌套对象中的任何更改。当您执行outer.inner.str时,它具有值类型的语义,因此就像您重新分配了.inner属性一样。但是,如果您真的有兴趣观察对象本身的变化,那么这种方法将根本行不通。例如:

nested.str = "inner changed"

不会导致输出。如果inner对象更改了自己的属性,例如:

,也不会有输出
init() {
   DisplatchQueue.main.asyncAfter(.now() + 1) {
      self.str = "async changed"
   }
}

因此,目前尚不清楚您到底要达到什么目标。如果要观察引用类型属性,则需要直接观察它。

class Inner: ObservableObject {
   @Published var str: String
   //...
}
class Outer: ObservableObject {
   var inner: Inner
   //...
}
//...
outer.inner.$str.sink { ... }
// or
outer.inner.objectWillChange.sink { ... }

如果您坚持的话,也可以使用协议来实现这一点:

protocol InnerProtocol: ObservableObject {
   var str: String { get set }
}
class Inner: InnerProtocol {
   @Published var str: String = "default"
}
class Outer<T: InnerProtocol>: ObservableObject {
   var inner: T
   init(_ inner: T) { self.inner = inner }
}

let inner = Inner()
let outer = Outer(inner)
outer.inner.$str.sink { ... }
inner.str = "changed"