CALayer issue with Swift 6 concurrency

I am having trouble fixing issue of Sendability of CALayer. Specifically, I have a SwiftUI view displaying camera frames using AVSampleBufferDisplayLayer or MTKView.

struct SampleBufferView: ViewRepresentable {
    
    typealias ViewType = DisplayLayerView
    
    let viewModel: CameraModel
    
    func makeView(context: Context) -> DisplayLayerView {
       let view = DisplayLayerView(frame: .zero)
        view.backgroundColor = UIColor.blue
        
        Task { @MainActor in
            // Notify viewModel about the layer
            viewModel.setDisplayLayer(view.layer)
        }
        return view
    }

And the viewModel forwards this layer to class CameraManager which runs on a global actor to protect it's mutable state.

 func setDisplayLayer(_ layer:CALayer) {
        Task { @CameraActor in
            await cameraManager.updateRotationCoordinator(displayLayer: layer)
        }
    }

I get warning that this isn't allowed in Swift 6 as CALayer is not sendable.

Here is how I use the layer in CameraManager.

 func updateRotationCoordinator(displayLayer: CALayer) {
        guard let device = sessionConfiguration.activeVideoInput?.device else { return }
        
        rotationCoordinator = nil
        cancellables.removeAll()
        
        rotationCoordinator = AVCaptureDevice.RotationCoordinator(device: device, previewLayer: displayLayer)
        
        guard let coordinator = rotationCoordinator else { return }
        
        coordinator.publisher(for: \.videoRotationAngleForHorizonLevelPreview)
            .receive(on: DispatchQueue.main)
            .sink { degrees in
                let radians = degrees * .pi / 180
                displayLayer.setAffineTransform(CGAffineTransform(rotationAngle: radians))
            }
            .store(in: &cancellables)
    }

Global-actor-isolated synchronous closure is Sendable and can capture non-sendable objects isolated to this actor:

func updateRotationCoordinator(_ callback: @escaping @MainActor (CGAffineTransform) -> Void {
    ...
        .receive(on: DispatchQueue.main)
        .sink { degrees in
                let radians = degrees * .pi / 180
                let t = CGAffineTransform(rotationAngle: radians)
                MainActor.assumeIsolated {
                    callback(t)
                }
            }
    ...
}

@MainActor
func setDisplayLayer(_ layer:CALayer) {
     // Closure must be constructed in @MainActor-isolated context
    var callback: @MainActor (CGAffineTransform) -> Void = { t in
        displayLayer.setAffineTransform(t)
    }
    Task { @CameraActor in
        await cameraManager.updateRotationCoordinator(callback)
    }
}
1 Like

But the layer is required in updateRotationCoordinator. Please refer to the last part of the code in the question.

Only for displayLayer.setAffineTransform, or for something else?

Well the RotationCoordinator takes this layer as an input and generates updates whenever the transform of this layer changes.

CALayer is not marked with any concurrency attributes yet, so it is not Sendable.

You can try downgrade this error into a warning by @preconcurrency import QuartzCore. Or you can retroactively state it by extension CALayer: @retroactive @unchecked Sendable {}.

Either way, it then becomes your duty to guarantee there's no actual runtime problems.

2 Likes

You need to wrap CALayer into something isolated to the @MainActor. If it is the only usage - closure will suffice. If there more, e.g. you need to read initial value of the transform, then you wrap layer into a GAIT:

@MainActor
struct CALayerRef {
    var layer: CALayer

    var transform: CGAffineTransform {
        get { layer.affineTransform }
        set { layer.setAfiineTransform(newValue)
    }
}
1 Like

Will not work as layer would be added to the rotationObserver in the actor context.

Closure or CALayerRef must be constructed in the @MainActor-isolated context. Once constructed, it can be passed from @MainActor to @CameraActor, because it is Sendable.

1 Like

There is a build error again here :

Reference to captured var 'callback' in concurrently-executing code; this is an error in the Swift 6 language mode

For me it compilers without errors or warnings using Xcode 16.1 (16B40), using both Swift 6 and Swift 5 + strict checking:

import SwiftUI
import Combine

@globalActor
actor CameraActor: GlobalActor {
    static let shared: CameraActor = .init()
}

final class CameraManager: Sendable {
    @CameraActor
    func updateRotation(_ callback: @escaping @MainActor (CGAffineTransform) -> Void) {
        var cancellables: Set<AnyCancellable> = []
        Just(CGAffineTransform.init())
            .receive(on: DispatchQueue.main)
            .sink { t in
                MainActor.assumeIsolated {
                    callback(t)
                }
            }.store(in: &cancellables)
    }
}

struct SampleBufferView: SwiftUI.UIViewRepresentable {
    let manager: CameraManager

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        view.backgroundColor = UIColor.blue

        let layer: CALayer = view.layer

        let callback: @MainActor (CGAffineTransform) -> Void = { t in
            layer.setAffineTransform(t)
        }

        Task { @MainActor in
            // Notify viewModel about the layer
            await manager.updateRotation(callback)
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}