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?