Search code examples
iosswiftcombineswift-concurrencyswift6

Swift 6 build errors when monitoring Published values of an Actor in a Task


The following code gives error when building under Swift 6 language version in Xcode. Is this an issue in architecture or has an easy fix? Type Bool is Sendable but it's publisher is not.

Non-sendable type 'Published<Bool>.Publisher' in implicitly asynchronous access to actor-isolated property '$isRecording' cannot cross actor boundary

import Foundation

final class Recorder {
    
    var writer = Writer()
    var isRecording = false
    
    func startRecording() {
        Task { [writer] in
            await writer.startRecording()
            print("started recording")
        }
    }
    
    func stopRecording() {
        Task { [writer] in
            await writer.stopRecording()
            print("stopped recording")
        }
    }
    
    func observeValues() {
        
        Task {
            for await value in await writer.$isRecording.values {
                isRecording = value
            }
        }
    }
}

actor Writer {
    @Published private(set) public var isRecording = false
    
    func startRecording() {
        isRecording = true
    }
    
    func stopRecording() {
        isRecording = false
    }
}

Please refer to this screenshot for actual errors in AVCam sample code I see (with the only modification of putting @preconcurrency in import AVFoundation).

Please refer to this screenshot for actual errors in AVCam sample code I see (without modifications)


Solution

  • Instead of @Published and then getting the values, I would directly use an AsyncStream to deliver the values. AsyncStream is sendable if the stream elements are sendable.

    @propertyWrapper
    struct Streamed<T: Sendable> {
        private var continuation: AsyncStream<T>.Continuation
        
        var projectedValue: AsyncStream<T>
        
        var wrappedValue: T {
            didSet {
                continuation.yield(wrappedValue)
            }
        }
        
        init(wrappedValue: T) {
            self.wrappedValue = wrappedValue
            (projectedValue, continuation) = AsyncStream.makeStream()
            continuation.yield(wrappedValue)
        }
    }
    
    actor Writer {
        @Streamed private(set) public var isRecording = false
        
        func startRecording() {
            isRecording = true
        }
        
        func stopRecording() {
            isRecording = false
        }
    }
    

    Then in observedValues,

    @MainActor
    func observeValues() {
        Task {
            for await value in await writer.$isRecording {
                isRecording = value
            }
        }
    }
    

    Note that for isRecording = value to be safe, either observeValues or the whole Recorder needs to be isolated to a global actor.