Home > Software design >  Observe change on a @Published var in Swift Combine after didSet?
Observe change on a @Published var in Swift Combine after didSet?

Time:11-17

Let's say that we have a following code written in Swift that uses Combine:

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

This code prints following result on the console (it can be quickly tested in 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

As we can see the code given to sink method is executed after willSet and before didSet of given property. Now my question is: is there any way to create this publisher or subscribe to it in such way that the code given to sink is executed after didSet and not before it (so that arrayCount and valuesCount would be the same when print from sink is executed in above example)?

CodePudding user response:

Published.Publisher uses willSet to emit values for the wrapped property. Unfortunately you cannot change this behaviour, the only solution is to implement your own property wrapper that uses didSet instead of willSet.

/// 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.
@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: PassthroughSubject<Value, Never>
    public var wrappedValue: Value { didSet { didChangeSubject.send(wrappedValue) } }

    public init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
        let 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()
    }
}

You can observe a @PostPublished the same way you do a @Published.

private class ModelWithPostPublished<Value> {
    @PostPublished var value: Value

    init(value: Value) {
        self.value = value
    }
}

ModelWithPostPublished(value: "original").$value.sink { print("value WAS set to \($0) }.store(in: &subscriptions)
  • Related