iOS: feature and style parity with the Android app#591
Conversation
Adds SettingsState fields + persistence for Cloudlog/QRZ-logbook/PSK-Reporter, decode highlight toggles, continent filter, distance unit, and tune timeout, ahead of the feature wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Cloudlog/Wavelog and QRZ.com logbook upload (pure request builders in FT8Engine + OnlineLogService, upload-on-log, catch-up sync, sync chips, LoggingSettings screen, test-connection actions) - PSK Reporter reception reports (IPFIX datagram port of the Android sender, 5-min per-call/band dedup, UDP flush) - QsoRecord syncedCloudlog/syncedQrz flags with legacy-JSON back-compat - Spectrum width wired end-to-end (parametric WaterfallAxis/RowBuilder, live application, ruler math mirroring 647b12e) - UTC period timestamps drawn on the waterfall (WaterfallTimestampGate port) - RX input level meter (AudioInputLevel port, info-bar meter) - App icon (1024 opaque from play_store_icon.png) Kit tests 209/209; simulator build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Auto-sequence toggles wired (autoCQAfterQSO->autoReturnToCq, real HUNT auto-answer, huntCallsCQ, autoCallFollow); earlyDecode documented inert - TX watchdog + stop-after-N via pure TxSupervisor - Late-start clip rule max(0, msIntoCycle - 2360) via TxTiming (with anti-modulo regression tests); txDelayMs honored, pttDelayMs inert on iOS - TUNE latching carrier with countdown + anti-click ramps (TuneTone) - Caller queue (CallerQueue policy + QUEUE row in ActiveQsoPanel) - ActiveQsoPanel header states, LOG/clear actions, TX stage selector chips - DecodeRow rebuilt to Android layout (accent bar, status pills with resolveQsoStatus priority, SNR bar, distance/age/DXCC line) - DXCC/continent prefix tables + DecodeAnnotator + display formatters - showOnlyCQ / dxOnly / continent filters wired; DecodeFilterSettings real Kit tests 298/298; simulator build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POTA Hunt tab fetches live activator spots from api.pota.app (pull-to- refresh, 60s auto-refresh, FT8 filter, tap-to-hunt band switch) - Activation QSO count derived from logbook records in the activation window (PotaQsoWindow port); history persisted to Documents JSON - Per-park activation ADIF export (PotaAdifExporter field set, MY_SIG=POTA) - Inter + Geist Mono bundled from the Android font set and adopted across all views (UI=Inter, data=Geist Mono, slashed zero); Info.plist migrated to an XcodeGen info block carrying UIAppFonts + existing keys Kit tests 329/329; simulator build green; smoke-run verified in simulator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code-review fixes (all 10 verified findings): - Supervisor stops (watchdog / stop-after-N) now disarm hunt so hunt-calls-CQ cannot silently restart a stopped TX run - Late-start clip slack locked to the physical 2360 ms constant; the late-start tolerance setting is now the skip threshold it describes - stop-after-N counts only transmissions actually handed to the player - forceLog no longer double-logs during the courtesy-73 window - Restored NSLocalNetworkUsageDescription lost in the Info.plist migration - Waterfall overlays/tap-to-tune map against the drawn data span (waterfall.displayMaxHz), not the live setting - PSK Reporter: no sends after opt-out, flush-on-stop, no stranded queue, datagrams capped at 1400 bytes including padding - Spectrum width default 3500 Hz (matches the previous fixed span) Audio device selection (parity with Android's input/output pickers): - Input Device picker backed by AVAudioSession availableInputs, preference persisted by port name and re-applied on replug - Output Device row with the system route picker (AVRoutePickerView) TX/RX audio-engine resilience (found live-testing in the simulator): - TxPlayerService restarts a stopped shared engine before keying (silent-TX) - AudioCaptureService handles AVAudioEngineConfigurationChange by rebuilding the mic tap at the new hardware format and restarting (silent-RX) Kit tests 350/350; simulator build green; CQ TX audio and live FT8 decode verified end-to-end in the simulator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #591 +/- ##
============================================
+ Coverage 29.36% 35.76% +6.39%
Complexity 197 197
============================================
Files 179 216 +37
Lines 24068 26486 +2418
Branches 3263 3263
============================================
+ Hits 7067 9472 +2405
- Misses 16780 16793 +13
Partials 221 221
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR substantially expands the iOS app to match the Android app’s on-air behavior and UI conventions by adding new pure-engine utilities (TX timing/safety, tuning tone, decode annotation, online logging helpers, POTA export/windowing), plus corresponding SwiftUI screen updates (waterfall mapping/timestamps, settings, typography, logging), backed by a large suite of new unit tests.
Changes:
- Adds new FT8Engine/FT8Audio “pure logic” modules (TX timing + supervision, tuning tone, caller queue, decode highlighting, POTA spots/export, online-log sync/clients) with extensive tests.
- Updates the iOS UI to adopt the Android font set and implements new/updated settings and waterfall overlays/mapping to match Android behavior.
- Adds iOS Info.plist generation changes (XcodeGen) and app assets (app icon), plus audio route input/output selection and resilience fixes.
Reviewed changes
Copilot reviewed 88 out of 94 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ios/FT8AFKit/Tests/FT8EngineTests/TxTimingTests.swift | Unit tests for TX late-start clip/skip math. |
| ios/FT8AFKit/Tests/FT8EngineTests/TxSupervisorTests.swift | Unit tests for TX watchdog + stop-after-N logic. |
| ios/FT8AFKit/Tests/FT8EngineTests/TxStageSelectorTests.swift | Unit tests for TX-stage chip rules + header state rules. |
| ios/FT8AFKit/Tests/FT8EngineTests/TuneToneTests.swift | Unit tests for TUNE carrier generation + ramping + labels. |
| ios/FT8AFKit/Tests/FT8EngineTests/QsoEngineTests.swift | Adds coverage for force-log / abandon-to-CQ behaviors. |
| ios/FT8AFKit/Tests/FT8EngineTests/QrzLogbookClientTests.swift | Tests for QRZ logbook form encoding + response parsing. |
| ios/FT8AFKit/Tests/FT8EngineTests/PotaAdifTests.swift | Tests for per-park POTA ADIF export + scoping/filenames. |
| ios/FT8AFKit/Tests/FT8EngineTests/PotaActivationsTests.swift | Tests for POTA activation windowing + persistence model rules. |
| ios/FT8AFKit/Tests/FT8EngineTests/OnlineLogSyncTests.swift | Tests for sync-flag persistence + unsynced selection + ADIF. |
| ios/FT8AFKit/Tests/FT8EngineTests/DxccPrefixTests.swift | Tests for DXCC prefix/entity/continent resolution mechanics. |
| ios/FT8AFKit/Tests/FT8EngineTests/DisplayFormattersTests.swift | Tests for age/distance formatting utilities. |
| ios/FT8AFKit/Tests/FT8EngineTests/CloudlogClientTests.swift | Tests for Cloudlog/Wavelog request building + auth parsing. |
| ios/FT8AFKit/Tests/FT8EngineTests/CallerQueueTests.swift | Tests for caller queue dedup/cap/staleness/dequeue policies. |
| ios/FT8AFKit/Tests/FT8AudioTests/WaterfallTimestampGateTests.swift | Tests for once-per-slot waterfall timestamp gating/labels. |
| ios/FT8AFKit/Tests/FT8AudioTests/AudioPortLabelTests.swift | Tests for audio port display label formatting. |
| ios/FT8AFKit/Tests/FT8AudioTests/AudioInputLevelTests.swift | Tests for RX input level metering thresholds and sanitization. |
| ios/FT8AFKit/Sources/FT8Engine/TxTiming.swift | Pure TX clip/skip math and clip→sample conversion. |
| ios/FT8AFKit/Sources/FT8Engine/TxSupervisor.swift | Pure TX safety supervisor (watchdog + stop-after-N). |
| ios/FT8AFKit/Sources/FT8Engine/TxStageSelector.swift | Pure TX chip ordering/labels and QSO panel header state logic. |
| ios/FT8AFKit/Sources/FT8Engine/TuneTone.swift | Pure tuning-carrier sample generator with ramps + chip label. |
| ios/FT8AFKit/Sources/FT8Engine/QsoRecord.swift | Adds online-sync flags with backwards-compatible decoding. |
| ios/FT8AFKit/Sources/FT8Engine/QsoEngine.swift | Adds force-log + abandon-to-CQ and double-log guard logic. |
| ios/FT8AFKit/Sources/FT8Engine/QrzLogbookClient.swift | Pure QRZ logbook form builders + response parsing helpers. |
| ios/FT8AFKit/Sources/FT8Engine/PotaSpots.swift | POTA spots model + tolerant JSON decode/filter/age/band mapping. |
| ios/FT8AFKit/Sources/FT8Engine/PotaAdif.swift | Per-park POTA ADIF export logic (incl. FT4/FT2 submode handling). |
| ios/FT8AFKit/Sources/FT8Engine/PotaActivations.swift | Activation record model + window scoping utilities. |
| ios/FT8AFKit/Sources/FT8Engine/OnlineLogSync.swift | Unsynced-record selection for catch-up uploads. |
| ios/FT8AFKit/Sources/FT8Engine/OnlineLogAdif.swift | Single-record ADIF builder for online upload endpoints. |
| ios/FT8AFKit/Sources/FT8Engine/DisplayFormatters.swift | Shared age/distance formatting for decode list. |
| ios/FT8AFKit/Sources/FT8Engine/DecodeAnnotator.swift | Logbook-driven decode highlight classification utilities. |
| ios/FT8AFKit/Sources/FT8Engine/CloudlogClient.swift | Cloudlog/Wavelog request builders + deterministic JSON encoding. |
| ios/FT8AFKit/Sources/FT8Engine/CallerQueue.swift | FIFO caller queue with dedup/cap/stale pruning. |
| ios/FT8AFKit/Sources/FT8Audio/WaterfallTimestampGate.swift | Waterfall slot-boundary gating and UTC label formatting. |
| ios/FT8AFKit/Sources/FT8Audio/WaterfallRowBuilder.swift | Makes displayed spectrum width configurable via maxHz parameter. |
| ios/FT8AFKit/Sources/FT8Audio/WaterfallAxis.swift | Single source of truth for frequency↔x mapping (now width-driven). |
| ios/FT8AFKit/Sources/FT8Audio/AudioPortLabel.swift | Platform-neutral audio port label composition. |
| ios/FT8AFKit/Sources/FT8Audio/AudioInputLevel.swift | Platform-neutral RX level metering/classification logic. |
| ios/FT8AF/project.yml | Switches to explicit Info.plist generation and embeds fonts/usage strings. |
| ios/FT8AF/FT8AF/Theme/Typography.swift | Adds Inter/Geist Mono font resolution + slashed-zero support. |
| ios/FT8AF/FT8AF/Screens/Waterfall/WaterfallScreen.swift | Adds RX input meter and switches to new typography helpers. |
| ios/FT8AF/FT8AF/Screens/Waterfall/WaterfallCanvas.swift | Adds per-slot UTC boundary labels and fixes overlay mapping span. |
| ios/FT8AF/FT8AF/Screens/Waterfall/SpectrumStrip.swift | Fixes tap/marker mapping to actual drawn spectrum width. |
| ios/FT8AF/FT8AF/Screens/Waterfall/FrequencyRuler.swift | Uses width-aware ruler ticks and new typography. |
| ios/FT8AF/FT8AF/Screens/SplashScreen.swift | Switches splash typography to new font helpers. |
| ios/FT8AF/FT8AF/Screens/Settings/TransmissionSettings.swift | Adds tune timeout setting and typography alignment. |
| ios/FT8AF/FT8AF/Screens/Settings/SettingsScreen.swift | Adds Online Logging settings entry + typography alignment. |
| ios/FT8AF/FT8AF/Screens/Settings/RadioAudioSettings.swift | Adds input device picker + output route picker + spectrum width option. |
| ios/FT8AF/FT8AF/Screens/Settings/LoggingSettings.swift | New screen for Cloudlog/QRZ/PSK Reporter toggles + test connection. |
| ios/FT8AF/FT8AF/Screens/Settings/DecodeFilterSettings.swift | Adds continent filter + highlight toggles + distance unit + blocked calls link. |
| ios/FT8AF/FT8AF/Screens/Settings/BlockedCallsignsSettings.swift | Typography alignment for blocked calls UI. |
| ios/FT8AF/FT8AF/Screens/Settings/AdvancedSettings.swift | Updates late-start tolerance wording and typography alignment. |
| ios/FT8AF/FT8AF/Screens/Settings/AboutScreen.swift | Typography alignment and formatting updates. |
| ios/FT8AF/FT8AF/Screens/Map/MapScreen.swift | Typography alignment in map UI. |
| ios/FT8AF/FT8AF/Screens/Map/AzimuthalMapCanvas.swift | Typography alignment in map canvas labels. |
| ios/FT8AF/FT8AF/Screens/Logbook/LogbookStats.swift | Typography alignment for stats UI. |
| ios/FT8AF/FT8AF/Screens/Logbook/LogbookCharts.swift | Typography alignment for charts UI. |
| ios/FT8AF/FT8AF/Screens/Logbook/LogbookAwards.swift | Typography alignment for awards UI. |
| ios/FT8AF/FT8AF/Screens/Logbook/ExportLogSheet.swift | Typography alignment for export UI. |
| ios/FT8AF/FT8AF/Screens/Decode/QsoSheet.swift | Typography alignment and QSO progress UI tweaks. |
| ios/FT8AF/FT8AF/Navigation/AppTabView.swift | Keeps Active QSO panel visible during hunt + adds TUNE toggle action wiring. |
| ios/FT8AF/FT8AF/Info.plist | Adds explicit Info.plist with app fonts + privacy usage strings. |
| ios/FT8AF/FT8AF/Engine/TxPlayerService.swift | Restarts shared AVAudioEngine before play to avoid silent TX. |
| ios/FT8AF/FT8AF/Engine/PotaService.swift | Adds network fetch/refresh wrapper for POTA spots feed. |
| ios/FT8AF/FT8AF/Engine/PotaActivationStore.swift | Adds JSON file persistence for POTA activation history. |
| ios/FT8AF/FT8AF/Engine/AudioRouteController.swift | Adds AVAudioSession input enumeration/selection and output label tracking. |
| ios/FT8AF/FT8AF/Engine/AudioCaptureService.swift | Rebuilds tap/format on engine configuration changes to avoid silent RX. |
| ios/FT8AF/FT8AF/Components/TxStrip.swift | Adds TUNE pill and applies shared typography conventions. |
| ios/FT8AF/FT8AF/Components/ToastOverlay.swift | Typography alignment for toasts. |
| ios/FT8AF/FT8AF/Components/QsoPathMap.swift | Typography alignment for map labels. |
| ios/FT8AF/FT8AF/Components/FrequencyPickerSheet.swift | Typography alignment for picker UI. |
| ios/FT8AF/FT8AF/Components/FilterChip.swift | Typography alignment for filter chips. |
| ios/FT8AF/FT8AF/Components/AudioRoutePickerButton.swift | Adds system output-route picker button wrapper. |
| ios/FT8AF/FT8AF/Assets.xcassets/AppIcon.appiconset/Contents.json | Adds filename reference for app icon asset. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @ObservationIgnored private var observer: NSObjectProtocol? | ||
|
|
||
| func start(preferredPortName: String) { | ||
| refresh(preferredPortName: preferredPortName) | ||
| observer = NotificationCenter.default.addObserver( | ||
| forName: AVAudioSession.routeChangeNotification, | ||
| object: nil, queue: .main | ||
| ) { [weak self] _ in | ||
| Task { @MainActor in | ||
| self?.refresh(preferredPortName: preferredPortName) | ||
| } | ||
| } | ||
| } |
| do { | ||
| try session.setPreferredInput(port) | ||
| currentInputUid = port.uid | ||
| return port.portName |
| /// Seconds elapsed since the spot's UTC timestamp, or nil when the timestamp | ||
| /// doesn't parse. Accepts "yyyy-MM-dd'T'HH:mm:ss" with optional fractional | ||
| /// seconds / trailing zone designator (only the first 19 chars are read). | ||
| /// Negative ages (clock skew) clamp to 0. Feed the result to `relativeAge`. | ||
| public static func ageSeconds(spotTimeUtc: String, now: Date) -> Int? { | ||
| let trimmed = String(spotTimeUtc.prefix(19)) | ||
| guard trimmed.count == 19 else { return nil } | ||
| let fmt = DateFormatter() | ||
| fmt.locale = Locale(identifier: "en_US_POSIX") | ||
| fmt.timeZone = TimeZone(identifier: "UTC") | ||
| fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" | ||
| guard let date = fmt.date(from: trimmed) else { return nil } | ||
| return max(0, Int(now.timeIntervalSince(date))) | ||
| } |
Summary
Brings the iOS app to parity with Android in features and style — everything feasible without rig-CAT hardware. 90 files, ~7.9k additions; FT8AFKit tests grew from 129 to 350, all green.
Operating features
QsoEngine.autoReturnToCqTxSupervisor; a supervisor stop also disarms hunt so it cannot silently restart TXmax(0, msIntoCycle − 2360)rule (with anti-% 15000regression tests per the CLAUDE.md TX-pipeline gotcha); the late-start tolerance setting is now a skip threshold;txDelayMshonored (pttDelayMsinert on iOS — no PTT line)Integrations
Style
Audio device selection & resilience
AVAudioEngine) and silent-RX (AVAudioEngineConfigurationChangekilled the mic tap with no restart) — both found live-testingReview
A 27-agent workflow review of the branch produced 10 verified findings (3 TX-safety, a dropped
NSLocalNetworkUsageDescriptionthat would have regressed WSJT-X UDP, PSK lifecycle/MTU bugs, waterfall span bugs, a double-log window); all 10 are fixed with tests in the final commits.Deferred (follow-up work)
Rig CAT transport (no serial path on iOS; BLE bridge is its own project — ALC/SWR/ATU-tune depend on it), FT4/FT2 mode switching, Hound/Field Day modes, GPS grid + time discipline, needed-DX local notifications, light theme, localization catalogs, CarPlay, PSK Reporter map overlay, POTA OAuth self-spot/upload.
Test plan
cd ios/FT8AFKit && swift test— 350/350 green (221 new tests)xcodebuildiPhone 16 Pro simulator — build green🤖 Generated with Claude Code