How to solve this Swift structured concurrency problem?

185 Views Asked by At

I have a class written in Swift that looks like this:

protocol MessageSubscriber: ObservableObject, Subscriber where Input == Message, Failure == Never {
    func cancel()
}

class MySubscriberClass: MessageSubscriber {
    @Published var data: [MyDataResponse] = []
    
    private var subscription: Subscription?
    
    func cancel() {
        subscription?.cancel()
    }
    
    func receive(subscription: Subscription) {
        self.subscription = subscription
        self.subscription?.request(.unlimited)
    }
    
    func receive(_ input: Message) -> Subscribers.Demand {
        handleMessage(input)
        return .unlimited
    }
    
    func receive(completion: Subscribers.Completion<Never>) {}
    
    @MainActor func handleMessage(_ message: Message) {
        if case .foo(let bar) = message.msg {
            data.removeAll()
            for index in bar.foobar.indices {
                if !bar.foobar[index].isEmpty {
                    data.append(MyDataResponse(parameter: bar.foobar[index], index: index))
                }
            }
        }
    }
}

A bit of background: This class is used in a view as a @StateObject and it works fine (The @Published data is updated and shown how I want). However, I got some purple triangle warnings

Publishing changes from background threads is not allowed ...

so I added @MainActor to handleMessage(_:) function.

The problem begins after adding @MainActor to handleMessage(_:) function. Now receive(_ input: Message) function is giving an error:

Call to main actor-isolated instance method 'handleMessage' in a synchronous nonisolated context

Sure, let's wrap the handleMessage(_:) call with Task { @MainActor in handleMessage(_:) }. This produces a warning

Capture of 'self' with non-sendable type 'MySubscriberClass' in a '@Sendable' closure.

Note here that I can't mark receive(_ input: Message) with @MainActor because

Main actor-isolated instance method 'receive' cannot be used to satisfy nonisolated protocol requirement

I can try to go the other way and mark the whole class with @MainActor. I believe this is the recommended approach since we are updating the UI from this class. Now I have to mark cancel() receive(subscription: Subscription) receive(_ input: Message) and receive(completion: Subscribers.Completion<Never>) as nonisolated because

Main actor-isolated instance method cannot be used to satisfy nonisolated protocol requirement

Next errors spawn from all the functions I just marked as nonisolated. `

Main actor-isolated property 'subscription' can not be referenced from a non-isolated context

from inside cancel() for example and

Call to main actor-isolated instance method 'handleMessage' in a synchronous nonisolated context

Let's fix those by wrapping everything with Task { @MainActor in }

nonisolated func cancel() {
    Task { @MainActor in
        subscription?.cancel()
    }
}

nonisolated func receive(subscription: Subscription) {
    Task { @MainActor in
        self.subscription = subscription
        self.subscription?.request(.unlimited)
    }
}

nonisolated func receive(_ input: Message) -> Subscribers.Demand {
    Task { @MainActor in
        handleMessage(input)
    }
    return .unlimited
}

Yet again I have a warning, but this time it comes from the line self.subscription = subsription inside receive(subscription: Subsription)

Capture of 'subscription' with non-sendable type 'any Subscription' in a '@Sendable' closure

What is the correct approach to make this code free of warnings?

Subscriber protocol is part of the Combine framework. In the view this class is passed as a parameter to a PassthroughSubject<Message, Never>.subscribe(_:)

0

There are 0 best solutions below