From 7047dc154605e2c2e69e1a5684559b5e43749f95 Mon Sep 17 00:00:00 2001 From: Bradley Mackey Date: Sat, 12 Sep 2026 16:09:13 +0400 Subject: [PATCH] Fix Xcode 27 build and runtime failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swift 6.3 adds the NoUseUnstructuredThrowingTask diagnostic and rejects retroactive Sendable conformances. Combined with the package's -warnings-as-errors flag, these broke the build in several places: - AsyncButton, VaultItemOnTapDecoratorViewGenerator and VaultItemFeedView discarded throwing unstructured Tasks. Errors were already deliberately swallowed at these sites, so the discard is now explicit via `try?`. - IntervalTimerTests and PendingValueTests did the same. The timer tests now record an Issue so a throw fails the test; PendingValue's task is cancelled by the test itself, so its throw stays ignored. - VaultItemPreviewViewGeneratorMock's VaultItemCopyActionHandler conformance required a retroactive Sendable conformance. It had no callers — every site passes VaultItemCopyActionHandlerMock — so the extension is removed. - VaultDetailEncryptionEditView assigned through the @State wrappedValue setter before self was fully initialized. Now uses State(initialValue:). Data.asType also crashed at runtime. UnsafeRawBufferPointer.load(as:) requires T-aligned memory and Data makes no alignment guarantee for its backing buffer, which the Swift 6.3 runtime now traps on. This took out the HOTP and TOTP suites entirely. Switched to loadUnaligned(as:). Co-Authored-By: Claude Opus 5 (1M context) --- .../CryptoEngine/Helpers/Data+Helpers.swift | 5 +++- .../VaultDetailEncryptionEditView.swift | 2 +- .../Views/Feed/VaultItemFeedView.swift | 4 +++- .../VaultiOS/Views/General/AsyncButton.swift | 5 +++- ...VaultItemOnTapDecoratorViewGenerator.swift | 6 ++++- .../VaultItemPreviewViewGenerator.swift | 8 ------- .../PendingValueTests.swift | 3 ++- .../Clock/IntervalTimerTests.swift | 24 ++++++++++++------- 8 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift b/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift index f41dd30e9..9775bbdad 100644 --- a/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift +++ b/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift @@ -9,9 +9,12 @@ extension UInt64 { extension Data { /// An unsafe operation to cast the bytes of this data to the provided type. + /// + /// `Data` gives no alignment guarantee for its backing buffer — a slice can start at any byte + /// offset — so the load must tolerate misalignment. An aligned `load` traps at runtime instead. func asType(_: T.Type) -> T { withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in - ptr.load(as: T.self) + ptr.loadUnaligned(as: T.self) } } diff --git a/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift b/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift index 96f00de91..41248d46d 100644 --- a/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift +++ b/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift @@ -27,7 +27,7 @@ struct VaultDetailEncryptionEditView: View { ) { self.title = title self.description = description - encryptionIsEnabled = encryptionInitiallyEnabled + _encryptionIsEnabled = State(initialValue: encryptionInitiallyEnabled) self.didSetNewEncryptionPassword = didSetNewEncryptionPassword self.didRemoveEncryption = didRemoveEncryption } diff --git a/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift b/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift index d801400fb..f57fa4fe4 100644 --- a/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift +++ b/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift @@ -231,7 +231,9 @@ public struct VaultItemFeedView< dataModel.items.move(fromOffsets: [move.fromIndex], toOffset: move.toIndex) } Task { - try await dataModel.reorder(items: [dropItem], to: move.reorderingPosition) + // The list has already moved optimistically. A failed persist leaves the + // stored order untouched and the next reload restores the on-disk order. + try? await dataModel.reorder(items: [dropItem], to: move.reorderingPosition) } return true } diff --git a/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift b/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift index 9c3f64ce2..e3c1fde5d 100644 --- a/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift +++ b/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift @@ -38,7 +38,10 @@ struct AsyncButton: View { isDisabled = false showProgressView = false } - try await action() + + // Errors belong to the action: call sites either catch them inline or surface them + // through their own view model state. The button only drives loading and disabled state. + try? await action() } }, label: { diff --git a/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift b/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift index 0dc09ac33..a993a69f1 100644 --- a/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift +++ b/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift @@ -20,7 +20,11 @@ public struct VaultItemOnTapDecoratorViewGenerator< behaviour: VaultItemViewBehaviour, ) -> some View { Button { - Task { try await onTap(metadata.id) } + Task { + // Fail closed. A thrown error means the tap action did not complete — most often a + // failed authentication gating a copy — so the tap must do nothing at all. + try? await onTap(metadata.id) + } } label: { generator.makeVaultPreviewView(item: item, metadata: metadata, behaviour: behaviour) } diff --git a/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift b/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift index faed2a318..74edccc3e 100644 --- a/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift +++ b/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift @@ -20,11 +20,3 @@ public protocol VaultItemPreviewSceneResponder { func scenePhaseDidChange(to scene: ScenePhase) func didAppear() } - -// MARK: - Mock - -extension VaultItemPreviewViewGeneratorMock: VaultItemCopyActionHandler { - public func textToCopyForVaultItem(id _: Identifier) -> VaultTextCopyAction? { - nil - } -} diff --git a/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift b/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift index 62a50b853..cc6631e8a 100644 --- a/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift +++ b/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift @@ -166,7 +166,8 @@ struct PendingValueTests { let waitForTaskStart = Pending.signal() Task.detached(priority: .high) { await waitForTaskStart.fulfill() - _ = try await sut.wait() + // The wait is torn down by `sut.cancel()` at the end of the test, so a throw is expected. + _ = try? await sut.wait() } try await waitForTaskStart.wait() diff --git a/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift b/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift index 9a760a520..54a71c8ce 100644 --- a/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift +++ b/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift @@ -21,10 +21,14 @@ enum IntervalTimerTests { let pendingStart = Pending.signal() let completed = Pending.signal() Task { - try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in - await pendingStart.fulfill() - try await sut.wait(for: 0.1) - await completed.fulfill() + do { + try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in + await pendingStart.fulfill() + try await sut.wait(for: 0.1) + await completed.fulfill() + } + } catch { + Issue.record(error) } } @@ -49,10 +53,14 @@ enum IntervalTimerTests { let pendingStart = Pending.signal() let completed = Pending.signal() Task { - try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in - await pendingStart.fulfill() - try await sut.wait(for: 0.1, tolerance: 0.1) - await completed.fulfill() + do { + try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in + await pendingStart.fulfill() + try await sut.wait(for: 0.1, tolerance: 0.1) + await completed.fulfill() + } + } catch { + Issue.record(error) } }