Search code examples
swiftswiftuiproperty-wrapper

How to implement a custom property wrapper which would publish the changes for SwiftUI to re-render it's view


Trying to implement a custom property wrapper which would also publish its changes the same way @Publish does. E.g. allow my SwiftUI to receive changes on my property using my custom wrapper.

The working code I have:

import SwiftUI

@propertyWrapper
struct MyWrapper<Value> {
    var value: Value

    init(wrappedValue: Value) { value = wrappedValue }

    var wrappedValue: Value {
        get { value }
        set { value = newValue }
    }
}

class MySettings: ObservableObject {
    @MyWrapper
    public var interval: Double = 50 {
        willSet { objectWillChange.send() }
    }
}

struct MyView: View {
    @EnvironmentObject var settings: MySettings

    var body: some View {
        VStack() {
            Text("\(settings.interval, specifier: "%.0f")").font(.title)
            Slider(value: $settings.interval, in: 0...100, step: 10)
        }
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView().environmentObject(MySettings())
    }
}

However, I do not like the need to call objectWillChange.send() for every property in MySettings class.

The @Published wrapper works well, so I tried to implement it as part of @MyWrapper, but I was not successful.

A nice inspiration I found was https://github.com/broadwaylamb/OpenCombine, but I failed even when trying to use the code from there.

When struggling with the implementation, I realised that in order to get @MyWrapper working I need to precisely understand how @EnvironmentObject and @ObservedObject subscribe to changes of @Published.

Any help would be appreciated.


Solution

  • Until the https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type gets implemented, I came up with the solution below.

    Generally, I pass the objectWillChange reference of the MySettings to all properties annotated with @MyWrapper using reflection.

    import Cocoa
    import Combine
    import SwiftUI
    
    protocol PublishedWrapper: class {
        var objectWillChange: ObservableObjectPublisher? { get set }
    }
    
    @propertyWrapper
    class MyWrapper<Value>: PublishedWrapper {
        var value: Value
        weak var objectWillChange: ObservableObjectPublisher?
    
        init(wrappedValue: Value) { value = wrappedValue }
    
        var wrappedValue: Value {
            get { value }
            set {
                value = newValue
                objectWillChange?.send()
            }
        }
    }
    
    class MySettings: ObservableObject {
        @MyWrapper
        public var interval1: Double = 10
    
        @MyWrapper
        public var interval2: Double = 20
    
        /// Pass our `ObservableObjectPublisher` to the property wrappers so that they can announce changes
        init() {
            let mirror = Mirror(reflecting: self)
            mirror.children.forEach { child in
                if let observedProperty = child.value as? PublishedWrapper {
                    observedProperty.objectWillChange = self.objectWillChange
                }
            }
        }
    }
    
    struct MyView: View {
        @EnvironmentObject
        private var settings: MySettings
    
        var body: some View {
            VStack() {
                Text("\(settings.interval1, specifier: "%.0f")").font(.title)
                Slider(value: $settings.interval1, in: 0...100, step: 10)
    
                Text("\(settings.interval2, specifier: "%.0f")").font(.title)
                Slider(value: $settings.interval2, in: 0...100, step: 10)
            }
        }
    }
    
    struct MyView_Previews: PreviewProvider {
        static var previews: some View {
            MyView().environmentObject(MySettings())
        }
    }