Skip to content

Instantly share code, notes, and snippets.

@Gernot
Last active May 13, 2021 12:55
Show Gist options
  • Save Gernot/8aa4e201d5d39309113d686dee1b9f4e to your computer and use it in GitHub Desktop.
Save Gernot/8aa4e201d5d39309113d686dee1b9f4e to your computer and use it in GitHub Desktop.
Assignable Extension on NSObject
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 {}
@Gernot
Copy link
Author

Gernot commented May 13, 2021

Alternative approach that doesn't work either: Initializing the Published with this:

extension Published {
    
    init<Object: NSObject, SourceValue>(object: Object, keyPath: KeyPath<Object, SourceValue>, transform: @escaping (SourceValue) -> (Value)) {
        self.init(wrappedValue: transform(object[keyPath: keyPath]))
        
        object.publisher(for: keyPath)
            .map(transform)
            .receive(on: DispatchQueue.main)
            .assign(to: &self.projectedValue)
    }
    
}

@Gernot
Copy link
Author

Gernot commented May 13, 2021

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
    }
    
}

@Gernot
Copy link
Author

Gernot commented May 13, 2021

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