From 798abf3b90e9945d6056ef3dcb215743d80c695a Mon Sep 17 00:00:00 2001 From: rzen Date: Wed, 15 Jul 2026 16:07:32 -0400 Subject: [PATCH] Watch: show version/build on screen, surface recording errors in UI A tester on real hardware has no console, so a failed start looked like a dead button and there was no way to tell which build the watch had actually installed. Show the stamped version/build under the record button, display the last start error in place of the hint text, and fall back to the default audio session mode if .spokenAudio is rejected with the .record category. Claude-Session: https://claude.ai/code/session_01GKfAiKiQKLDTmbiGva4FGE --- .../Services/WatchAudioRecorder.swift | 8 +++++++- Notes Watch App/Views/RecordView.swift | 17 +++++++++++++++++ Notes Watch App/WatchAppServices.swift | 7 +++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Notes Watch App/Services/WatchAudioRecorder.swift b/Notes Watch App/Services/WatchAudioRecorder.swift index 3d13a21..b5057aa 100644 --- a/Notes Watch App/Services/WatchAudioRecorder.swift +++ b/Notes Watch App/Services/WatchAudioRecorder.swift @@ -71,7 +71,13 @@ final class WatchAudioRecorder { guard !isRecording else { return } let session = AVAudioSession.sharedInstance() - try session.setCategory(.record, mode: .spokenAudio) + do { + try session.setCategory(.record, mode: .spokenAudio) + } catch { + // .spokenAudio isn't guaranteed compatible with .record on every + // route — fall back rather than refuse to record at all. + try session.setCategory(.record, mode: .default) + } try session.setActive(true) let dir = Self.recordingsDirectory diff --git a/Notes Watch App/Views/RecordView.swift b/Notes Watch App/Views/RecordView.swift index aeeb7ad..0473ad8 100644 --- a/Notes Watch App/Views/RecordView.swift +++ b/Notes Watch App/Views/RecordView.swift @@ -17,6 +17,12 @@ struct RecordView: View { Text(Self.elapsedString(recorder.elapsed)) .font(.system(.title3, design: .rounded).monospacedDigit()) .foregroundStyle(.secondary) + } else if let error = services.lastError { + Text(error) + .font(.caption2) + .foregroundStyle(.red) + .multilineTextAlignment(.center) + .lineLimit(3) } else { Text("Tap to record") .font(.footnote) @@ -24,6 +30,7 @@ struct RecordView: View { } pendingFooter } + buildFooter } .padding() .containerBackground(.red.gradient, for: .navigation) @@ -76,6 +83,16 @@ struct RecordView: View { } } + /// Version + build stamped by update_build_number.sh, so a tester can tell + /// at a glance which build the watch actually installed. + private var buildFooter: some View { + let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "?" + let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "?" + return Text("v\(version) (\(build))") + .font(.system(size: 10)) + .foregroundStyle(.tertiary) + } + /// mm:ss for the elapsed readout. static func elapsedString(_ interval: TimeInterval) -> String { let total = Int(interval) diff --git a/Notes Watch App/WatchAppServices.swift b/Notes Watch App/WatchAppServices.swift index 654e624..9274741 100644 --- a/Notes Watch App/WatchAppServices.swift +++ b/Notes Watch App/WatchAppServices.swift @@ -34,6 +34,11 @@ final class WatchAppServices { private let logger = Logger(subsystem: "dev.rzen.indie.Notes.watchkitapp", category: "recording") + /// Why the last start attempt failed, surfaced in the UI — on a real watch + /// there's no console, so a silent failure is indistinguishable from a dead + /// button. Cleared when a recording starts successfully. + private(set) var lastError: String? + /// Request permission (once) then begin recording. A no-op if already /// recording or permission is refused. func startRecording() async { @@ -44,8 +49,10 @@ final class WatchAppServices { } do { try recorder.startRecording() + lastError = nil } catch { logger.error("could not start recording: \(error, privacy: .public)") + lastError = error.localizedDescription } }