How to Build a Podcast Player App in SwiftUI

A Podcast Player app lets users subscribe to RSS feeds, stream episodes via AVPlayer, and resume playback exactly where they left off across sessions. It's a strong App Store product for developers comfortable with async audio pipelines, XML parsing, and recurring subscriptions.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Advanced Estimated time: 2–4 weeks Updated: May 12, 2026

Prerequisites

Architecture overview

Podcast data — feeds and episodes — lives in SwiftData with a cascade-delete relationship. AVPlayer is wrapped in an @Observable view model that manages the audio session lifecycle, lock screen controls via MPNowPlayingInfoCenter, and playback position persistence back into the model context on a timer. Views form a three-screen stack: the library (feed list), an episode list, and a sheet-style mini-player. RSS feeds are fetched with URLSession and parsed on a background actor using XMLParser to keep the main thread free.

PodcastApp/
├── Models/
│   ├── PodcastFeed.swift      (@Model, SwiftData)
│   └── Episode.swift          (@Model, SwiftData)
├── Views/
│   ├── LibraryView.swift
│   ├── EpisodeListView.swift
│   └── PlayerView.swift
├── Services/
│   ├── RSSParser.swift        (background actor)
│   └── PlayerViewModel.swift  (@Observable)
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model

Define PodcastFeed and Episode as SwiftData @Model classes so subscriptions, episode metadata, and per-episode playback positions all survive app restarts.

import SwiftData
import Foundation

@Model final class PodcastFeed {
    var id: UUID = UUID()
    var title: String = ""
    var feedURL: String = ""
    var artworkURL: String = ""

    @Relationship(deleteRule: .cascade)
    var episodes: [Episode] = []
}

@Model final class Episode {
    var id: UUID = UUID()
    var title: String = ""
    var audioURL: String = ""
    var publishDate: Date = Date()
    var duration: TimeInterval = 0
    var playbackPosition: TimeInterval = 0
    var isPlayed: Bool = false
}

2. Core UI — podcast library

Build the library view with a SwiftData-driven List and AsyncImage for artwork so subscribers see their feeds the moment the view appears.

struct PodcastLibraryView: View {
    @Query(sort: \PodcastFeed.title) private var feeds: [PodcastFeed]
    @State private var showAdd = false

    var body: some View {
        NavigationStack {
            List(feeds) { feed in
                NavigationLink(destination: EpisodeListView(feed: feed)) {
                    HStack(spacing: 12) {
                        AsyncImage(url: URL(string: feed.artworkURL)) { img in
                            img.resizable().aspectRatio(contentMode: .fill)
                        } placeholder: { Color.secondary.opacity(0.15) }
                        .frame(width: 54, height: 54)
                        .clipShape(RoundedRectangle(cornerRadius: 8))
                        VStack(alignment: .leading, spacing: 2) {
                            Text(feed.title).font(.headline).lineLimit(1)
                            Text("\(feed.episodes.count) episodes")
                                .font(.caption).foregroundStyle(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Library")
            .toolbar { Button("Add", systemImage: "plus") { showAdd = true } }
            .sheet(isPresented: $showAdd) { AddFeedView() }
        }
    }
}

3. Subscription and playback with AVPlayer

Wrap AVPlayer in an @Observable view model that activates the .playback audio session, resumes from the saved position, and exposes a toggle so the UI stays in sync.

import AVFoundation
import MediaPlayer

@Observable final class PlayerViewModel {
    private var player: AVPlayer?
    private(set) var isPlaying = false
    private(set) var currentTime: TimeInterval = 0
    var currentEpisode: Episode?

    func play(_ episode: Episode) {
        guard let url = URL(string: episode.audioURL) else { return }
        if currentEpisode?.id != episode.id {
            let session = AVAudioSession.sharedInstance()
            try? session.setCategory(.playback, mode: .spokenAudio)
            try? session.setActive(true)
            player = AVPlayer(url: url)
            let seek = CMTime(seconds: episode.playbackPosition, preferredTimescale: 600)
            player?.seek(to: seek)
        }
        currentEpisode = episode
        player?.play()
        isPlaying = true
    }

    func togglePlayPause() {
        isPlaying ? player?.pause() : player?.play()
        isPlaying.toggle()
    }
}

4. Privacy Manifest

Add PrivacyInfo.xcprivacy to your app target — required since the iOS 17 SDK — declaring UserDefaults access and the purchase history your StoreKit subscription collects.

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key><false/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>CA92.1</string></array>
    </dict>
  </array>
  <key>NSPrivacyCollectedDataTypes</key>
  <array>
    <dict>
      <key>NSPrivacyCollectedDataType</key>
      <string>NSPrivacyCollectedDataTypePurchaseHistory</string>
      <key>NSPrivacyCollectedDataTypeLinked</key><true/>
      <key>NSPrivacyCollectedDataTypeTracking</key><false/>
      <key>NSPrivacyCollectedDataTypeUsePurposes</key>
      <array><string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string></array>
    </dict>
  </array>
</dict>
</plist>

Common pitfalls

Adding monetization: Subscription

Implement auto-renewable subscriptions with StoreKit 2. Create a subscription group in App Store Connect, define monthly and annual SKUs, then load them at runtime with Product.products(for: skuIDs). Gate premium features — offline downloads, ad-free playback, unlimited subscriptions — behind a Transaction.currentEntitlement(for:) check. Attach a .subscriptionStatusTask(for:) modifier to your root view so entitlement state refreshes automatically on renewal, cancellation, or billing-grace-period events. Listen to Transaction.updates as a long-lived async stream to handle edge cases, and finish every transaction with transaction.finish() to clear the queue.

Shipping this faster with Soarias

Soarias scaffolds the full Xcode project with the correct entitlements (background audio, StoreKit in-app purchases) already wired, pre-configures the AVAudioSession setup code, auto-generates your PrivacyInfo.xcprivacy based on the features you select, and builds a fastlane lane for App Store screenshots and ASC submission — all before you write a line of feature code.

An advanced podcast player typically takes 2–4 weeks to build and ship solo, with a meaningful chunk of that time lost to entitlements, manifest wiring, and submission plumbing. Soarias compresses that setup to under an hour, giving back roughly 4–6 days that you can spend on RSS parsing quality, playback UX polish, and optimizing your subscription paywall conversion.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. The $99/year Apple Developer Program membership is required to distribute on TestFlight, submit to the App Store, and use StoreKit auto-renewable subscriptions in production. You can build and sideload on a personal device with a free account, but you cannot publish without membership.

How do I submit this to the App Store?

Archive the app in Xcode via Product → Archive, then distribute through the Xcode Organizer or run fastlane deliver. In App Store Connect, fill in the metadata, screenshots, privacy nutrition labels, and your subscription pricing. Submit for review and expect 1–3 business days. If you have an active subscription in-app purchase, attach it to the version before submitting.

Can users download episodes for offline playback?

Yes. Use URLSession.downloadTask(with:) to fetch the audio file and save the resulting local URL into the Episode model. On playback, check episode.localFileURL first and fall back to episode.audioURL for streaming. Track download progress with URLSessionDownloadDelegate and expose it via a published property in your view model.

Last reviewed: 2026-05-12 by the Soarias team.