Callback from actor - proper usage of Swift concurrency feature

I'm trying to get familiar with concurrency feature migrating my code to new paradigm.
Sometime ago I wrote simple propertyWrapper to simplify callback syntax from e.g. model class to "background" classes. Example below:

class Model: ObservableObject {
    @Published var viewText = "text"
    
    var background = Background()
    init() {
        background.$published { value in
            self.viewText = value
        }
        detach { self.background.back() }   //simulate callback from background (e.g. system)
    }
}

class Background {
    @MyPublisher private(set) var published: String = "text"
    
    func back() {
        published = "back"
    }
}

ProperyWrapper (simplified version - full version accepts execute dispatchers and optionals) below:

@propertyWrapper
class MyPublisher<Value> {
    private var clousure: ((Value) -> Void)?
    
    lazy private(set) var projectedValue = { [unowned self] (clousure: @escaping (Value) -> Void) in
        self.clousure = clousure
    }

    var wrappedValue: Value {
        didSet {
            clousure?(wrappedValue)
        }
    }
    
    init (wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }
}

Modification of propertyWrapper fo actor usage seemed simple at first (making projectedValue nonisolated to allow access from Model class). Modified code below:

@propertyWrapper
class MyPublisherActorNOK<Value> {
    private var clousure: ((Value) -> Void)?
    
    lazy private(set) nonisolated var projectedValue = { [unowned self] (clousure: @escaping (Value) -> Void) in
        self.clousure = clousure
    }

    var wrappedValue: Value {
        didSet {
            clousure?(wrappedValue)
        }
    }
    
    init (wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }
}

@MainActor
class ModelWithActorNOK: ObservableObject {
    @Published var viewText = "text"
    
    var background = BackgroundActorNOK()
    init() {
        background.$published { value in
            self.viewText = value  // !!! Publishing changes from background threads is not allowed...
        }
        detach { await self.background.back() }   //simulate callback from background (e.g. system)
    }
}

actor BackgroundActorNOK {
    @MyPublisherActorNOK private(set) var published: String = "text"
    
    func back() {
        published = "back"
    }
}

The problem is that callback is executed in actor, background executor thread, not in main thread. So first question: Why Swift accepts such code at all without any warning? It seems like my code breaks concurrency rules?

I changed propertyWrapper a bit more to remedy the problem (adding @MainActor attribute to closure and calling it from new async task):

@propertyWrapper
class MyPublisherActor<Value> {
    private var clousure: (@MainActor (Value) -> Void)?
    
    lazy private(set) nonisolated var projectedValue = { [unowned self] (clousure: @MainActor @escaping (Value) -> Void) in
        self.clousure = { _ in clousure(wrappedValue) }
    }

    var wrappedValue: Value {
        didSet {
            async { await clousure?(wrappedValue) }
        }
    }
    
    init (wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }
}

Final code looks for me like sort of workaround (e.g. it won't work for callback from actor to non-main actor). So next question is : What would be proper way of adopting this propertyWrapper for concurrency feature - in line with Swift's concurrency paradigm and syntax?

Terms of Service

Privacy Policy

Cookie Policy