refactor(apple): adopt Swift 6 language mode

Enable complete strict concurrency and switch the app target to Swift 6.

- Isolate model dependency protocols to @MainActor
- Make the CoreRepository blocking-FFI bridge race-free: nonisolated(unsafe)
  core handle, nonisolated runCore/readSnapshot, @Sendable work block,
  Sendable domain models
- Fix Binding method-reference captures; @preconcurrency imports for
  CoreNFC/AVFoundation/VnidropCore; isolate the NFC/QR delegate helpers
This commit is contained in:
2026-07-19 18:39:34 +02:00
parent bfa489def1
commit b1b8fa202c
12 changed files with 61 additions and 43 deletions

View File

@@ -1,7 +1,7 @@
#if os(iOS)
import UIKit
import AVFoundation
import CoreNFC
@preconcurrency import AVFoundation
@preconcurrency import CoreNFC
import UniformTypeIdentifiers
@MainActor
@@ -98,6 +98,8 @@ final class IosReceiveInvitationActions: NSObject, ReceiveInvitationActions, UID
case .authorized:
completion(true)
case .notDetermined:
// The permission callback is delivered back on the main queue.
nonisolated(unsafe) let completion = completion
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async { completion(granted) }
}
@@ -183,11 +185,14 @@ final class QrScannerViewController: UIViewController, AVCaptureMetadataOutputOb
DispatchQueue.global(qos: .userInitiated).async { [session] in session.startRunning() }
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
// The metadata output delegate queue is `.main`, so hop back onto the main
// actor to touch view-controller state.
nonisolated func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
let value = metadataObjects
.compactMap { $0 as? AVMetadataMachineReadableCodeObject }
.first { $0.type == .qr }?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !value.isEmpty { finish(.success(value)) }
guard !value.isEmpty else { return }
MainActor.assumeIsolated { finish(.success(value)) }
}
private func finish(_ result: Result<String, Error>) {
@@ -203,7 +208,8 @@ final class QrScannerViewController: UIViewController, AVCaptureMetadataOutputOb
}
/// NFC invitation reader, ported from `InvitationNfcReader`.
final class InvitationNfcReader: NSObject, NFCNDEFReaderSessionDelegate {
// Runs entirely on the NFC session's `.main` delegate queue.
final class InvitationNfcReader: NSObject, NFCNDEFReaderSessionDelegate, @unchecked Sendable {
private let onResult: (Result<String, Error>) -> Void
private var session: NFCNDEFReaderSession?
private var finished = false

View File

@@ -1,6 +1,6 @@
#if os(iOS)
import UIKit
import CoreNFC
@preconcurrency import CoreNFC
@MainActor
func makePlatformShareActions() -> TransferShareActions { IosTransferShareActions() }
@@ -63,7 +63,8 @@ final class IosTransferShareActions: NSObject, TransferShareActions {
/// Writes a VniDrop invitation to a writable NDEF tag, ported from
/// `InvitationNfcWriter` in `TransferShareActions.ios.kt`.
final class InvitationNfcWriter: NSObject, NFCNDEFReaderSessionDelegate {
// Runs entirely on the NFC session's `.main` delegate queue.
final class InvitationNfcWriter: NSObject, NFCNDEFReaderSessionDelegate, @unchecked Sendable {
private let ticket: String
private let onResult: (Result<Void, Error>) -> Void
private var session: NFCNDEFReaderSession?
@@ -95,9 +96,13 @@ final class InvitationNfcWriter: NSObject, NFCNDEFReaderSessionDelegate {
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {}
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
guard let tag = tags.first else {
guard let firstTag = tags.first else {
return finish(.failure(InvitationError.message("No NFC tag was detected")))
}
// CoreNFC completion handlers run on the session's `.main` queue; these
// framework values are safe to use there.
nonisolated(unsafe) let session = session
nonisolated(unsafe) let tag = firstTag
session.connect(to: tag) { [weak self] connectError in
guard let self else { return }
if let connectError { return self.finish(.failure(connectError)) }