iPhone
The iPhone app exists so SoundStorm can be on the App Store rather than only a home-screen web app. It is a native Swift shell around the server's own page today, with native audio planned as the second stage - and a deliberate decision not to rewrite the app in Swift.
Three shapes, and the one chosen
Three ways of building it were weighed:
- A thin wrapper - a native shell that loads the server's web app.
- A wrapper whose audio is native - the same shell, with music and audiobooks played by iOS itself rather than by the page.
- A fully native app, rebuilt in Swift.
The second was chosen, built in two stages.
app.js is the whole product across five kinds of media. A Swift rewrite
would mean building every feature twice from then on - and Android would still be on the web. What
only native code can give on a phone is CarPlay, audio that iOS will not stop, and the lock screen:
all of it audio. So only the audio goes native. Nothing commits the shell to staying thin; it can
grow screen by screen later if that ever looks worth it.The reasoning that ruled out a WebView app on Android does not carry over. Android's WebView has
no Media Session API and is stopped in the background; an iPhone's WKWebView is Safari's
engine, and with background audio enabled (UIBackgroundModes) and the
.playback audio session, the page's <audio> keeps playing when the
phone locks.
Stage one: the shell
Plain Swift in ios/SoundStorm, with no Capacitor. Capacitor is built to bundle a web
app inside the app, and this web app lives on each person's own server; stage two is Swift anyway.
What the shell does:
- Asks for the server's address on first launch, since every install is somebody's
own, and checks that
/healthzanswers like SoundStorm before keeping it. With no scheme typed it assumes https. - Shows the page full screen, laid out under the status bar with the same
env(safe-area-inset-*)values the installed web app uses. - Shows
alert,confirmandprompt- aWKWebViewshows none of them unless its app does, and SoundStorm asks before removing downloads. - Sends links off the server to Safari, but only for a link that was actually tapped in the main frame; other schemes and new windows are not opened on a page's say-so.
- Sets
window.soundstormAppbefore the page runs, which is howapp.jsknows to show Change server beside Sign out. - Allows plain http only on the local network
(
NSAllowsLocalNetworking).
Messages from the page are accepted only from the server's own origin, port included - anyone can
get a *.net.soundstorm.dev name, so a matching host alone is not enough.
Moving to the secure name
The first TestFlight build had a bug the Android app had had too. Opened at the away-from-home
name while at home, the page moves itself to the install's home name
(moveToSecureName - the server offers it to any page not already on it), and the app
took that move for a link out of SoundStorm and sent it to Safari.
The app now follows a move to https on <id>.home.soundstorm.dev, on the same
port, where the id is the current name's own. It follows the move for this launch and does not
save it. A phone leaves the house; with the home name saved, the app would stop working the
moment it did. Not saving also closes a hole a security review found: from a plain-http address,
anybody on the same Wi-Fi could otherwise have pinned the app to a server of their own.
The status bar in Now Playing
The status bar hides while Now Playing is open, and only then - the owner's choice. Android hides
both bars everywhere, but an iPhone has no swipe that brings a hidden status bar back for a glance at
the time, so everywhere else it stays. The app watches the page's body for the
np-open class from its document-start script rather than being told by
app.js, so the feature needed no change to the web app and no server deploy.
No service worker, and what that costs
A WKWebView runs a service worker only for App-Bound Domains: a fixed list of at most
ten, set when the app is built. Every install has its own address, so the list cannot name them. The
practical result is that opening the app with no connection shows the "can't reach" screen rather
than the downloads. That is expected, not a bug in the shell; offline downloads on the iPhone are
part of stage two.
Stage two: native audio
The plan is an AVPlayer behind a bridge the page talks to in place of
<audio>, giving the system's Now Playing, remote commands and, later, CarPlay - the
same shape the Android app already uses with Media3. It is not a small bridge, because the page's
audio is not one element:
- crossfade uses a second, hidden element;
- gapless playback preloads the next song into memory;
- leveling sets
volume, and audiobooks setplaybackRate; - downloads play from the Cache API through blob addresses.
The bridge has to answer for all of those. One question decides how urgent it is: whether the lock screen shows the page's Media Session title and controls on a real iPhone. The simulator cannot say, and it has not yet been checked on a device.
Photo backup
The iPhone app backs up the camera roll to the person's own photo folder
(ios/SoundStorm/PhotoBackup.swift). It answers the page's
window.soundstormApp.backup(...) messages with the same status fields the Android app
reports, so the page has no iPhone code: the switch and its options (Wi-Fi only, videos, only while
charging) are the same Settings card.
- What it reads: the photo library, with full or limited access, asked for when backup is turned on. Newest first.
- Nothing twice: each batch is first checked with
POST /api/photos/backup/checkby name and month (size 0, since sizing a photo kept in iCloud would mean downloading it), so a reinstall or a new phone sends nothing the server has. - How it sends: each original is written to a file
(
PHAssetResourceManager, iCloud allowed; a Live Photo's still only) and uploaded toPUT /api/photos/backupin a backgroundURLSession, so a file already going finishes with the app put away. It sends the web view's own session cookie - WKWebView keeps its cookies apart from URLSession's - to the server only, never through a redirect. - When: while the app is open, 30 seconds after a photo is added, and as a
BGProcessingTaskiOS starts when it chooses (needing power if "only while charging"). Wi-Fi only means a connection that is not expensive or constrained. - Where to: the page's secure address, its away-from-home twin, then the address the app was opened with.
- On a shared phone: signing out, or another account signing in, turns backup off; the page asks the next person afresh.
Checked against a local server in the simulator: the six sample photos arrived in the right year and month, byte for byte. The background task and an upload finishing with the app closed want a real iPhone over a night.
Several saved servers
A phone may know several servers - its owner's, and a parent's. The connect screen is
Your servers: the latest used first, the one in use ticked, a tap to switch, a hold
for Rename and Remove, and a way to add another. The list (ServerAddress.all, in
ios/Shared and used by the Apple TV app too) keeps each address and a name; a first name
is the address without .home.soundstorm.dev. Sign-ins stay with each server, since
cookies belong to an address, so switching signs nobody out.
How it is tested
An XCUITest in ios/SoundStormUITests drives the app through connecting and signing
in, and skips itself when no server answers locally. simctl cannot type or tap, and
scripting the Simulator through System Events waits on a macOS permission prompt nobody sees, so the
UI test is how the app gets driven. Two small traps from building it: a centred stack view measures a
multi-line label as one line until preferredMaxLayoutWidth is set, and in the UI test the
keyboard's accessory bar sits over the web form's submit button, so the test presses Return instead -
which is also what a person does.
The iPhone and Apple TV apps share one bundle id, so Apple's Universal Purchase makes them a single App Store listing (see TVs).