From 7169aad6a33bf5c7a570088aca597d9363ef58ed Mon Sep 17 00:00:00 2001 From: Vigneswaran Rajkumar <118706051+IAmVigneswaran@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:27:22 +0800 Subject: [PATCH] Fix build warnings that appeared in Xcode 27. Handle discarded throwing Tasks without changing launch or Queue behaviour. GitHub CI stays on Xcode 26.6. --- .cursorrules | 1 + AGENT.md | 1 + ARCHITECTURE.md | 4 +++- CHANGELOG.md | 10 ++++++++++ GUARDRAILS.md | 3 ++- .../Marker Data/Models/Database/DatabaseManager.swift | 10 +++++++--- .../Marker Data/Models/Queue/QueueModel.swift | 8 ++++---- .../Models/Roles/RolesManager+DropDelegate.swift | 8 ++++++-- .../Models/Settings/SettingsContainer.swift | 7 ++++++- .../Utilities/Extensions/TaskExtension.swift | 11 +++++++++-- .../Marker Data/Views/Detail Views/QueueView.swift | 2 +- 11 files changed, 50 insertions(+), 15 deletions(-) diff --git a/.cursorrules b/.cursorrules index 9adee0e8..b8b0b463 100644 --- a/.cursorrules +++ b/.cursorrules @@ -67,6 +67,7 @@ Keep these four files consistent when architecture or agent guidance changes. - Do not delete a second `DialogIcon.swift in Sources` (or other shared files) from `project.pbxproj` — that is two-target membership, not a duplicate file. - Do not force-unwrap `UTType("com.apple.finalcutpro.xml")` / `.xmld` — resolve with `UTType(identifier) ?? UTType(importedAs:identifier, conformingTo:)` and keep `UTImportedTypeDeclarations` in `Marker-Data-Info.plist`. First Extract layout evaluates these types; FCP may be absent (App Preview). Do not split the Share Destination **Asset Description File** document type. - File menu custom items replace `.newItem` so system Close stays last — do not empty `.newItem` and do not add a second Close. +- Do not discard `Task { try await … }` — Xcode 27 warns (`#NoUseUnstructuredThrowingTask`) because those errors are swallowed. Catch inside, drop a spurious `throws`/`try`, or call `@MainActor` methods directly (not `_ = Task`). ## Definition of done for code changes - App builds **unsigned** Debug and Release for arm64 (`CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`; Workflow Extension still embeds). diff --git a/AGENT.md b/AGENT.md index 73181f74..b30994b6 100644 --- a/AGENT.md +++ b/AGENT.md @@ -321,6 +321,7 @@ Exact overlay strings and always/never rules: **`GUARDRAILS.md`**. - **File menu Close last:** custom File items replace `.newItem` (`FileCommands`). Do not empty `.newItem` (that puts system Close first) and do not add a second Close. - **Pagemaker `confirm` is unreachable in-app:** the only `confirm(` in bundled `Resources/Pagemaker.html` sits behind `isMacOSSafari()`, and WKWebView’s default user agent has no `Safari` token. Keep the delegate correct, but you cannot exercise it from the UI — and `update_pagemaker.yml` can change that HTML upstream. - **Tolerant `Color ==`:** `ColorExtension.swift` overrides `Color ==` with a 0.1-per-channel tolerance that shadows SwiftUI’s and drives `SettingsStore` equality, so small colour edits do not mark a configuration dirty. It is not transitive — never put `Color` or `SettingsStore` in a `Set`, key a dictionary on them, or dedupe an array. Full reasoning in **GUARDRAILS.md → Signs**. +- **Unused throwing unstructured `Task`:** Xcode 27 (`#NoUseUnstructuredThrowingTask`) warns when `Task { try await … }` is discarded — those errors were already being swallowed. Catch inside, drop a spurious `throws`/`try`, or call `@MainActor` methods directly. Do not silence with `_ = Task`. `DatabaseManager.loadProfilesFromDisk` keeps a **non-throwing** deferred `Task { await MainActor.run }` so profiles still appear after `init`. ## Definition of done for most changes - App builds **unsigned** Debug **and** Release for **arm64** (`CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`; Workflow Extension still embeds). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f24edb7c..abdded46 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -483,6 +483,7 @@ Settings model: `ColorSwatchSettingsModel` (nested under SettingsStore, Codable) - Validation: unique profile names; must not collide with extract-only format display names - Property spelling **`plaform`** is intentional in current code — preserve when editing - `duplicateProfile(profileName:)` appends `" copy"` and runs `addProfile(saveToDisk: true)` synchronously on the main actor, so `DatabaseValidationError.nameAlreadyExists` reaches `DatabaseSettingsView`’s “Failed to duplicate profile” alert. Duplicating the same profile twice is refused by design (matching `ConfigurationsViewModel.duplicateConfiguration`, which also uses a flat `" copy"`); neither panel auto-increments. +- `loadProfilesFromDisk()` still defers `addProfile(..., saveToDisk: false)` with a non-throwing `Task { await MainActor.run }` so profiles can appear after `init` (same launch timing as before). Do not put `try` on that task. | Model | Notable fields | |-------|----------------| @@ -855,4 +856,5 @@ Source/Marker Data/Marker Data Uninstaller/ 15. Workflow Extension decodes `SettingsStore` without running migrations — main app must migrate on launch first. 16. FCPXML UTTypes must resolve without Final Cut Pro installed: public `UTType.fcpxml` / `.fcpxmld` (private `finalCutProType`: `UTType(identifier)` then `importedAs:conformingTo:`) plus `UTImportedTypeDeclarations` in `Marker-Data-Info.plist`. Never `UTType("com.apple.finalcutpro.xml")!`. Keep the Share Destination **Asset Description File** document type (do not rename/split it). 17. File menu custom items replace `.newItem` (`FileCommands`). Do not empty `.newItem` in `Marker_DataApp` (puts system Close first) and do not add a second Close. -18. Definition of done: unsigned arm64 Debug+Release build; settings migrate; `.fcpxml`/`.fcpxmld` + pasteboard intake; FCPXML UTTypes use `importedAs` fallback; File menu Close last; WE Extract + Roles overlays; queue finds/uploads via `manifestURL`; agent docs (`AGENT.md` / `ARCHITECTURE.md` / `GUARDRAILS.md` / `.cursorrules`) stay aligned. +18. Do not discard throwing unstructured `Task { try await … }` (Xcode 27 `#NoUseUnstructuredThrowingTask`). Catch inside, drop a spurious `throws`/`try`, or call `@MainActor` methods directly. `loadProfilesFromDisk` may keep a **non-throwing** deferred `Task { await MainActor.run }` so launch timing stays unchanged. +19. Definition of done: unsigned arm64 Debug+Release build; settings migrate; `.fcpxml`/`.fcpxmld` + pasteboard intake; FCPXML UTTypes use `importedAs` fallback; File menu Close last; WE Extract + Roles overlays; queue finds/uploads via `manifestURL`; agent docs (`AGENT.md` / `ARCHITECTURE.md` / `GUARDRAILS.md` / `.cursorrules`) stay aligned. diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b8675d..ff83a280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +### 2.1.4 (28) + +**🎉 Released:** +- 15th September 2026 + +**🐞 Bug Fix:** +- Fixed build warnings that appeared in Xcode 27 + +--- + ### 2.1.3 (27) **🎉 Released:** diff --git a/GUARDRAILS.md b/GUARDRAILS.md index af6db90e..ea4e0a0e 100644 --- a/GUARDRAILS.md +++ b/GUARDRAILS.md @@ -90,12 +90,13 @@ Shared component: `Views/Components/DropTargetOverlay.swift` (main app + Workflo 15. **Workflow Extension does not migrate settings.** `RolesManager` JSON-decodes `SettingsStore` with no `SettingsVersioningManager`. Open the main app after a schema bump. 16. **FCPXML `UTType` force-unwrap** — `UTType("com.apple.finalcutpro.xml")!` traps on Extract first layout when Final Cut Pro is absent (App Preview / review Macs). Use `importedAs` fallback and Info.plist imported types. Do not copy the old Marker Data force-unwrap. 17. **File menu Close last:** custom File items replace `.newItem`. Emptying `.newItem` makes system Close the first File item; do not duplicate Close. -18. **`Task { try await MainActor.run { … } }` inside a `throws` method swallows the error.** The task result is discarded, so the method always returns normally and the caller’s `catch` is dead. `DatabaseManager` is already `@MainActor` — call directly and let the error propagate. `duplicateProfile` was fixed this way; `setActiveProfile` and `loadProfilesFromDisk` still use the wrapper but throw nothing, so they are harmless. Do not reintroduce the pattern for anything that validates. +18. **`Task { try await MainActor.run { … } }` inside a `throws` method swallows the error.** The task result is discarded, so the method always returns normally and the caller’s `catch` is dead. `DatabaseManager` is already `@MainActor` — call directly and let the error propagate. `duplicateProfile` was fixed this way; `setActiveProfile` and `loadProfilesFromDisk` still use a non-throwing `Task { await MainActor.run }` wrapper (no `try`) so launch timing stays deferred and Xcode 27 does not warn. Do not reintroduce `try` on those wrappers, and do not use the pattern for anything that validates. 19. **Record an extract failure once.** `extractAndUpdateProgress` must rethrow the `MarkersExtractor` error, not append to `failedTasks` — appending there and then tripping the `exportResultisNil` guard produced two entries for one file, the second reading “Failed to get export result”, and duplicate `ExtractionFailure.id` (the URL) in `FailedExtractionsView`. The task group `catch` is the single recording point. 20. **`NSAlert` does not pick up the Icon Composer icon**, and its `alertStyle` defaults to `.warning`. Pagemaker’s WebView JavaScript panels are the app’s only `NSAlert`s — route them through `PagemakerUIDelegate.makeAlert(message:)` (sets `MarkerDataAppIcon.alertImage`, `.informational`, message as `messageText`). Never re-add generic “Alert” / “Confirm” / “Prompt” titles; SwiftUI dialogs elsewhere have no such title. Keep `confirm` returning `.alertFirstButtonReturn` so Cancel reaches JavaScript as `false`. 21. **`Color ==` is tolerant, app-wide, and not transitive.** `ColorExtension.swift` defines `static func == (Color, Color)` delegating to `isEqual(to:tolerance: 0.1)`; Swift prefers the same-module declaration, so this shadows SwiftUI's exact `==` for **every** `Color` comparison compiled into the main app — including synthesized `Equatable` on `SettingsStore` (verified). Consequences: a colour tweak under ~25/255 in all three channels leaves `SettingsContainer.checkForUnsavedChanges()` false, so the Configurations “Changed” badge and ⌘S / ⌘Z stay disabled even though `preferences.json` was already updated; and `0.0 == 0.1`, `0.1 == 0.2`, `0.0 != 0.2` breaks `Equatable` transitivity, so never put `Color` (or `SettingsStore`) in a `Set`, use it as a dictionary key, or dedupe an array of them. The tolerance exists because `Color.hex` **truncates** (`Int(components.red * 255.0)`): 179 of 256 sampled colours drift one step through encode/decode, which under exact equality would mark the store permanently dirty. Fix properly by rounding in `hex` and comparing `lhs.hex == rhs.hex` — measured zero round-trip failures — not by lowering the tolerance alone. Note `hex` also feeds `imageLabelFontColor` / `imageLabelFontStrokeColor`, so the drift reaches rendered labels. 22. **Pagemaker’s `confirm` panel is unreachable in-app — do not “verify” it by clicking.** The only `confirm(` in bundled `Resources/Pagemaker.html` is gated behind `isMacOSSafari()`, and WKWebView’s default user agent carries no `Safari` token, so the branch never runs inside Marker Data. `webView(_:runJavaScriptConfirmPanelWithMessage:…)` must still return `.alertFirstButtonReturn` correctly (Sign 20), but that contract cannot be exercised from the UI — reason about it, don’t test-and-conclude. Two things would make it live: setting `applicationNameForUserAgent` to include `Safari`, or an upstream Pagemaker change, since `update_pagemaker.yml` refreshes that HTML outside this repo’s review. 23. **`.appDialogIcon()` propagates through the environment.** It wraps SwiftUI `.dialogIcon(_:)`, so one trailing call covers every `.alert` / `.confirmationDialog` earlier in the same modifier chain. `ConfigurationSettingsView` has four dialogs and **one** call — that is correct, not a missing icon; `DatabaseSettingsView`’s three calls are redundant but harmless. Only a dialog-bearing chain with **no** call anywhere is broken. Keep adding one per dialog in new code (safe habit), but do not “fix” an existing single trailing call. +24. **Unused throwing unstructured `Task` (Xcode 27 / `#NoUseUnstructuredThrowingTask`).** `Task { try await … }` without storing the task or catching inside silently drops errors. Handle with `do/catch` (or drop a spurious `throws` / `try`) so the task is non-throwing; on `@MainActor` types, call directly. Do not silence with `_ = Task`. --- diff --git a/Source/Marker Data/Marker Data/Models/Database/DatabaseManager.swift b/Source/Marker Data/Marker Data/Models/Database/DatabaseManager.swift index fc5ef7fe..e59c98da 100644 --- a/Source/Marker Data/Marker Data/Models/Database/DatabaseManager.swift +++ b/Source/Marker Data/Marker Data/Models/Database/DatabaseManager.swift @@ -130,10 +130,14 @@ class DatabaseManager: ObservableObject { do { let data = try Data(contentsOf: url) let decoded: DatabaseProfileModel = try decoder.decode(T.self, from: data) - + + // Same deferred MainActor hop as before: decode here, append + // after init returns. Non-throwing Task so Xcode 27 does not + // warn (#NoUseUnstructuredThrowingTask). saveToDisk: false + // does not throw. Task { - try await MainActor.run { - try self.addProfile(decoded, saveToDisk: false) + await MainActor.run { + try? self.addProfile(decoded, saveToDisk: false) } } } catch { diff --git a/Source/Marker Data/Marker Data/Models/Queue/QueueModel.swift b/Source/Marker Data/Marker Data/Models/Queue/QueueModel.swift index da64f519..06e06316 100644 --- a/Source/Marker Data/Marker Data/Models/Queue/QueueModel.swift +++ b/Source/Marker Data/Marker Data/Models/Queue/QueueModel.swift @@ -30,7 +30,7 @@ class QueueModel: ObservableObject { self.databaseManager = databaseManager } - func scanFolder(at url: URL, append: Bool = false) async throws { + func scanFolder(at url: URL, append: Bool = false) async { // Skip if upload is in progress if self.uploadInProgress { return @@ -84,10 +84,10 @@ class QueueModel: ObservableObject { throw QueueError.missingOutputDirectory } - try await self.scanFolder(at: exportFolder) + await self.scanFolder(at: exportFolder) } - func upload() async throws { + func upload() async { defer { self.uploadInProgress = false self.taskGroup = nil @@ -148,7 +148,7 @@ class QueueModel: ObservableObject { // Check file type if url.hasDirectoryPath { Task { [weak self] in - try await self?.scanFolder(at: url, append: true) + await self?.scanFolder(at: url, append: true) } } else { Self.logger.notice("Skipping file \(url.path(percentEncoded: false)). Not supported.") diff --git a/Source/Marker Data/Marker Data/Models/Roles/RolesManager+DropDelegate.swift b/Source/Marker Data/Marker Data/Models/Roles/RolesManager+DropDelegate.swift index 9596acbd..f34186bb 100644 --- a/Source/Marker Data/Marker Data/Models/Roles/RolesManager+DropDelegate.swift +++ b/Source/Marker Data/Marker Data/Models/Roles/RolesManager+DropDelegate.swift @@ -73,8 +73,12 @@ extension RolesManager: DropDelegate { return } - if let extractedRoles = await self.getRoles(fcpxml: try FCPXMLFile(at: urlUnwrapped)) { - await self.setRoles(extractedRoles) + do { + if let extractedRoles = await self.getRoles(fcpxml: try FCPXMLFile(at: urlUnwrapped)) { + await self.setRoles(extractedRoles) + } + } catch { + Self.logger.error("Failed to load FCPXMLD: \(error.localizedDescription, privacy: .public)") } } } diff --git a/Source/Marker Data/Marker Data/Models/Settings/SettingsContainer.swift b/Source/Marker Data/Marker Data/Models/Settings/SettingsContainer.swift index 03953fed..c70d46ba 100644 --- a/Source/Marker Data/Marker Data/Models/Settings/SettingsContainer.swift +++ b/Source/Marker Data/Marker Data/Models/Settings/SettingsContainer.swift @@ -63,7 +63,12 @@ class SettingsContainer: ObservableObject { .dropFirst() // Ignore initial value .sink { _ in Task(priority: .background) { @MainActor in - try await self.store.saveAsCurrent() + do { + try await self.store.saveAsCurrent() + } catch { + Self.logger.error("Failed to save store: \(error.localizedDescription, privacy: .public)") + } + await self.checkForUnsavedChanges() } } diff --git a/Source/Marker Data/Marker Data/Utilities/Extensions/TaskExtension.swift b/Source/Marker Data/Marker Data/Utilities/Extensions/TaskExtension.swift index 677cf414..fd927cd5 100644 --- a/Source/Marker Data/Marker Data/Utilities/Extensions/TaskExtension.swift +++ b/Source/Marker Data/Marker Data/Utilities/Extensions/TaskExtension.swift @@ -14,9 +14,16 @@ extension Task where Failure == Error { static func synchronous(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) { let semaphore = DispatchSemaphore(value: 0) - Task(priority: priority) { + // Explicit non-throwing Task so Xcode 27 does not warn about a discarded + // throwing unstructured task (#NoUseUnstructuredThrowingTask). Errors + // cannot be rethrown across the semaphore wait; the only caller is + // settings migration, which does not throw. + Task(priority: priority) { defer { semaphore.signal() } - return try await operation() + + do { + _ = try await operation() + } catch {} } semaphore.wait() diff --git a/Source/Marker Data/Marker Data/Views/Detail Views/QueueView.swift b/Source/Marker Data/Marker Data/Views/Detail Views/QueueView.swift index b9670876..ad3035da 100644 --- a/Source/Marker Data/Marker Data/Views/Detail Views/QueueView.swift +++ b/Source/Marker Data/Marker Data/Views/Detail Views/QueueView.swift @@ -106,7 +106,7 @@ struct QueueView: View { // Start upload button Button { Task { - try await queueModel.upload() + await queueModel.upload() } } label: { Label("Start Upload", systemImage: "square.and.arrow.up")