Skip to content

iOS: feature and style parity with the Android app#591

Open
patrickrb wants to merge 5 commits into
devfrom
feat/ios-parity
Open

iOS: feature and style parity with the Android app#591
patrickrb wants to merge 5 commits into
devfrom
feat/ios-parity

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

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

  • Auto-sequence wired for real: HUNT auto-answers the strongest eligible CQ (sets slot parity from the heard slot), hunt-calls-CQ, auto-call-follow, auto-CQ-after-QSO → QsoEngine.autoReturnToCq
  • TX safety: TX watchdog + stop-after-N via a pure TxSupervisor; a supervisor stop also disarms hunt so it cannot silently restart TX
  • TX timing: late-start clip locked to the physical max(0, msIntoCycle − 2360) rule (with anti-% 15000 regression tests per the CLAUDE.md TX-pipeline gotcha); the late-start tolerance setting is now a skip threshold; txDelayMs honored (pttDelayMs inert on iOS — no PTT line)
  • TUNE latching carrier with countdown and anti-click ramps
  • Caller queue (dedup, cap 10, stale pruning) with a tappable QUEUE row; Active QSO panel gains Android's header states, LOG / clear actions, and the CQ/GRID/RPT/R-RPT/RR73/73 stage-selector chips
  • Decode list rebuilt to the Android layout: accent bar, CALLING/CQ/TO YOU labels, status pills with Android's priority (PENDING > POTA > NEW DXCC > NEW GRID > NEW BAND > WORKED > CQ), 5-segment SNR bar, distance, relative age, DXCC entity line; ~130-entity DXCC/continent prefix tables
  • Decode filters wired: show-only-CQ, DX-only, continent filter, highlight toggles, km/mi, live blocklist count
  • Waterfall: spectrum width setting applied end-to-end (live), UTC timestamps at period boundaries, RX input-level meter; overlays map against the drawn data span

Integrations

  • Cloudlog/Wavelog + QRZ.com logbook upload (upload-on-log with retry, catch-up sync, per-QSO sync flags + chips, test-connection buttons) — new Online Logging settings screen
  • PSK Reporter reception reports (byte-faithful IPFIX port of the Android sender; 5-min dedup, MTU-capped datagrams, flush-on-stop, no sends after opt-out)
  • POTA: live activator spots from api.pota.app (pull-to-refresh, 60 s auto-refresh, FT8 filter, tap-to-hunt), activation QSO count derived from the logbook window, persisted history, per-park ADIF export (MY_SIG=POTA)

Style

  • Inter + Geist Mono bundled from the Android font set and adopted app-wide (UI = Inter, data = Geist Mono, slashed zero) — 270 call sites
  • App icon (was an empty placeholder)
  • Color palette already matched Android hex-for-hex

Audio device selection & resilience

  • Input Device picker (AVAudioSession inputs, persisted by port name, re-applied on replug) and an Output Device row with the system route picker — iOS equivalents of Android's audio pickers
  • Fixed silent-TX (player node keyed against a stopped shared AVAudioEngine) and silent-RX (AVAudioEngineConfigurationChange killed the mic tap with no restart) — both found live-testing

Review

A 27-agent workflow review of the branch produced 10 verified findings (3 TX-safety, a dropped NSLocalNetworkUsageDescription that 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)
  • xcodebuild iPhone 16 Pro simulator — build green
  • Smoke-tested in the simulator: CQ TX audio produced and live FT8 signals decoded end-to-end (acoustic coupling from a phone), fonts/icon render, POTA spots load

🤖 Generated with Claude Code

patrickrb and others added 5 commits July 13, 2026 14:34
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

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.43411% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 35.76%. Comparing base (647b12e) to head (b11a660).

Files with missing lines Patch % Lines
...FT8AFKit/Tests/FT8EngineTests/QsoEngineTests.swift 94.11% 6 Missing ⚠️
ios/FT8AFKit/Sources/FT8Engine/PotaSpots.swift 92.64% 5 Missing ⚠️
ios/FT8AFKit/Sources/FT8Audio/AudioPortLabel.swift 92.00% 2 Missing ⚠️
...os/FT8AFKit/Sources/FT8Engine/CloudlogClient.swift 97.36% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             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              
Flag Coverage Δ
ios 96.42% <99.43%> (+3.48%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...os/FT8AFKit/Sources/FT8Audio/AudioInputLevel.swift 100.00% <100.00%> (ø)
ios/FT8AFKit/Sources/FT8Audio/WaterfallAxis.swift 100.00% <100.00%> (ø)
...T8AFKit/Sources/FT8Audio/WaterfallRowBuilder.swift 100.00% <100.00%> (ø)
...FKit/Sources/FT8Audio/WaterfallTimestampGate.swift 100.00% <100.00%> (ø)
ios/FT8AFKit/Sources/FT8Engine/CallerQueue.swift 100.00% <100.00%> (ø)
...s/FT8AFKit/Sources/FT8Engine/DecodeAnnotator.swift 100.00% <100.00%> (ø)
...FT8AFKit/Sources/FT8Engine/DisplayFormatters.swift 100.00% <100.00%> (ø)
ios/FT8AFKit/Sources/FT8Engine/DxccPrefix.swift 100.00% <100.00%> (ø)
ios/FT8AFKit/Sources/FT8Engine/OnlineLogAdif.swift 100.00% <100.00%> (ø)
ios/FT8AFKit/Sources/FT8Engine/OnlineLogSync.swift 100.00% <100.00%> (ø)
... and 33 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +26 to +38
@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)
}
}
}
Comment on lines +69 to +72
do {
try session.setPreferredInput(port)
currentInputUid = port.uid
return port.portName
Comment on lines +90 to +103
/// 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)))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants