Properties2
| Type | Practice |
| Note created | Jun 27, 2026 |
How I run my personal SwiftUI app (Trellis) on my own iPhone without paying the €99/year Apple Developer Program fee. A free Apple ID can sign and install apps, but the catch is that those signatures expire after ~7 days — the app icon greys out and stops launching, and you have to re-install + re-trust from a Mac every week. SideStore removes that chore: it re-signs the app with your Apple ID and refreshes the signature in the background, so a “stable” copy keeps working without a weekly Mac visit.
The model I settled on: a stable copy installed through SideStore, and a separate dev copy loaded over USB for testing — see Dev and stable side by side.
Why SideStore
- Free Apple ID signing → certs valid 7 days. USB/Xcode installs go stale and grey out (“app not available”).
- SideStore signs with your Apple ID and auto-refreshes on Wi-Fi (needs its one-time pairing + anisette setup done). The stable app then survives indefinitely with no manual re-trust.
- It re-signs whatever
.ipayou feed it, so the build’s own code-signing doesn’t matter — even an unsigned or dev-signed build works.
Building an .ipa
Xcode projects don’t emit .ipa directly. Build the app for a device, then wrap the .app in a Payload/ folder and zip it.
# 1. Build a device build (Debug is fine; SideStore re-signs anyway)
xcodebuild -project "Trellis iOS.xcodeproj" -scheme "Trellis iOS" \
-configuration Debug -destination 'generic/platform=iOS' \
-derivedDataPath build -allowProvisioningUpdates build
# 2. Package the .app into an .ipa
APP="build/Build/Products/Debug-iphoneos/Trellis iOS.app"
work=$(mktemp -d); mkdir "$work/Payload"
cp -R "$APP" "$work/Payload/"
( cd "$work" && zip -qr -y "$work/Trellis.ipa" Payload )
cp "$work/Trellis.ipa" ~/Desktop/Trellis.ipaAn .ipa is just a zip with the app under Payload/. ~4 MB for Trellis.
Installing via SideStore
- AirDrop
~/Desktop/Trellis.ipafrom the Mac to the iPhone → Save to Files. - On the phone: SideStore → ”+” (My Apps) → pick the
.ipafrom Files → it signs with your Apple ID and installs. - First launch only: Settings → General → VPN & Device Management → trust your Apple ID (needs an internet connection to verify).
Free Apple ID limits
These bite when sideloading; SideStore shows an “App IDs remaining” screen.
- 3 active sideloaded apps at once. SideStore itself uses one slot. (Empirically, app extensions do not seem to consume an active-app slot, only an App ID.)
- 10 App IDs per rolling 7 days. Each new bundle id (and each extension) registers an App ID. They can’t be deleted manually on a free account — they expire on their own after ~7 days (this is the part “just wait” fixes).
- A widget/extension consumes an extra App ID — budget for it.
- SideStore auto-refresh renews the cert, not the version. New code = you must hand SideStore a new
.ipa(manually, or automatically via a source — see below).
Dev and stable side by side
Two copies coexist if they have different bundle identifiers (iOS treats them as separate apps with separate data/login/keychain). The neat trick: inject the dev identity as xcodebuild command-line overrides, so the checked-in Xcode project is never modified.
# Dev build: different bundle id, name, and app icon — no project edits
xcodebuild -project "Trellis iOS.xcodeproj" -scheme "Trellis iOS" \
-configuration Debug -destination 'platform=iOS,id=<DEVICE_UDID>' \
-derivedDataPath build-dev -allowProvisioningUpdates \
PRODUCT_BUNDLE_IDENTIFIER=codes.diego.Trellis.dev \
INFOPLIST_KEY_CFBundleDisplayName="Trellis Dev" \
ASSETCATALOG_COMPILER_APPICON_NAME=TrellisDev \
build
# Install over USB
xcrun devicectl device install app --device <DEVICE_UDID> \
"build-dev/Build/Products/Debug-iphoneos/Trellis iOS.app"- Stable = Release
.ipa(codes.diego.Trellis, name “Trellis”) → SideStore, auto-refreshed. - Dev = the Debug build above (
codes.diego.Trellis.dev, “Trellis Dev”) → USB. It’s Xcode-signed, so it greys out after ~7 days — just rebuild + reinstall when iterating (that’s the point of the USB channel). - Distinct icon: the
ASSETCATALOG_COMPILER_APPICON_NAMEoverride points at a second*.appiconsetadded toAssets.xcassets(the only project file change needed — adding an asset, not editingproject.pbxproj). For Trellis I reused the real leaf (foreground.png) recoloured black on hazard-yellow so the dev copy reads as a “construction” build. - On a free account, dev + stable + SideStore = the full 3 app slots — delete any other sideloaded leftovers first.
Gotchas
- Device must be unlocked for a USB install (
devicectl) — otherwise: “developer disk image could not be mounted… device is locked.” - Icon caching: after changing the icon, the home screen may keep the old one — respring/reboot or delete+reinstall to force the refresh.
- “App not available” (greyed icon) = the signature lapsed → re-sign (SideStore refresh) or, for a USB build, rebuild + re-trust.
project.pbxprojis fragile — prefer build-setting overrides (above) or Xcode over hand-editing it.- Don’t mix channels: a USB/Xcode reinstall over the SideStore copy replaces it with a 7-day-expiring signature. Keep stable on SideStore, dev on USB.
Next step: automate via a source
Manually shuttling .ipas gets old. SideStore (like AltStore) supports a source — a JSON manifest pointing at hosted .ipas with version numbers. With CI publishing each build + updating the manifest, SideStore auto-updates the app in the background. See the open question of where to host releases (GitHub vs Tangled with a self-hosted runner).