Properties2
TypePractice
Note createdJun 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 .ipa you 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.ipa

An .ipa is just a zip with the app under Payload/. ~4 MB for Trellis.

Installing via SideStore

  1. AirDrop ~/Desktop/Trellis.ipa from the Mac to the iPhone → Save to Files.
  2. On the phone: SideStore → ”+” (My Apps) → pick the .ipa from Files → it signs with your Apple ID and installs.
  3. 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_NAME override points at a second *.appiconset added to Assets.xcassets (the only project file change needed — adding an asset, not editing project.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.pbxproj is 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).