Skip to main content

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 via PlatformModifiers.swift.

Data flow

  1. Subscription: FeedFetcher downloads the feed → FeedParserParsedFeedStorageKit.LibraryStore.save(...) (upsert + deduplication by feedURL/guid).
  2. Playback: AppModel.play resolves the URL (local file if downloaded), the resume position and the settings (volume, effects, private-feed credentials) → PlaybackController drives SwitchingAudioEngine, which routes to AVPlayer (streaming, radio) or AVAudioEngine (local files with equaliser / silence skipping). NowPlayingCoordinator reflects state to MPNowPlayingInfoCenter and receives commands from MPRemoteCommandCenter.
  3. 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.
  4. Sync: SyncKit exchanges subscriptions and episode actions over HTTP Basic; AppModel.syncNow applies the remote state then pushes the local one.

UI layer (app)

  • RootViewNavigationSplitView (sidebar + detail), collapsing to a stack on iPhone
  • AppModel (@Observable, @MainActor) — global state and orchestration
  • PlaybackController, 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 (v1v10). Secrets (the synchronisation password, private-feed credentials) are never in the database: they live in the Keychain.