Files
ipx/ios/project.yml
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

136 lines
4.9 KiB
YAML

# XcodeGen makes the .xcodeproj from this, so the project is something a diff can show rather
# than a pbxproj nobody reviews. On a Mac:
#
# brew install xcodegen && cd ios && xcodegen generate && open iPX.xcodeproj
name: iPX
options:
bundleIdPrefix: net.sdf1
deploymentTarget:
iOS: "17.0"
createIntermediateGroups: true
settings:
base:
SWIFT_VERSION: "5.9"
TARGETED_DEVICE_FAMILY: "1,2"
CODE_SIGN_STYLE: Automatic
targets:
iPX:
type: application
platform: iOS
sources:
- Sources
- Shared
- Resources/Assets.xcassets
dependencies:
- target: iPXWidgets
info:
path: Resources/Info.plist
properties:
CFBundleDisplayName: iPX
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: "12"
# Playing on with the screen locked. A plist key, not a signed entitlement, so it works on
# a free personal team; CarPlay's own dashboard app is an entitlement Apple has to grant.
UIBackgroundModes: [audio, fetch, remote-notification]
# The unread badge's refresh while the app is closed (#421).
BGTaskSchedulerPermittedIdentifiers: [net.sdf1.ipx.refresh]
# HTTPS only, so TestFlight need not ask the export-compliance question on every build.
ITSAppUsesNonExemptEncryption: false
UILaunchScreen: {}
UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false
UISceneConfigurations:
UIWindowSceneSessionRoleApplication:
- UISceneConfigurationName: Default Configuration
UISceneDelegateClassName: $(PRODUCT_MODULE_NAME).SceneDelegate
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
# Plain HTTP only for a server on your own network (192.168.1.130:8099); the tunnel is HTTPS.
NSAppTransportSecurity:
NSAllowsLocalNetworking: true
# The page's service worker runs only for these, so iPX opens offline only at them
# (#435): the server, and Cloudflare Access and Authentik, where its sign-in goes. Ten at
# most, and a server not here loses the bridge (WebViewController.makeWebView).
WKAppBoundDomains:
- ipodderx.sdf1.net
- rays-sdf1.cloudflareaccess.com
- authentik.sdf1.net
# The icon's count pushed by ipx (#433). Development here; Xcode makes it production when it
# exports for TestFlight.
entitlements:
path: Resources/iPX.entitlements
properties:
aps-environment: development
# Shared with the widget (#428), which reads what the app last knew from it.
com.apple.security.application-groups: [group.net.sdf1.ipx]
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: net.sdf1.ipx
MARKETING_VERSION: "0.1.0"
DEVELOPMENT_TEAM: 8743L8D729
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
# The script below writes into the built Info.plist, which the sandbox would refuse.
ENABLE_USER_SCRIPT_SANDBOXING: NO
postBuildScripts:
# A debug build is app-bound to localhost too, so the simulator can test the service
# worker against a page served on the Mac (#435). Never in a release.
- name: App-bound localhost (debug)
basedOnDependencyAnalysis: false
script: |
if [ "$CONFIGURATION" = Debug ]; then
/usr/libexec/PlistBuddy -c "Add :WKAppBoundDomains: string localhost" "$TARGET_BUILD_DIR/$INFOPLIST_PATH"
fi
# The widget (#428): the unread count and the last episode, from what the app writes to the App
# Group. Its build number has to match the app's, or App Store Connect refuses the upload.
iPXWidgets:
type: app-extension
platform: iOS
sources:
- Widgets
- Shared
info:
path: Widgets/Info.plist
properties:
CFBundleDisplayName: iPX
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: "12"
NSExtension:
NSExtensionPointIdentifier: com.apple.widgetkit-extension
entitlements:
path: Widgets/iPXWidgets.entitlements
properties:
com.apple.security.application-groups: [group.net.sdf1.ipx]
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: net.sdf1.ipx.widgets
MARKETING_VERSION: "0.1.0"
DEVELOPMENT_TEAM: 8743L8D729
# Shared/ is built here too, where the app's player does not exist.
SWIFT_ACTIVE_COMPILATION_CONDITIONS: $(inherited) WIDGET
iPXTests:
type: bundle.unit-test
platform: iOS
sources:
- Tests
dependencies:
- target: iPX
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: net.sdf1.ipx.tests
GENERATE_INFOPLIST_FILE: YES
schemes:
iPX:
build:
targets:
iPX: all
iPXTests: [test]
test:
targets:
- iPXTests