Last active
May 13, 2021 12:55
-
-
Save Gernot/8aa4e201d5d39309113d686dee1b9f4e to your computer and use it in GitHub Desktop.
Assignable Extension on NSObject
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Combine | |
/** | |
I have two objects, Foo and Bar. Bar is a classic NSObject that hat a KVO observable Value thatchanges on an arbitrairy thread. | |
Foo is an ObservableObject with a published value that is derived from bar. That published value should change in the Main thread so SwiftUI does not complain. | |
However, if I do it with the Publisher/Combine solution only the initial value is nil, and the UI flickers. Instead I want to set the inital value in the init to the current value, and use the publisher only for new values and no longer for initial values. | |
Now, with the transformation/map function required in both the publisher and the setter, it gets complicated fast. So: Let's do a function on NSObject that encapulates all this! But, as usual, the swift compiler is in the way, complaining abut type issues. How do I build a function that does this in a generic way? | |
The following is obviously wrong, but as close as I could get. | |
*/ | |
class Bar: NSObject { | |
var value: Int? | |
} | |
class Foo: ObservableObject { | |
init(bar: Bar) { | |
self.bar = bar | |
//I want to replace this: | |
_value = Published(wrappedValue: bar.value.map { String($0) }) | |
bar.publisher(for: \.value, options: .new) | |
.map { $0.map { String($0) } } | |
.receive(on: DispatchQueue.main) | |
.assign(to: &$value) | |
//with this: | |
//bar.assign(\.value, to: &_value) { String($0) } | |
} | |
private let bar: Bar | |
@Published var value: String? | |
} | |
protocol Assignable where Self: NSObject { | |
func assign<SourceValue, TargetValue>(_ keyPath: KeyPath<Self, SourceValue>, to published: inout Published<TargetValue>, transform: @escaping(SourceValue) -> (TargetValue)) | |
} | |
extension Assignable { | |
func assign<SourceValue, TargetValue>(_ keyPath: KeyPath<Self, SourceValue>, to published: inout Published<TargetValue>, transform: @escaping(SourceValue) -> (TargetValue)) { | |
published = Published(wrappedValue: transform(self[keyPath: keyPath])) | |
self.publisher(for: keyPath, options: .new) | |
.map(transform) | |
.receive(on: DispatchQueue.main) | |
.assign(to: &published.projectedValue) | |
} | |
} | |
extension Bar: Assignable {} | |
At least this compiles:
extension Published {
init<Object: NSObject, SourceValue>(object: Object, keyPath: KeyPath<Object, SourceValue>, transform: @escaping (SourceValue) -> (Value)) {
var mutable = Self.init(wrappedValue: transform(object[keyPath: keyPath]))
object.publisher(for: keyPath)
.map(transform)
.receive(on: DispatchQueue.main)
.assign(to: &mutable.projectedValue)
self = mutable
}
}
Solved it! Here's the final extension. Thanks for listening to me talking to myself. (Swift does that to people.)
extension Published {
init<Object: NSObject>(observe keyPath: KeyPath<Object, Value>, in object: Object) {
self.init(observe: keyPath, in: object, transform: {$0})
}
init<ObservedObject: NSObject, ObservedValue>(observe keyPath: KeyPath<ObservedObject, ObservedValue>, in object: ObservedObject, transform: @escaping (ObservedValue) -> (Value)) {
var mutable = Self.init(wrappedValue: transform(object[keyPath: keyPath]))
object.publisher(for: keyPath)
.map(transform)
.receive(on: DispatchQueue.main)
.assign(to: &mutable.projectedValue)
self = mutable
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Alternative approach that doesn't work either: Initializing the
Published
with this: