Reputation: 8826
class ObservableFormViewModel: ObservableObject {
@Published var isSubmitAllowed: Bool = true
@Published var username: String = ""
@Published var password: String = ""
var somethingElse: Int = 10
}
var form = ObservableFormViewModel()
let formSubscription = form.$isSubmitAllowed.sink { _ in
print("Form changed: \(form.isSubmitAllowed) \"\(form.username)\" \"\(form.password)\"")
}
form.isSubmitAllowed = false
form.isSubmitAllowed = false
form.isSubmitAllowed = false
The output is:
Form changed: true "" ""
Form changed: true "" ""
Form changed: false "" ""
Form changed: false "" ""
My question is:
true
output comes 2 while false
only 2?Upvotes: 4
Views: 2280
Reputation: 4248
In many cases, a published variable is only set internally in the class, i.e.:
@Published private(set) var publishedVar = false
In these cases, it is only a concern of the containing class that the same value is set repeatedly, not of any observers. Observers only want to know when the value changes, and don't want to be notified if the value has not changed, without needing to use removeDuplicates()
everywhere the property is observed.
For properties like this, we can recreate @Published to remove duplicates for us:
import Combine
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct PublishedWithoutDuplicates<Value> where Value: Equatable {
public var wrappedValue: Value {
willSet {
if newValue != wrappedValue {
subject.send(wrappedValue)
}
}
}
private let subject: CurrentValueSubject<Value, Never>
public let projectedValue: AnyPublisher<Value, Never>
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
self.subject = CurrentValueSubject(wrappedValue)
self.projectedValue = self.subject.eraseToAnyPublisher()
}
}
This can be used in the same way as @Published
:
@PublishedWithoutDuplicates private(set) var publishedVar = false
For example, the following code:
class Test {
@PublishedWithoutDuplicates var test = false {
didSet {
print("didSet \(test)")
}
}
}
let t = Test()
let d = t.$test.sink(receiveValue: { print ("Observed \($0)") })
t.test = false
t.test = true
t.test = true
t.test = false
t.test = false
gives the output:
Observed false
didSet false
Observed true
didSet true
didSet true
Observed false
didSet false
didSet false
However, this property wrapper won't work as expected if Test
conforms to ObserableObject
, because it doesn't have a reference to the parent object to call objectWillChange.send()
. To do that, we need to use a private method to get the parent object. Use at your own risk:
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct PublishedWithoutDuplicates<Value> where Value: Equatable {
public static subscript<T: Any>(
_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
) -> Value {
get {
instance[keyPath: storageKeyPath].wrappedValue
}
set {
guard newValue != instance[keyPath: storageKeyPath].wrappedValue else { return }
if let observable = instance as? any ObservableObject,
let publisher = observable.objectWillChange as any Publisher as? ObservableObjectPublisher {
publisher.send()
}
instance[keyPath: storageKeyPath].wrappedValue = newValue
}
}
public var wrappedValue: Value {
willSet {
if newValue != wrappedValue {
subject.send(newValue)
}
}
}
private let subject: CurrentValueSubject<Value, Never>
public let projectedValue: AnyPublisher<Value, Never>
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
self.subject = CurrentValueSubject(wrappedValue)
self.projectedValue = self.subject.eraseToAnyPublisher()
}
}
If Test
is a class, this property wrapper will be updated with the subscript
static method, otherwise wrappedValue
will be set directly. We handle both cases here, so the property wrapper will work as expected in structs, classes, and in classes conforming to ObservableObject
it will also call objectWillChange.send()
.
class Test: ObservableObject {
@PublishedWithoutDuplicates var test = false {
didSet {
print("didSet \(test)")
}
}
}
var t = Test()
let d = t.$test.sink(receiveValue: { print ("Observed \($0)") })
let d2 = t.objectWillChange.sink(receiveValue: { print ("ObjectWillChange") })
t.test = false
t.test = true
t.test = true
t.test = false
t.test = false
Observed false
didSet false
ObjectWillChange
Observed true
didSet true
didSet true
ObjectWillChange
Observed false
didSet false
didSet false
Modified from Swift by Sundell
Upvotes: 1
Reputation: 54426
why true output comes 2 while false only 2?
The first output is run when you create formSubscription
. The next three are triggered by your consecutive form.isSubmitAllowed = false
statements.
Note that you change form.isSubmitAllowed
three times to false
and in output it occurs only two times:
form.isSubmitAllowed = false
form.isSubmitAllowed = false
form.isSubmitAllowed = false
// Form changed: true "" ""
Form changed: true "" ""
Form changed: false "" ""
Form changed: false "" ""
This is because you're not printing the changed value but the old one.
Try this instead:
let formSubscription = form.$isSubmitAllowed.sink { isSubmitAllowed in
print("Form changed: \(isSubmitAllowed) \"\(form.username)\" \"\(form.password)\"")
}
This prints:
// Form changed: true "" ""
Form changed: false "" ""
Form changed: false "" ""
Form changed: false "" ""
If you want to remove duplicates just use removeDuplicates
:
let formSubscription = form.$isSubmitAllowed.removeDuplicates().sink { value in
print("Form changed: \(value) \"\(form.username)\" \"\(form.password)\"")
}
form.isSubmitAllowed = false
form.isSubmitAllowed = false
form.isSubmitAllowed = false
This prints:
Form changed: true "" ""
Form changed: false "" ""
Upvotes: 3