Skip to content

Pull request Compositonal layout improvements on opennetltd/Composed


This PR is primarily focussed on improving support for compositional layouts, however it also provides some improvements for flow layouts too.

  • Swift version has been updated to 5.9 (our current minimum)
  • iOS version has been updated to 14 (our current minimum)
  • An assertion has been added to UICollectionViewCompositionalLayout to ease debugging section providers that include children that have not yet adopted CompositionalLayoutHandler
  • debugLog has been updated to use an auto closure for the message
    • This is unrelated to other changes but provides a small performance improvement when debug logs are disabled
  • Supplementary view updates have been moved to a separate batch updates
    • Without this some updates will crash on iOS 17 with a compositional layout (see https://github.com/opennetltd/ComposedUITests/commit/4f54a1f19c81e6fa2f5354bd714f045e1c39a8be)
    • This has the bonus of always using the correct index path during a batch updates, which should be an improvement for flow layouts too

xcutils v0.4.0


Release Notes

  • Improve release process
  • Increase Swift version 5.9
  • Increase macOS version to 13

Smuggling Values Across Actors


I recently started updating an app to use the Swift 6 language mode and ran in to an issue using AVCaptureMetadataOutputObjectsDelegate. The issue is that the compiler cannot reason about which actor the delegate function is called on and it must be treated as nonisolated, however we know that it's being called on a specific actor (in this case the MainActor), so how can we tell the compiler?

In Swift 5 language mode we can use MainActor.assumeIsolated and call it a day. But in Swift 6 this will produce an error:

import AVFoundation
import UIKit

@MainActor
final class ViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
    private var output: AVCaptureMetadataOutput?

    override func viewDidLoad() {
        super.viewDidLoad()

        let output = AVCaptureMetadataOutput()
        output.setMetadataObjectsDelegate(self, queue: .main)
        self.output = output
    }

    private func doSomethingWithMetadataObjects(_ metadataObjects: [AVMetadataObject]) {}

    // Must be marked nonisolated because the AVCaptureMetadataOutputObjectsDelegate protocol cannot declare the actor on which the function will be called.
    nonisolated func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        // We know it's on the main actor so we should be able to assume
        // isolated and pass metadata objects to another function.
        MainActor.assumeIsolated {
            // This does not compile though, presumably because AVMetadataObject
            // is not Sendable.
            doSomethingWithMetadataObjects(metadataObjects) // ❌ Error: Sending 'metadataObjects' risks causing data races
        }
    }
}

This reminded me of a macro I created recently, or more specifically the type used to workaround a similar issue with stored properties in Swift 5: UnsafeSendable. This type works by using @unchecked Sendable to send store values we know are sendable but have no native way of telling the compiler.

In this case, however, it can also be used to smuggle values across an actor boundary that we know is safe.

public struct Smuggler<Smuggled>: @unchecked Sendable {
    public var smuggled: Smuggled

    public init(unsafeSmuggled smuggled: Smuggled) {
        self.smuggled = smuggled
    }

    @available(*, deprecated, message: "Smuggler is not needed when `Smuggled` is Sendable")
    public init(unsafeSmuggled smuggled: Smuggled) where Smuggled: Sendable {
        self.smuggled = smuggled
    }
}

With this we can smuggle our value through:

    nonisolated func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        // We know it's on the main actor so we should be able to assume
        // isolated and pass metadata objects to another function.
        let smuggler = Smuggler(unsafeSmuggled: metadataObjects)
        MainActor.assumeIsolated {
            let metadataObjects = smuggler.smuggled
            doSomethingWithMetadataObjects(metadataObjects) // ✅ Works
        }
    }

We can add an extension to MainActor that makes this a little more convenient.

extension MainActor {
    public static func assumeIsolated<Smuggled>(
        smuggling smuggled: Smuggled,
        operation: @MainActor (_ smuggled: Smuggled) -> Void,
        file: StaticString = #fileID,
        line: UInt = #line
    ) {
        let smuggler = Smuggler(unsafeSmuggled: smuggled)
        withoutActuallyEscaping(operation) { escapingOperation in
            let smuggledOperation = Smuggler(unsafeSmuggled: escapingOperation)
            assumeIsolated({
                smuggledOperation.smuggled(smuggler.smuggled)
            }, file: file, line: line)
        }
    }
}

I have been playing around with trying to add a function to GlobalActor that would make the API a bit nicer but I think it's going to require a macro, lest we resort to reimplementing MainActor.assumeIsolated 😊

Although this isn't really "smuggling" because the value is not actually crossing an actor boundary I like the term and I'm sticking to it! Especially because I assume this workaround will be temporary.

P.S. I created a demo project of the issue, which I also submitted to Apple as feedback FB13950073 ✌️

HashableMacro 1.0.0


Release Notes

Initial release with support for adding Hashable conformance on iOS, macOS, watchOS, tvOS, visionOS, and Linux.