Files
ipx/ios/Sources/App.swift
Ray Slakinski f844cf349a iOS: Siri and Shortcuts, and a widget (#427, #428)
Siri: App Intents with phrases that need no setup: Resume, Pause,
Skip forward, Skip back, Set a sleep timer, How many unread. The
playback ones are AudioPlaybackIntents, run in the app and allowed to
start audio from the background. Playback is now one shared instance
(Playback.shared) so Siri and the widget reach it with no page loaded.
It remembers the last episode and its place (LastPlayed), so Resume
works with the app closed; the page, opened afterwards, does not know
about an episode started that way.

Widget: small and medium on the home screen, circular and rectangular
on the lock screen. It shows the unread count and the last episode,
with a Resume or Pause button (PlayPauseIntent, shared with Siri). It
draws what the app writes to the App Group group.net.sdf1.ipx. The app
asks for a redraw on play, pause, a new episode or a new count, not on
every position, since iOS rations redraws. Shared/ builds into both
targets, with WIDGET set in the widget's, where the player does not
exist.

ipx's push now carries content-available, which wakes the closed app
for a moment to give the widget the count the icon just got (#433).

Tested: unit tests for the snapshot and the remembered place, 19 in
all; in the simulator, playing an episode wrote the snapshot to the
App Group. Not testable there: Siri, and the widget drawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:33:04 -05:00

74 lines
3.0 KiB
Swift

import UIKit
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
Badge.registerBackgroundRefresh()
Offline.shared.resume()
Outbox.watch()
return true
}
/// A download kept going while the app was closed has finished (#425): the system launched us
/// to hand it over, and waits on this handler.
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
Offline.shared.backgroundCompletion = completionHandler
Offline.shared.resume()
}
/// Apple's address for this phone's copy of the app (#433), handed to the page to register.
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Push.token = deviceToken.map { String(format: "%02x", $0) }.joined()
}
/// ipx's count, arriving with the app closed (#433): `content-available` wakes the app for a
/// moment, which the badge alone does not, so the widget's count follows the icon's (#428).
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guard let n = (userInfo["aps"] as? [String: Any])?["badge"] as? Int else {
completionHandler(.noData)
return
}
WidgetFeed.update { $0.unread = n }
completionHandler(.newData)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
NSLog("ipx: push registration: %@", error.localizedDescription)
}
}
/// The one window. iOS will not run an app that has not adopted scenes, and a CarPlay app would
/// be a second scene beside this one.
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options: UIScene.ConnectionOptions) {
guard let scene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: scene)
window.rootViewController = WebViewController()
window.makeKeyAndVisible()
self.window = window
}
/// iOS wants the request made as the app leaves, with the user's last look at the list; the
/// one made on coming forward stays for a run that was cut short.
func sceneDidEnterBackground(_ scene: UIScene) {
Badge.scheduleBackgroundRefresh()
}
func sceneDidBecomeActive(_ scene: UIScene) {
Badge.requestPermission()
Badge.scheduleBackgroundRefresh()
Task {
await Outbox.flush()
await Badge.refresh()
}
}
}