Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combining custom property wrapper with @Published

I wish to apply a custom property wrapper to a variable already wrapped in @Published, nesting them like
(A) @Custom @Published var myVar or
(B) @Published @Custom var myVar
(notice the application order of the wrappers).

In the case of (A) I get the error

'wrappedValue' is unavailable: @Published is only available on properties of classes

and for (B)

error: key path value type 'Int' cannot be converted to contextual type 'Updating<Int>'

neither of which are particularly helpful. Any ideas how to make it work?

Minimal code example

import Combine

class A {
    @Updating @Published var b: Int
    
    init(b: Int) {
        self.b = b
    }
}

@propertyWrapper struct Updating<T> {
    var wrappedValue: T {
        didSet {
            print("Update: \(wrappedValue)")
        }
    }
}

let a = A(b: 1)
let cancellable = a.$b.sink {
    print("Published: \($0)")
}
a.b = 2
// Expected output:
// ==> Published: 1
// ==> Published: 2
// ==> Update: 2
like image 323
Milo Wielondek Avatar asked Nov 26 '22 22:11

Milo Wielondek


1 Answers

The only solution I've found is a workaround: Make a custom @propertyWrapper that has a @Published property inside of it.

Example:

/// Workaround @Published not playing nice with other property wrappers. 
/// Use this to replace @Published to temporarily help debug a property being accessed off the main thread.

@propertyWrapper
public class MainThreadPublished<Value> {
    @Published
    private var value: Value
    
    
    public var projectedValue: Published<Value>.Publisher {
        get {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            return $value
        }
        @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
        set {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            $value = newValue
        }
    }
    
    public var wrappedValue: Value {
        get {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            return value
        }
        set {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            value = newValue
        }
    }
    
    public init(wrappedValue value: Value) {
        self.value = value
    }

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

Further reading:

  • Property Wrapper proposal: https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type
  • Not Apple's source for the @Published, but a useful example: https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombine/Published.swift

EDIT:

I also just found this article that may provide an alternate approach, but I don't have time to investigate:

  • Nesting Property Wrappers: https://noahgilmore.com/blog/nesting-property-wrappers/
like image 157
MechEthan Avatar answered Jan 29 '23 04:01

MechEthan