Skip to content

Instantly share code, notes, and snippets.

View jubishop's full-sized avatar
🎼
Making a podcast app.

Justin Bishop jubishop

🎼
Making a podcast app.
View GitHub Profile

• - Event 70a4b966 happened on October 18, 2025 at 15:23:19 UTC on one iPhone18,1 running iOS 26.0.1. The crash is an EXC_BREAKPOINT raised inside CF_IS_OBJC on an AVFoundation worker thread while handling an AVTimebaseObserver callback, and it fired immediately after the system posted mediaServicesWereResetNotification (app was still in foreground).

  • During a media-services reset our playback stack runs handleMediaServicesReset() to tear down and rebuild audio state (PodHaven/Play/PlayManager.swift:470). That calls clearOnDeck() → PodAVPlayer.clear() which re-adds the periodic time observer after relaunching the episode (PodHaven/Play/Utility/ PodAVPlayer.swift:226).
  • In that observer we pass DispatchQueue.global(qos: .utility) to AVPlayer.addPeriodicTimeObserver. Apple explicitly documents that supplying a concurrent queue “isn’t supported and results in undefined behavior”; you must provide a serial queue or nil. citeturn6apple-docs-mcp__get_apple_doc_content0 Combined with
@jubishop
jubishop / llm.md
Created October 14, 2025 21:46
what a world.md

• Here’s the key sequence that can leave podcastList stale if we gate executeTrendingSectionFetch on trendingSection == currentTrendingSection:

  1. User taps a new chip. selectTrendingSection(:) updates currentTrendingSection, clears the list, triggers observeCurrentDisplay(), and kicks off performTrendingSectionFetch(: ).
  2. Before the fetch finishes, the user either jumps to a different trending chip or moves into search. At that instant the list already holds the “new” (possibly empty) dataset for what’s on screen.
  3. The original fetch eventually completes for the old section. If executeTrendingSectionFetch returns false (because the section no longer matches currentTrendingSection), the outer task skips observeCurrentDisplay(), so there’s no new syncPodcastListEntries() call.
  4. When the user returns to the chip whose data just landed, podcastList is still showing whatever collection was left from step 2—because we never updated it after the fetch completed—and it stays that
@jubishop
jubishop / gist:9b001eba30525084f1a35d3e7fbb3c8e
Created September 5, 2025 22:00
Staged changes in PodHaven FakeFileManager
diff --git a/PodHavenTests/Fakes/FakeFileManager.swift b/PodHavenTests/Fakes/FakeFileManager.swift
index 5d001e2..96972bb 100644
--- a/PodHavenTests/Fakes/FakeFileManager.swift
+++ b/PodHavenTests/Fakes/FakeFileManager.swift
@@ -3,115 +3,61 @@
import Foundation
@testable import PodHaven
final class FakeFileManager: FileManageable, @unchecked Sendable {
@jubishop
jubishop / staged.diff
Created September 5, 2025 21:59
Staged changes in PodHaven FakeFileManager
diff --git a/PodHavenTests/Fakes/FakeFileManager.swift b/PodHavenTests/Fakes/FakeFileManager.swift
index 5d001e2..96972bb 100644
--- a/PodHavenTests/Fakes/FakeFileManager.swift
+++ b/PodHavenTests/Fakes/FakeFileManager.swift
@@ -3,115 +3,61 @@
import Foundation
@testable import PodHaven
final class FakeFileManager: FileManageable, @unchecked Sendable {

For long-running episode downloads, switch CacheManager to schedule URLSession background downloads instead of relying on beginBackgroundTask. A background session lets the system continue transfers while your app is suspended or terminated, and relaunches the app to deliver completion events.

High-level approach

  • Use URLSessionConfiguration.background(withIdentifier:) with a stable identifier.
  • Create a URLSession with a URLSessionDownloadDelegate to receive progress and completion.
  • Start downloads with downloadTask(with:) and return immediately; don’t await the data.
  • In the delegate:
    • didWriteData: report progress
    • didFinishDownloadingTo: move the temp file to your cache directory, update the DB, and notify state
  • didCompleteWithError: handle failures/resume data
@jubishop
jubishop / String.swift
Created August 18, 2025 05:05
String hash
extension String {
// MARK: - Hashing
func hash(to length: Int = 8) -> String {
guard length > 0 else { return "" }
let data = self.data(using: .utf8)!
let hash = SHA256.hash(data: data)
let hashData = Data(hash)
private var duration: CMTime? {
guard let timeComponents = rssEpisode.iTunes.duration?.split(separator: ":").reversed(),
timeComponents.count <= 3
else { return CMTime.zero }
var seconds = 0
for (position, value) in timeComponents.enumerated() {
guard let value = Int(value) else { return CMTime.zero }
var multiplier = 1
for _ in 0..<position { multiplier *= 60 }
@jubishop
jubishop / tests.swift
Created December 24, 2024 17:23
queue tests
import Foundation
import GRDB
import Testing
@testable import PodHaven
@Suite("of Queue repo tests")
actor QueueTests {
private let repo: Repo
private let podcast: Podcast
@jubishop
jubishop / file.swift
Last active December 24, 2024 17:20
queue management
// MARK: - Queue Management
func dequeue(_ episodeID: Int64) async throws {
try await appDB.db.write { db in
guard let oldPosition = try _fetchOldPosition(db, for: episodeID) else {
return
}
try _moveInQueue(db, episodeID: episodeID, from: oldPosition, to: Int.max)
try Episode.filter(id: episodeID)
@jubishop
jubishop / generics.swift
Created December 11, 2024 17:22
generic swift status AVPlayer
protocol StatusObservable: NSObject, Sendable {
associatedtype Status: Equatable, Sendable
var status: Status { get }
var error: Error? { get }
}
extension AVPlayer: @retroactive Sendable {}
extension AVPlayer: StatusObservable {
typealias Status = AVPlayer.Status
}