Screens and navigation use SwiftUI. UIKit remains at focused boundaries: CarPlay, the popup library, system presentations (Safari, mail, AirPlay and sharing), and small wrappers that keep the reference appearance and native accessibility: the seek slider, the gradient, the marquee title (MarqueeText), the equalizer and buffering indicators (PlaybackActivityIndicator), and the artwork containers (CrossfadeImage, PlaybackArtworkImage). Playback state stays outside those wrappers.
SwiftRadio/
App/ SwiftRadioApp (@main), AppDelegate, AppEnvironment, RootView,
ConfiguredStationsLoader, ArtworkLoaderKey, UITestRadioPlayer (Debug only)
CarPlay/ CarPlaySceneDelegate and its observation helper
Config/ Config, Content (String Catalog keys), InfoSection
Data/ stations.json
Features/ One folder per screen: Loading, Stations, NowPlaying, StationInfo, About
Helpers/ Small Foundation extensions
UI/ Views shared across screens: artwork, gradient, marquee, equalizer,
AirPlay, Safari, mail, share sheet, share card
Images.xcassets, Localizable.xcstrings, Info.plist, Info-CarPlay.plist
Packages/SwiftRadioCore/
Models/ RadioStation, StationsResponse
Stations/ StationsStore, the loader protocol, bundle and remote loaders,
CatalogListContent (what a size-limited list shows)
Playback/ PlayerService, the FRadioPlayer boundary, audio session, now playing info,
remote commands
Images/ ArtworkLoader
Handoff/ HandoffActivity
AppEnvironment: builds onePlayerService, oneStationsStore, and oneArtworkLoaderat launch. The phone scene and the CarPlay scene share those instances, so a station picked in the car updates the phone, the lock screen, and Control Center.RootView: showsLoadingScreenuntil the first catalog load settles, then aNavigationStackholding the station list, anavigationDestinationfor station info, and the popup player.SwiftRadioCore: a local Swift package that holds the models, the stores, and the only import of FRadioPlayer. It has no SwiftUI in it and carries the unit tests.- CarPlay: the
SwiftRadio-CarPlaytarget compiles the same sources with-D CarPlayand swaps inInfo-CarPlay.plist.CarPlaySceneDelegaterenders aCPListTemplatefrom the shared store:- Catalog states: a loading message while the first load runs, and a Retry row when loading failed or the catalog is empty, so the driver can recover without the phone.
- Item limit: catalogs are paged within
CPListTemplate.maximumItemCount, with a More row and at most four list pages so Now Playing fits the template-depth limit. Limits can change while driving. - Selection: tapping a station pushes the Now Playing template. Tapping the station that is already playing or buffering doesn't restart its stream, and a station whose stream URL can't be opened is ignored.
LNPopupUI draws the bar above the station list and expands it into the full player.
- Presentation:
RootViewattaches.popup(isBarPresented:isPopupOpen:)to theNavigationStack. The bar shows wheneverStationsStore.currentStationis set, including when CarPlay made the selection. - Bar content:
NowPlayingViewfeeds the title, artwork, progress, and play/pause button to the bar through.popupTitle,.popupImage,.popupProgress, and.popupBarButtons. - Full player: the same view's body is the expanded content: blurred artwork backdrop, marquee title, transport controls, and the options sheet.
- Sequencing: options that leave the player (station info, station website) wait for the options sheet's
onDismiss, close the popup, then run fromonClose, so a push or a sheet never fights either dismissal animation.
- Session:
PlayerServiceactivates a non-mixable.playbacksession right before it loads, plays or seeks, never at launch. Opening the app doesn't stop other apps' audio; starting a station does. - Mixing: set
Config.mixesWithOtherAudio = trueto keep other audio playing under the radio. A mixable session can't own the lock screen, Control Center or CarPlay Now Playing, so those controls stop working. - Remote commands: live streams enable Stop and disable Pause, files do the opposite. Play, play/pause, next and previous stay enabled. With no station selected every command reports
.noActionableNowPlayingItemand does nothing. - Now Playing info: each update replaces the whole dictionary, so a live stream never keeps a file's scrubber or an old track's artwork. Clock ticks don't republish it: the system advances elapsed time from the published rate.
See the SwiftRadioCore guide for service contracts and tests.
