在 didSet 之后,观察 Swift 中 @Published var 的变化吗?

Les*_*ary 7 ios swift combine

假设我们有以下用 Swift 编写的使用合并的代码:

import UIKit
import Combine

class Test {
  @Published var array: [Int] = [] {
    willSet {
      print("willSet \(newValue.count)")
    }
    didSet {
      print("didSet \(array.count)")
    }
  }
}

var test = Test()
var subscriber = test.$array.sink { values in
  print("arrayCount: \(test.array.count) valuesCount: \(values.count)")
}

print("1 arrayCount \(test.array.count)")
test.array = [1, 2, 3]
print("2 arrayCount \(test.array.count)")
test.array = [1]
print("3 arrayCount \(test.array.count)")
Run Code Online (Sandbox Code Playgroud)

此代码在控制台上打印以下结果(可以在 Playground 中快速测试):

arrayCount: 0 valuesCount: 0
1 arrayCount 0
willSet 3
arrayCount: 0 valuesCount: 3
didSet 3
2 arrayCount 3
willSet 1
arrayCount: 3 valuesCount: 1
didSet 1
3 arrayCount 1
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,sink 方法的代码在 willSet 之后和给定属性的 didSet 之前执行。现在我的问题是:有没有什么方法可以创建这个发布者或订阅它,以便给接收器的代码在 didSet 之后执行,而不是在它之前执行(这样,当执行从接收器打印时, arrayCount 和 valueCount 将是相同的)在上面的例子中)?

Dáv*_*tor 7

Published.Publisher用于willSet发出包装属性的值。不幸的是,您无法更改此行为,唯一的解决方案是实现您自己的属性包装器,didSet而不是使用willSet.

您还可以使用输入参数自定义是否希望在订阅发布projectValue者时接收当前值(匹配@Published行为)emitCurrentValue。如果设置为true,则当前wrappedValue订阅将发送给新订阅者。

/// A type that publishes changes about its `wrappedValue` property _after_ the property has changed (using `didSet` semantics).
/// Reimplementation of `Combine.Published`, which uses `willSet` semantics.
@available(iOS 13, *)
@propertyWrapper
public class PostPublished<Value> {
  /// A `Publisher` that emits the new value of `wrappedValue` _after it was_ mutated (using `didSet` semantics).
  public let projectedValue: AnyPublisher<Value, Never>
  /// A `Publisher` that fires whenever `wrappedValue` _was_ mutated. To access the new value of `wrappedValue`, access `wrappedValue` directly, this `Publisher` only signals a change, it doesn't contain the changed value.
  public let valueDidChange: AnyPublisher<Void, Never>
  private let didChangeSubject: any Subject<Value, Never>
  public var wrappedValue: Value {
    didSet {
      didChangeSubject.send(wrappedValue)
    }
  }

  /// - parameter emitCurrentValue: whether to emit the current wrapped value when subscribing to `projectValue`
  public init(wrappedValue: Value, emitCurrentValue: Bool = false) {
    self.wrappedValue = wrappedValue
    let didChangeSubject: any Subject<Value, Never>
    if emitCurrentValue {
      didChangeSubject = CurrentValueSubject(wrappedValue)
    } else {
      didChangeSubject = PassthroughSubject<Value, Never>()
    }
    self.didChangeSubject = didChangeSubject
    self.projectedValue = didChangeSubject.eraseToAnyPublisher()
    self.valueDidChange = didChangeSubject.voidPublisher()
  }
}

public extension Publisher {
  /// Maps the `Output` of its upstream to `Void` and type erases its upstream to `AnyPublisher`.
  func voidPublisher() -> AnyPublisher<Void, Failure> {
    map { _ in Void() }
      .eraseToAnyPublisher()
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以像观察 a@PostPublished一样观察 a @Published

class UsingPostPublished {
  @PostPublished var dontEmitInitial = 0
  @PostPublished(emitCurrentValue: true) var emitInitial = 0
}

private var cancellables = Set<AnyCancellable>()

let usingPostPublished = UsingPostPublished()
usingPostPublished.$dontEmitInitial.sink {
  print("dontEmitInitial did change to \($0)")
}.store(in: &cancellables)

usingPostPublished.$emitInitial.sink {
  print("emitInitial did change to \($0)")
}.store(in: &cancellables)

usingPostPublished.emitInitial = 1
usingPostPublished.dontEmitInitial = 1
Run Code Online (Sandbox Code Playgroud)


Ari*_*ner 6

首先,如果目的是让发布者在 didSet 上发出事件,那么您可以简单地使用CurrentValueSubject. 它的语法不像 那样漂亮@Published,但它确实有效:

var array = CurrentValueSubject<[Int], Never>([])
// ...
let second = array.value[1]
// ...
array.sink {
    print("array count: $0.count")
}
array.send([1, 2, 3])
Run Code Online (Sandbox Code Playgroud)

如果您已经有一个@Published希望保留的变量(例如,您的 SwiftUI 视图使用它),但您还希望发生变化后观察变化,那么您可以添加 aCurrentValueSubject并更新它didSet

class Test {
  @Published var array: [Int] = [] {
    didSet {
        arrayChangedEvent.send(array)
    }
  }
    var arrayChangedEvent = CurrentValueSubject<[Int], Never>([])
}
Run Code Online (Sandbox Code Playgroud)

然后以几乎相同的方式观察数组:arrayChangedEvent.sink { /* ... */ }


小智 6

好吧,我会说使用一个简单的技巧

var test = Test()
var subscriber = test.$array
.receive(on: DispatchQueue.main)
.sink { values in
print("arrayCount: \(test.array.count) valuesCount: \(values.count)")
}
Run Code Online (Sandbox Code Playgroud)