mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
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:
@@ -17,6 +17,7 @@ struct DeviceInfo {
|
|||||||
let batteryLevel: String?
|
let batteryLevel: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
protocol DeviceInfoProvider {
|
protocol DeviceInfoProvider {
|
||||||
func load() async -> DeviceInfo
|
func load() async -> DeviceInfo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import Foundation
|
|||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
/// Receive-destination descriptor, ported from `core/FileSystemService.kt`.
|
/// Receive-destination descriptor, ported from `core/FileSystemService.kt`.
|
||||||
enum ReceiveFolderKind: String, Codable {
|
enum ReceiveFolderKind: String, Codable, Sendable {
|
||||||
case fileSystemPath
|
case fileSystemPath
|
||||||
case iosSecurityScopedUrl
|
case iosSecurityScopedUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ReceiveFolder: Equatable, Codable {
|
struct ReceiveFolder: Equatable, Codable, Sendable {
|
||||||
let kind: ReceiveFolderKind
|
let kind: ReceiveFolderKind
|
||||||
let value: String
|
let value: String
|
||||||
let displayName: String
|
let displayName: String
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import Foundation
|
|||||||
/// the generated UniFFI records/enums into these so the UI never depends on the
|
/// the generated UniFFI records/enums into these so the UI never depends on the
|
||||||
/// binding surface directly.
|
/// binding surface directly.
|
||||||
|
|
||||||
struct CoreStatus: Equatable {
|
struct CoreStatus: Equatable, Sendable {
|
||||||
let endpointId: String
|
let endpointId: String
|
||||||
let activeTransfers: UInt64
|
let activeTransfers: UInt64
|
||||||
let activeShares: UInt64
|
let activeShares: UInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CoreEventModel: Equatable, Identifiable {
|
struct CoreEventModel: Equatable, Identifiable, Sendable {
|
||||||
let id: String
|
let id: String
|
||||||
let timestamp: Int64
|
let timestamp: Int64
|
||||||
let scope: String
|
let scope: String
|
||||||
@@ -21,17 +21,17 @@ struct CoreEventModel: Equatable, Identifiable {
|
|||||||
let dataJson: String
|
let dataJson: String
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ShareAccessPolicy: Equatable {
|
enum ShareAccessPolicy: Equatable, Sendable {
|
||||||
case requireApproval
|
case requireApproval
|
||||||
case anyoneWithTransfer
|
case anyoneWithTransfer
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TransferDirection: Equatable {
|
enum TransferDirection: Equatable, Sendable {
|
||||||
case send
|
case send
|
||||||
case receive
|
case receive
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TransferStatus: Equatable {
|
enum TransferStatus: Equatable, Sendable {
|
||||||
case importing
|
case importing
|
||||||
case sharing
|
case sharing
|
||||||
case receiving
|
case receiving
|
||||||
@@ -41,7 +41,7 @@ enum TransferStatus: Equatable {
|
|||||||
case stopped
|
case stopped
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Transfer: Equatable, Identifiable {
|
struct Transfer: Equatable, Identifiable, Sendable {
|
||||||
let localId: String
|
let localId: String
|
||||||
let transferId: UInt64
|
let transferId: UInt64
|
||||||
let direction: TransferDirection
|
let direction: TransferDirection
|
||||||
@@ -59,7 +59,7 @@ struct Transfer: Equatable, Identifiable {
|
|||||||
var id: String { localId }
|
var id: String { localId }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Share: Equatable {
|
struct Share: Equatable, Sendable {
|
||||||
let transferId: UInt64
|
let transferId: UInt64
|
||||||
let ticket: String
|
let ticket: String
|
||||||
let transferName: String
|
let transferName: String
|
||||||
@@ -68,7 +68,7 @@ struct Share: Equatable {
|
|||||||
let totalSize: UInt64
|
let totalSize: UInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransferMetadataModel: Equatable {
|
struct TransferMetadataModel: Equatable, Sendable {
|
||||||
let transferId: UInt64
|
let transferId: UInt64
|
||||||
let transferName: String
|
let transferName: String
|
||||||
let senderName: String?
|
let senderName: String?
|
||||||
@@ -77,12 +77,12 @@ struct TransferMetadataModel: Equatable {
|
|||||||
let totalSize: UInt64
|
let totalSize: UInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TicketInspectionModel: Equatable {
|
struct TicketInspectionModel: Equatable, Sendable {
|
||||||
let kind: String
|
let kind: String
|
||||||
let metadata: TransferMetadataModel
|
let metadata: TransferMetadataModel
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReceiverDeliveryStatus: Equatable {
|
enum ReceiverDeliveryStatus: Equatable, Sendable {
|
||||||
case requested
|
case requested
|
||||||
case accepted
|
case accepted
|
||||||
case refused
|
case refused
|
||||||
@@ -91,7 +91,7 @@ enum ReceiverDeliveryStatus: Equatable {
|
|||||||
case unknown
|
case unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ReceiverRequestModel: Equatable, Identifiable {
|
struct ReceiverRequestModel: Equatable, Identifiable, Sendable {
|
||||||
let id: String
|
let id: String
|
||||||
let transferId: UInt64
|
let transferId: UInt64
|
||||||
let remoteEndpointId: String
|
let remoteEndpointId: String
|
||||||
@@ -106,7 +106,7 @@ struct ReceiverRequestModel: Equatable, Identifiable {
|
|||||||
let completedAt: Int64?
|
let completedAt: Int64?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CoreState: Equatable {
|
struct CoreState: Equatable, Sendable {
|
||||||
var isInitialized: Bool = false
|
var isInitialized: Bool = false
|
||||||
var status: CoreStatus?
|
var status: CoreStatus?
|
||||||
var events: [CoreEventModel] = []
|
var events: [CoreEventModel] = []
|
||||||
@@ -116,7 +116,7 @@ struct CoreState: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Coalesced change hints emitted from the event sink, ported from `CoreSignal`.
|
/// Coalesced change hints emitted from the event sink, ported from `CoreSignal`.
|
||||||
enum CoreSignal: Equatable {
|
enum CoreSignal: Equatable, Sendable {
|
||||||
case approvalChanged(transferId: UInt64)
|
case approvalChanged(transferId: UInt64)
|
||||||
case receiverHistoryChanged(transferId: UInt64)
|
case receiverHistoryChanged(transferId: UInt64)
|
||||||
/// Transfer status/history changed enough to re-read the durable snapshot.
|
/// Transfer status/history changed enough to re-read the durable snapshot.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import VnidropCore
|
@preconcurrency import VnidropCore
|
||||||
|
|
||||||
/// Swift port of `core/CoreRepository.kt`. Owns the `VnidropCore` handle, maps the
|
/// Swift port of `core/CoreRepository.kt`. Owns the `VnidropCore` handle, maps the
|
||||||
/// generated UniFFI records into app domain models, publishes an observable
|
/// generated UniFFI records into app domain models, publishes an observable
|
||||||
@@ -16,13 +16,16 @@ final class CoreRepository: ObservableObject {
|
|||||||
/// Coalesced change hints; subscribe to react to approval/history/transfer changes.
|
/// Coalesced change hints; subscribe to react to approval/history/transfer changes.
|
||||||
var signals: AnyPublisher<CoreSignal, Never> { signalsSubject.eraseToAnyPublisher() }
|
var signals: AnyPublisher<CoreSignal, Never> { signalsSubject.eraseToAnyPublisher() }
|
||||||
|
|
||||||
private var core: VnidropCore?
|
// Set on the main actor (initialize/shutdown) but read from `queue` inside
|
||||||
|
// `runCore`; the underlying core is internally synchronized, so this crossing
|
||||||
|
// is safe. `nonisolated(unsafe)` documents that contract for Swift 6.
|
||||||
|
private nonisolated(unsafe) var core: VnidropCore?
|
||||||
private let queue = DispatchQueue(label: "com.vnidrop.core", qos: .userInitiated)
|
private let queue = DispatchQueue(label: "com.vnidrop.core", qos: .userInitiated)
|
||||||
private lazy var sink = RepositoryEventSink { [weak self] event in
|
private lazy var sink = RepositoryEventSink { [weak self] event in
|
||||||
Task { @MainActor in self?.handle(event: event) }
|
Task { @MainActor in self?.handle(event: event) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let maxEvents = 200
|
private nonisolated static let maxEvents = 200
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
@@ -186,7 +189,7 @@ final class CoreRepository: ObservableObject {
|
|||||||
// MARK: - Internals
|
// MARK: - Internals
|
||||||
|
|
||||||
/// Snapshot of the values read from the core in one pass.
|
/// Snapshot of the values read from the core in one pass.
|
||||||
private struct CoreSnapshot {
|
private struct CoreSnapshot: Sendable {
|
||||||
let status: CoreStatus
|
let status: CoreStatus
|
||||||
let transfers: [Transfer]
|
let transfers: [Transfer]
|
||||||
let events: [CoreEventModel]
|
let events: [CoreEventModel]
|
||||||
@@ -194,11 +197,11 @@ final class CoreRepository: ObservableObject {
|
|||||||
|
|
||||||
/// Reads the current core state. Safe to call off the main actor (pure core
|
/// Reads the current core state. Safe to call off the main actor (pure core
|
||||||
/// FFI reads); does not touch `@Published` state.
|
/// FFI reads); does not touch `@Published` state.
|
||||||
private func readSnapshot() -> CoreSnapshot? {
|
private nonisolated func readSnapshot() -> CoreSnapshot? {
|
||||||
guard let core = self.core else { return nil }
|
guard let core = self.core else { return nil }
|
||||||
let status = core.status()
|
let status = core.status()
|
||||||
let transfers = (try? core.listTransfers())?.map { $0.toModel() } ?? state.transfers
|
let transfers = (try? core.listTransfers())?.map { $0.toModel() } ?? []
|
||||||
let events = (try? core.listEvents(transferId: nil))?.prefix(Self.maxEvents).map { $0.toModel() } ?? state.events
|
let events = (try? core.listEvents(transferId: nil))?.prefix(Self.maxEvents).map { $0.toModel() } ?? []
|
||||||
return CoreSnapshot(
|
return CoreSnapshot(
|
||||||
status: CoreStatus(
|
status: CoreStatus(
|
||||||
endpointId: status.endpointId,
|
endpointId: status.endpointId,
|
||||||
@@ -221,7 +224,7 @@ final class CoreRepository: ObservableObject {
|
|||||||
if let snapshot = readSnapshot() { applySnapshot(snapshot) }
|
if let snapshot = readSnapshot() { applySnapshot(snapshot) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requireCore() throws -> VnidropCore {
|
private nonisolated func requireCore() throws -> VnidropCore {
|
||||||
guard let core = self.core else {
|
guard let core = self.core else {
|
||||||
throw InvitationError.message("Initialize the core first.")
|
throw InvitationError.message("Initialize the core first.")
|
||||||
}
|
}
|
||||||
@@ -229,7 +232,7 @@ final class CoreRepository: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Runs a blocking core call off the main actor and hops the result back.
|
/// Runs a blocking core call off the main actor and hops the result back.
|
||||||
private func runCore<T>(_ block: @escaping () throws -> T) async -> Result<T, Error> {
|
private nonisolated func runCore<T: Sendable>(_ block: @escaping @Sendable () throws -> T) async -> Result<T, Error> {
|
||||||
await withCheckedContinuation { continuation in
|
await withCheckedContinuation { continuation in
|
||||||
queue.async {
|
queue.async {
|
||||||
let result: Result<T, Error>
|
let result: Result<T, Error>
|
||||||
@@ -243,7 +246,7 @@ final class CoreRepository: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func nextTransferId() -> UInt64 {
|
private nonisolated static func nextTransferId() -> UInt64 {
|
||||||
UInt64.random(in: 1...UInt64(Int64.max))
|
UInt64.random(in: 1...UInt64(Int64.max))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import VnidropCore
|
|||||||
|
|
||||||
/// A file/folder selected for sharing, ported from `PickedShareFile` in
|
/// A file/folder selected for sharing, ported from `PickedShareFile` in
|
||||||
/// `core/FilePicker.kt`.
|
/// `core/FilePicker.kt`.
|
||||||
struct PickedShareFile: Equatable, Identifiable {
|
struct PickedShareFile: Equatable, Identifiable, Sendable {
|
||||||
let value: String
|
let value: String
|
||||||
let displayName: String
|
let displayName: String
|
||||||
var sizeBytes: UInt64? = nil
|
var sizeBytes: UInt64? = nil
|
||||||
@@ -18,6 +18,7 @@ struct PickedShareFile: Equatable, Identifiable {
|
|||||||
|
|
||||||
/// Receive-destination and share-source platform bridge, ported from
|
/// Receive-destination and share-source platform bridge, ported from
|
||||||
/// `core/FileSystemService.kt` and its iOS/desktop actuals.
|
/// `core/FileSystemService.kt` and its iOS/desktop actuals.
|
||||||
|
@MainActor
|
||||||
protocol FileSystemService {
|
protocol FileSystemService {
|
||||||
var supportsCustomReceiveFolders: Bool { get }
|
var supportsCustomReceiveFolders: Bool { get }
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ struct InvitationReviewPanel: View {
|
|||||||
.background(colors.backgroundSurface200, in: RoundedRectangle(cornerRadius: 14))
|
.background(colors.backgroundSurface200, in: RoundedRectangle(cornerRadius: 14))
|
||||||
|
|
||||||
Field(label: String(localized: "field_receiver_name"),
|
Field(label: String(localized: "field_receiver_name"),
|
||||||
value: Binding(get: { state.receiverName }, set: model.setReceiverName))
|
value: Binding(get: { state.receiverName }, set: { model.setReceiverName($0) }))
|
||||||
Text(state.receiveFolder?.displayName ?? String(localized: "value_unavailable"))
|
Text(state.receiveFolder?.displayName ?? String(localized: "value_unavailable"))
|
||||||
.font(VniType.bodySmall)
|
.font(VniType.bodySmall)
|
||||||
.foregroundStyle(state.folderAccessStatus == .writable ? colors.foregroundLight : colors.destructiveDefault)
|
.foregroundStyle(state.folderAccessStatus == .writable ? colors.foregroundLight : colors.destructiveDefault)
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ struct TransferComposer: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Field(label: String(localized: "field_transfer_name"),
|
Field(label: String(localized: "field_transfer_name"),
|
||||||
value: Binding(get: { state.transferName }, set: model.setTransferName))
|
value: Binding(get: { state.transferName }, set: { model.setTransferName($0) }))
|
||||||
Field(label: String(localized: "field_sender_name"),
|
Field(label: String(localized: "field_sender_name"),
|
||||||
value: Binding(get: { state.senderName }, set: model.setSenderName))
|
value: Binding(get: { state.senderName }, set: { model.setSenderName($0) }))
|
||||||
Text(LocalizedStringKey("send_access_title")).font(.headline)
|
Text(LocalizedStringKey("send_access_title")).font(.headline)
|
||||||
PolicyOption(
|
PolicyOption(
|
||||||
icon: "checkmark.shield", titleKey: "send_access_approval", descKey: "send_access_approval_description",
|
icon: "checkmark.shield", titleKey: "send_access_approval", descKey: "send_access_approval_description",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct BugReportDraft {
|
|||||||
|
|
||||||
/// Bug-report submission. The full diagnostics transport (URLSession + build
|
/// Bug-report submission. The full diagnostics transport (URLSession + build
|
||||||
/// config) lands in the diagnostics phase; this protocol is the stable seam.
|
/// config) lands in the diagnostics phase; this protocol is the stable seam.
|
||||||
|
@MainActor
|
||||||
protocol BugReportService {
|
protocol BugReportService {
|
||||||
func submit(_ draft: BugReportDraft, deviceInfo: DeviceInfo?) async -> Result<Void, Error>
|
func submit(_ draft: BugReportDraft, deviceInfo: DeviceInfo?) async -> Result<Void, Error>
|
||||||
func previewLogBytes() async -> Int
|
func previewLogBytes() async -> Int
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ struct PreferencesSettings: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Section(String(localized: "field_username")) {
|
Section(String(localized: "field_username")) {
|
||||||
TextField(String(localized: "field_username"),
|
TextField(String(localized: "field_username"),
|
||||||
text: Binding(get: { model.state.username }, set: model.setUsername))
|
text: Binding(get: { model.state.username }, set: { model.setUsername($0) }))
|
||||||
}
|
}
|
||||||
if model.state.supportsCustomReceiveFolders {
|
if model.state.supportsCustomReceiveFolders {
|
||||||
Section(String(localized: "preferences_receive_folder_title")) {
|
Section(String(localized: "preferences_receive_folder_title")) {
|
||||||
@@ -228,25 +228,25 @@ struct BugReportSettings: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section(String(localized: "bug_report_what_label")) {
|
Section(String(localized: "bug_report_what_label")) {
|
||||||
TextField("", text: Binding(get: { model.state.bugWhatHappened }, set: model.setBugWhatHappened),
|
TextField("", text: Binding(get: { model.state.bugWhatHappened }, set: { model.setBugWhatHappened($0) }),
|
||||||
prompt: Text(LocalizedStringKey("bug_report_what_hint")), axis: .vertical)
|
prompt: Text(LocalizedStringKey("bug_report_what_hint")), axis: .vertical)
|
||||||
.lineLimit(3, reservesSpace: true)
|
.lineLimit(3, reservesSpace: true)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
Section(String(localized: "bug_report_expected_label")) {
|
Section(String(localized: "bug_report_expected_label")) {
|
||||||
TextField("", text: Binding(get: { model.state.bugExpected }, set: model.setBugExpected),
|
TextField("", text: Binding(get: { model.state.bugExpected }, set: { model.setBugExpected($0) }),
|
||||||
prompt: Text(LocalizedStringKey("bug_report_expected_hint")), axis: .vertical)
|
prompt: Text(LocalizedStringKey("bug_report_expected_hint")), axis: .vertical)
|
||||||
.lineLimit(3, reservesSpace: true)
|
.lineLimit(3, reservesSpace: true)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
Section(String(localized: "bug_report_steps_label")) {
|
Section(String(localized: "bug_report_steps_label")) {
|
||||||
TextField("", text: Binding(get: { model.state.bugSteps }, set: model.setBugSteps),
|
TextField("", text: Binding(get: { model.state.bugSteps }, set: { model.setBugSteps($0) }),
|
||||||
prompt: Text(LocalizedStringKey("bug_report_steps_hint")), axis: .vertical)
|
prompt: Text(LocalizedStringKey("bug_report_steps_hint")), axis: .vertical)
|
||||||
.lineLimit(3, reservesSpace: true)
|
.lineLimit(3, reservesSpace: true)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
Section(String(localized: "bug_report_contact_label")) {
|
Section(String(localized: "bug_report_contact_label")) {
|
||||||
TextField("", text: Binding(get: { model.state.bugContact }, set: model.setBugContact),
|
TextField("", text: Binding(get: { model.state.bugContact }, set: { model.setBugContact($0) }),
|
||||||
prompt: Text(LocalizedStringKey("bug_report_contact_hint")))
|
prompt: Text(LocalizedStringKey("bug_report_contact_hint")))
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
@preconcurrency import AVFoundation
|
||||||
import CoreNFC
|
@preconcurrency import CoreNFC
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -98,6 +98,8 @@ final class IosReceiveInvitationActions: NSObject, ReceiveInvitationActions, UID
|
|||||||
case .authorized:
|
case .authorized:
|
||||||
completion(true)
|
completion(true)
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
|
// The permission callback is delivered back on the main queue.
|
||||||
|
nonisolated(unsafe) let completion = completion
|
||||||
AVCaptureDevice.requestAccess(for: .video) { granted in
|
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||||
DispatchQueue.main.async { completion(granted) }
|
DispatchQueue.main.async { completion(granted) }
|
||||||
}
|
}
|
||||||
@@ -183,11 +185,14 @@ final class QrScannerViewController: UIViewController, AVCaptureMetadataOutputOb
|
|||||||
DispatchQueue.global(qos: .userInitiated).async { [session] in session.startRunning() }
|
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
|
let value = metadataObjects
|
||||||
.compactMap { $0 as? AVMetadataMachineReadableCodeObject }
|
.compactMap { $0 as? AVMetadataMachineReadableCodeObject }
|
||||||
.first { $0.type == .qr }?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
.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>) {
|
private func finish(_ result: Result<String, Error>) {
|
||||||
@@ -203,7 +208,8 @@ final class QrScannerViewController: UIViewController, AVCaptureMetadataOutputOb
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// NFC invitation reader, ported from `InvitationNfcReader`.
|
/// 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 let onResult: (Result<String, Error>) -> Void
|
||||||
private var session: NFCNDEFReaderSession?
|
private var session: NFCNDEFReaderSession?
|
||||||
private var finished = false
|
private var finished = false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreNFC
|
@preconcurrency import CoreNFC
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func makePlatformShareActions() -> TransferShareActions { IosTransferShareActions() }
|
func makePlatformShareActions() -> TransferShareActions { IosTransferShareActions() }
|
||||||
@@ -63,7 +63,8 @@ final class IosTransferShareActions: NSObject, TransferShareActions {
|
|||||||
|
|
||||||
/// Writes a VniDrop invitation to a writable NDEF tag, ported from
|
/// Writes a VniDrop invitation to a writable NDEF tag, ported from
|
||||||
/// `InvitationNfcWriter` in `TransferShareActions.ios.kt`.
|
/// `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 ticket: String
|
||||||
private let onResult: (Result<Void, Error>) -> Void
|
private let onResult: (Result<Void, Error>) -> Void
|
||||||
private var session: NFCNDEFReaderSession?
|
private var session: NFCNDEFReaderSession?
|
||||||
@@ -95,9 +96,13 @@ final class InvitationNfcWriter: NSObject, NFCNDEFReaderSessionDelegate {
|
|||||||
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {}
|
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {}
|
||||||
|
|
||||||
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
|
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")))
|
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
|
session.connect(to: tag) { [weak self] connectError in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
if let connectError { return self.finish(.failure(connectError)) }
|
if let connectError { return self.finish(.failure(connectError)) }
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ targets:
|
|||||||
# (the editor reads these build settings, not the manual plist).
|
# (the editor reads these build settings, not the manual plist).
|
||||||
INFOPLIST_KEY_CFBundleDisplayName: VniDrop
|
INFOPLIST_KEY_CFBundleDisplayName: VniDrop
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.utilities
|
INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.utilities
|
||||||
SWIFT_VERSION: "5.9"
|
SWIFT_VERSION: "6.0"
|
||||||
|
SWIFT_STRICT_CONCURRENCY: complete
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
CODE_SIGN_STYLE: Automatic
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
|||||||
Reference in New Issue
Block a user