Architecture
Skingomz separates pure business logic (testable Swift Packages, without UIKit/SwiftUI) from a thin SwiftUI UI layer.
Skingomz (SwiftUI app, iOS/iPadOS/macOS)
│ depends on
▼
Packages/Core
├─ PodcastModel value types: Podcast, Episode, PlayState
├─ FeedKit RSS/Atom/iTunes/Podcasting 2.0 parsing + FeedFetcher
├─ StorageKit SQLite via GRDB (subscriptions, queue, downloads, playback)
├─ PlaybackKit audio engines (AVPlayer / AVAudioEngine) + PlaybackController
├─ DiscoveryKit iTunes + fyyd + Radio Browser search, OPML parser
└─ SyncKit Nextcloud + gpodder.net providers
Principles
- Clean-room: the model and parsers are written from public specs
(RSS 2.0, Atom,
itunes:/podcast:namespaces, iTunes Search, gpodder protocol, OPML). No copylefted third-party code is reused. - Logic testable outside the IDE: each package is tested with
swift test(the network is abstracted behind protocols → stubs in the tests). - Single native target:
SDKROOT = auto, iOS + macOS from the same target (no Mac Catalyst). A few iOS-only modifiers are neutralised viaPlatformModifiers.swift.
Data flow
- Subscription:
FeedFetcherdownloads the feed →FeedParser→ParsedFeed→StorageKit.LibraryStore.save(...)(upsert + deduplication byfeedURL/guid). - Playback:
AppModel.playresolves the URL (local file if downloaded), the resume position and the settings (volume, effects, private-feed credentials) →PlaybackControllerdrivesSwitchingAudioEngine, which routes toAVPlayer(streaming, radio) orAVAudioEngine(local files with equaliser / silence skipping).NowPlayingCoordinatorreflects state toMPNowPlayingInfoCenterand receives commands fromMPRemoteCommandCenter. - Playback persistence: the position is saved periodically, on pause,
when moving to the background and at the end of a track (
playstate), enabling resume and synchronisation. Radio stations (live streams) have no position. - Sync:
SyncKitexchanges subscriptions and episode actions over HTTP Basic;AppModel.syncNowapplies the remote state then pushes the local one.
UI layer (app)
RootView—NavigationSplitView(sidebar + detail), collapsing to a stack on iPhoneAppModel(@Observable,@MainActor) — global state and orchestrationPlaybackController,DownloadController,SyncSettings— dedicated states, injected via the SwiftUI environment
Persistence (SQLite / GRDB)
Main tables: podcast (including the isRadio marker), episode (+ a unique index
podcastId,guid), and separate tables that survive feed refreshes:
queue, download, playstate, podcast_pref. Versioned migrations via
DatabaseMigrator (v1…v10). Secrets (the synchronisation password,
private-feed credentials) are never in the database: they live in the Keychain.