xcutils v0.4.0
- Tags:
- open-source
Release Notes
- Improve release process
- Increase Swift version 5.9
- Increase macOS version to 13
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 ✌️
This is not the final fix, but it will at least prevent the crash.
We still need to find the root cause of this because it causes headers to not be refreshed, leading to visual bugs.
It'll be interesting to see if this also reduces the crash rate on iOS 16.
Initial release with support for adding Hashable
conformance on iOS, macOS, watchOS, tvOS, visionOS, and Linux.
Add PrivacyInfo.xcprivacy
Add PrivacyInfo.xcprivacy
Without this change we can see a crash in the app because the size for an unknown cell is requested. I think this is caused by the call to performBatchUpdates
triggering a layout before the indexes have updated.
This works around a bug from ~iOS 14 that causes the last N items in a section to ignore any updates applied to them when N items are deleted from the same section and the items are at the end of the section.
This also removes the conversion of inserts/removals in to updates, which reduces the need to account for these synthesised updates and also more closely matches the intent of the user; if something has truly moved or been updated the appropriate methods can be used, while removals and inserts that happen to result in the same position will not animate as a refresh.
This also fixes the crash that #6 was originally aiming to fix. It became much easier to reason about the changes once the reloads were not synthesised and were using the post-updates index paths. I think this probably fixes some other bugs. At a minimum the existing tests were updated or still passed, plus new tests have been added.
I also created https://github.com/opennetltd/ComposedUITests/pull/1, which adds some more tests to the integration project. These are mostly used to validate that the unit tests here are correct according to UICollectionView
, e.g. it does not crash, request any unexpected cells, or fallback to a reloadData
.