7

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
Milo Wielondek
  • 4,164
  • 3
  • 33
  • 45
  • I'm running into the same error message for scenario "B". Did you ever figure out the cause of this, or even a solution? – Rengers Mar 05 '21 at 20:24
  • How do plan to consume the `@Published` property? Asking because the minimal example doesn't justify the use of a `@Published` wrapper, which is mainly targeted at SwiftUI views. – Cristik Sep 16 '21 at 17:40

2 Answers2

2

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:


EDIT:

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

MechEthan
  • 5,703
  • 1
  • 35
  • 30
0

None of the options you provided can be applicated to make a custom property wrapper behave like it was marked with @Published (A and B)

The real question is, how do i observe property/status update changes?

  1. Using @Published wrapper, which handles status update automatically

  2. Tracking the status update manually, by implementing

    willSet {objectWillChange.send()}

Considering the option 1 cant work as you cant apply 2 property wrappers, you can go for manual status update tracking. In order to accomplish this, you will need to make your class conforming the ObservableObject protocol.

class A: ObservableObject {
    @Updating var b: Int{
       willSet {objectWillChange.send()}
    }
    
    init(b: Int) {
        self.b = b
    }
}

Your views will now be able to refresh whenever var b changes, having at the same time your @Updating wrapper fully working.

Simone
  • 91
  • 1
  • 8