Zum Inhalt

App Store Submission Readiness (SP2) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Prepare the iOS + tvOS app for App Store submission up to the "Submit" click — privacy manifest, encryption flag, app-store export config + build script, baseline screenshots, metadata draft, and an App Store Connect checklist — without touching app Swift code.

Architecture: All changes are configuration, scripts, and docs (no Swift logic), so there is zero app-regression surface. The privacy manifest declares exactly the three required-reason APIs the codebase actually uses (audited). A single universal App Store record covers iOS + tvOS (shared bundle id de.jkaindl.physarum). The macOS Developer-ID DMG pipeline is left untouched.

Tech Stack: XcodeGen (apple/PhysarumApp/project.yml), Apple privacy manifest (PrivacyInfo.xcprivacy), xcodebuild archive/-exportArchive, xcrun simctl for screenshots, plist tooling (plutil).

Spec: docs/superpowers/specs/2026-06-21-appstore-submission-readiness-design.md


Conventions

  • Repo root: the physarum checkout (all paths below are repo-relative); App dir: apple/PhysarumApp.
  • Build (macOS) for regression: cd apple/PhysarumApp && xcodegen generate && xcodebuild -scheme Physarum -destination 'platform=macOS' build.
  • iOS/tvOS compile: -destination 'generic/platform=iOS Simulator' / 'generic/platform=tvOS Simulator'.
  • Test target: PhysarumAppTests (host suite must stay green: xcodebuild test -scheme Physarum -destination 'platform=macOS').
  • Commits: Conventional Commits, no emoji, never --no-verify. Commit per task. Do not push (gated). Work on main.
  • No Swift code changes in this plan — only project.yml, plists, shell/TS scripts, and Markdown docs.

Task 1: Privacy manifest (PrivacyInfo.xcprivacy) + wire into the target

Audited required-reason API usage (do not add others without evidence): - UserDefaults/@AppStorage → NSPrivacyAccessedAPICategoryUserDefaults, reason CA92.1 (app's own data). Used in RootView/AppState/EditorView. - File timestamp (.contentModificationDateKey in Capture/CaptureCleanup.swift) → NSPrivacyAccessedAPICategoryFileTimestamp, reason C617.1 (timestamps of files inside the app container). - ProcessInfo.processInfo.systemUptime (Editor/SliderPrecision.swift, Remote/RemoteController.swift) → NSPrivacyAccessedAPICategorySystemBootTime, reason 35F9.1 (measure elapsed time for in-app events). - Disk space, active keyboard: NOT used → do not declare.

Files: - Create: apple/PhysarumApp/PrivacyInfo.xcprivacy - Modify: apple/PhysarumApp/project.yml (add the manifest to the target sources: list, after line ~16 - path: Sources)

  • [ ] Step 1: Create the manifest

Create apple/PhysarumApp/PrivacyInfo.xcprivacy:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key>
  <false/>
  <key>NSPrivacyTrackingDomains</key>
  <array/>
  <key>NSPrivacyCollectedDataTypes</key>
  <array/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>CA92.1</string>
      </array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>C617.1</string>
      </array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>35F9.1</string>
      </array>
    </dict>
  </array>
</dict>
</plist>
  • [ ] Step 2: Lint the plist

Run: plutil -lint apple/PhysarumApp/PrivacyInfo.xcprivacy Expected: … OK.

  • [ ] Step 3: Wire it into the target

In apple/PhysarumApp/project.yml, under the app target's sources: list (the one containing - path: Sources), add:

      - path: PrivacyInfo.xcprivacy
  • [ ] Step 4: Regenerate, build, and confirm it lands in the bundle

Run:

cd apple/PhysarumApp && xcodegen generate && xcodebuild -scheme Physarum -destination 'platform=macOS' build
Expected: ** BUILD SUCCEEDED **. Then locate the built app and confirm the manifest is bundled:
APP=$(find ~/Library/Developer/Xcode/DerivedData -name 'Physarum.app' -path '*/Debug/Physarum.app' 2>/dev/null | head -1)
plutil -p "$APP/Contents/Resources/PrivacyInfo.xcprivacy" 2>/dev/null | head -5 || find "$APP" -name PrivacyInfo.xcprivacy
Expected: the manifest's keys print (or the file is found inside the bundle).

  • [ ] Step 5: iOS + tvOS compile (no break)

Run: xcodebuild -scheme Physarum -destination 'generic/platform=iOS Simulator' build && xcodebuild -scheme Physarum -destination 'generic/platform=tvOS Simulator' build Expected: ** BUILD SUCCEEDED ** twice.

  • [ ] Step 6: Commit
git add apple/PhysarumApp/PrivacyInfo.xcprivacy apple/PhysarumApp/project.yml
git commit -m "feat(appstore): add PrivacyInfo.xcprivacy (UserDefaults, FileTimestamp, SystemBootTime)"

Task 2: Encryption-compliance flag

Files: - Modify: apple/PhysarumApp/project.yml (target info.properties, ~line 31)

  • [ ] Step 1: Add the flag

In apple/PhysarumApp/project.yml, under the app target's info: → properties: block (line ~31, where LSApplicationCategoryType/usage strings live), add:

        ITSAppUsesNonExemptEncryption: false

(Rationale: only CryptoKit SHA-256 for content hashing — standard/exempt. This pre-answers the export-compliance question on every submission.)

  • [ ] Step 2: Regenerate + build + verify it reaches the built Info.plist

Run:

cd apple/PhysarumApp && xcodegen generate && xcodebuild -scheme Physarum -destination 'platform=macOS' build
APP=$(find ~/Library/Developer/Xcode/DerivedData -name 'Physarum.app' -path '*/Debug/Physarum.app' 2>/dev/null | head -1)
plutil -extract ITSAppUsesNonExemptEncryption raw "$APP/Contents/Info.plist"
Expected: ** BUILD SUCCEEDED ** and the plutil -extract prints false.

  • [ ] Step 3: Commit
git add apple/PhysarumApp/project.yml
git commit -m "feat(appstore): declare ITSAppUsesNonExemptEncryption=false"

Task 3: App-Store export config + build script

Files: - Create: scripts/ExportOptions-appstore.plist - Create: scripts/build-appstore.sh (executable)

The existing scripts/ExportOptions.plist (developer-id, macOS DMG) and scripts/build-macos-app.sh MUST remain untouched.

  • [ ] Step 1: Create the app-store export options

Create scripts/ExportOptions-appstore.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>method</key>
  <string>app-store</string>
  <key>teamID</key>
  <string>U9X7M39R56</string>
  <key>signingStyle</key>
  <string>automatic</string>
  <key>uploadSymbols</key>
  <true/>
  <key>destination</key>
  <string>export</string>
</dict>
</plist>

(Note: Xcode 16 renamed app-store → app-store-connect; app-store is still accepted. signingStyle automatic lets Xcode generate the App Store provisioning profile from Johannes' account.)

  • [ ] Step 2: Lint the plist

Run: plutil -lint scripts/ExportOptions-appstore.plist Expected: … OK.

  • [ ] Step 3: Create the build script (mirrors build-macos-app.sh structure)

Create scripts/build-appstore.sh:

#!/usr/bin/env bash
# Archives the native Physarum app for iOS + tvOS and exports App Store .ipa packages.
#
# Output: dist-appstore/Physarum-iOS.xcarchive + dist-appstore/ios/*.ipa (and tvos/*.ipa).
# Requires: Xcode (xcodebuild), xcodegen, and an Apple account with App Store
# distribution enabled (Apple Distribution cert + App Store provisioning). The
# archive/export run with -allowProvisioningUpdates so Xcode can create the profile.
#
# The macOS Developer-ID DMG pipeline (build-macos-app.sh) is separate and untouched.
set -euo pipefail
cd "$(dirname "$0")/.."

SCHEME="Physarum"
TEAM_ID="U9X7M39R56"
PROJ_DIR="apple/PhysarumApp"
EXPORT_PLIST_ABS="$(pwd)/scripts/ExportOptions-appstore.plist"
OUT="dist-appstore"

for tool in xcodebuild xcodegen; do
  command -v "$tool" >/dev/null 2>&1 || { echo "error: required tool not found: $tool" >&2; exit 1; }
done
[ -f "$EXPORT_PLIST_ABS" ] || { echo "error: missing $EXPORT_PLIST_ABS" >&2; exit 1; }

( cd "$PROJ_DIR" && xcodegen generate )
mkdir -p "$OUT"

archive_and_export() {
  local platform="$1" tag="$2"
  local archive="$OUT/Physarum-$tag.xcarchive"
  echo "==> Archiving $tag ($platform)"
  xcodebuild archive \
    -project "$PROJ_DIR/Physarum.xcodeproj" \
    -scheme "$SCHEME" \
    -destination "generic/platform=$platform" \
    -archivePath "$archive" \
    -allowProvisioningUpdates \
    DEVELOPMENT_TEAM="$TEAM_ID"
  echo "==> Exporting $tag .ipa"
  xcodebuild -exportArchive \
    -archivePath "$archive" \
    -exportOptionsPlist "$EXPORT_PLIST_ABS" \
    -exportPath "$OUT/$tag" \
    -allowProvisioningUpdates
}

archive_and_export "iOS" "ios"
archive_and_export "tvOS" "tvos"

echo "Done. Upload the .ipa(s) in $OUT/ via Xcode Organizer or 'xcrun altool/notarytool'/Transporter."
  • [ ] Step 4: Make executable + syntax-check (do NOT run a real archive — it needs Johannes' account)

Run:

chmod +x scripts/build-appstore.sh
bash -n scripts/build-appstore.sh && echo "syntax OK"
command -v shellcheck >/dev/null && shellcheck scripts/build-appstore.sh || echo "(shellcheck not installed — skipped)"
Expected: syntax OK (shellcheck clean if installed). A full archive is Johannes' step (gated on his App Store distribution setup).

  • [ ] Step 5: Commit
git add scripts/ExportOptions-appstore.plist scripts/build-appstore.sh
git commit -m "feat(appstore): app-store export options + iOS/tvOS archive script"

Task 4: Baseline screenshots from the simulator

Files: - Create: scripts/shoot-appstore-screens.sh (executable) - Output (gitignored build artifacts go to): docs/appstore/screenshots/<device>/

Captures the rendered player hero per required device size. No app code change; the app boots to its first filled curated preset. Richer multi-state shots (editor/library) are Johannes' curation or a later XCUITest follow-up.

  • [ ] Step 1: Create the screenshot script

Create scripts/shoot-appstore-screens.sh:

#!/usr/bin/env bash
# Builds Physarum for the simulator and captures App Store baseline screenshots
# (the rendered player hero) for each required device size.
#
# Output: docs/appstore/screenshots/<device>/01-hero.png
# Requires: Xcode + simulators for iPhone 16 Pro Max, iPad Pro 13-inch (M4), Apple TV 4K.
set -euo pipefail
cd "$(dirname "$0")/.."

SCHEME="Physarum"
PROJ="apple/PhysarumApp/Physarum.xcodeproj"
BUNDLE_ID="de.jkaindl.physarum"
OUT="docs/appstore/screenshots"
( cd apple/PhysarumApp && xcodegen generate )

shoot() {
  local sim="$1" dir="$2" sdk="$3"
  echo "==> $sim"
  local udid
  # Portable UDID extraction (macOS awk lacks 3-arg match): grep the UUID off the device line.
  udid=$(xcrun simctl list devices available | grep -F "$sim (" | grep -oiE '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}' | head -1)
  [ -n "$udid" ] || { echo "skip: no simulator named '$sim'"; return 0; }
  xcrun simctl boot "$udid" 2>/dev/null || true
  xcrun simctl bootstatus "$udid" -b
  # Build the app for this simulator (into a temp DerivedData so the repo stays clean) and locate the .app
  local dd="${TMPDIR:-/tmp}/physarum-dd-$dir"
  xcodebuild -project "$PROJ" -scheme "$SCHEME" -destination "id=$udid" -derivedDataPath "$dd" build >/dev/null
  local app
  app=$(find "$dd/Build/Products" -name 'Physarum.app' -path "*$sdk*" | head -1)
  [ -n "$app" ] || { echo "skip: no .app built for $sim"; return 0; }
  xcrun simctl install "$udid" "$app"
  xcrun simctl launch "$udid" "$BUNDLE_ID"
  sleep 8   # let the Metal renderer warm up and the pattern form
  mkdir -p "$OUT/$dir"
  xcrun simctl io "$udid" screenshot "$OUT/$dir/01-hero.png"
  echo "wrote $OUT/$dir/01-hero.png"
}

shoot "iPhone 16 Pro Max" "iphone-6.9" "iphonesimulator"
shoot "iPad Pro 13-inch (M4)" "ipad-13" "iphonesimulator"
shoot "Apple TV 4K" "appletv" "appletvsimulator"

echo "Done. Review docs/appstore/screenshots/*/01-hero.png (replace/add as desired)."
  • [ ] Step 2: Make executable + syntax-check

Run: chmod +x scripts/shoot-appstore-screens.sh && bash -n scripts/shoot-appstore-screens.sh && echo "syntax OK" Expected: syntax OK.

  • [ ] Step 3: Run it + verify the PNGs are non-empty and rendered (not black)

Run: ./scripts/shoot-appstore-screens.sh Then verify each produced PNG exists and has plausible size (a black/empty frame is a failure):

for f in docs/appstore/screenshots/*/01-hero.png; do echo "$f: $(stat -f%z "$f") bytes"; done
Expected: each file exists and is substantially larger than a blank frame (tens to hundreds of KB). If a simulator name doesn't exist on this machine, the script skips it — note which sizes were captured. If a frame is black, fall back to a longer sleep or capture on a real device (Johannes).

  • [ ] Step 4: Add a .gitignore note OR commit the baseline PNGs

Decide: commit the baseline PNGs (so they're versioned) — they are small and useful. Ensure they are tracked:

git add scripts/shoot-appstore-screens.sh docs/appstore/screenshots
git status --short

  • [ ] Step 5: Commit
git commit -m "feat(appstore): simulator screenshot script + baseline hero shots"

Task 5: Metadata draft, privacy labels, and ASC checklist

Files: - Create: docs/appstore/metadata.md - Create: docs/appstore/privacy-labels.md - Create: docs/appstore/ASC-submission-checklist.md

  • [ ] Step 1: Metadata draft

Create docs/appstore/metadata.md (Johannes finalizes; the App Store name must be globally unique — verify in ASC):

# App Store metadata — Physarum (draft)

- **Name:** Physarum  *(if taken in ASC, try "Physarum — Living Ink" / "Physarum Ambient")*
- **Subtitle (≤30):** Living, audio-reactive art
- **Category:** Entertainment (Primary). Secondary: Graphics & Design (optional)
- **Age rating:** 4+
- **Price:** Free
- **Promotional text (≤170):** A mesmerizing slime-mold simulation you can shape and play — audio-reactive, endlessly evolving, beautiful on iPhone, iPad, and the big screen via Apple TV.
- **Keywords (≤100, comma-separated):** generative,art,slime mold,physarum,audio reactive,visualizer,ambient,simulation,creative,wallpaper,relaxing,apple tv
- **Description:**

Physarum turns the emergent behaviour of slime mold into living, breathing art.
Thousands of agents lay down and follow trails, and from those simple rules
intricate, ever-shifting patterns emerge. Shape them with a full parameter
editor, let them react to music through your microphone, and capture stills,
videos, or Live Photos. On Apple TV, Physarum becomes a lean-back ambient
display you steer from the couch or mirror live from your phone.

- Full authoring editor: tune agents, sensing, deposition, decay, colour, post-FX
- Audio-reactive: the visuals pulse and bloom to sound
- Capture: high-res stills, video clips, Live Photos
- Apple TV ambient mode with phone control
- Curated presets to start from; save your own

- **Support URL:** https://git.jkaindl.de/jkaindl/physarum  *(or a dedicated support page)*
- **Marketing URL:** https://uplink.jkaindl.de  *(optional)*
- **Copyright:** © 2026 Johannes Kaindl
  • [ ] Step 2: Privacy nutrition labels

Create docs/appstore/privacy-labels.md:

# App Privacy answers (ASC "App Privacy" questionnaire)

**Summary: Data Not Collected.** Physarum does not collect or transmit any user data.

- **Tracking:** No. (Manifest: NSPrivacyTracking=false, no tracking domains.)
- **Microphone:** Audio is analysed on-device into frequency bands to drive the
  visuals in real time. It is never recorded, stored, or transmitted. → not "collected".
- **Photos:** Add-only — the app writes captured images/clips to the user's photo
  library on request. It never reads or collects photos. → not "collected".
- **Local network / Bonjour:** Used only to connect to the user's own Apple TV on
  the local network for screen control. No data leaves the local network; nothing
  collected. 
- **Required-reason APIs (declared in PrivacyInfo.xcprivacy):** UserDefaults (CA92.1,
  app's own settings), File timestamp (C617.1, pruning the app's own temp files),
  System boot time (35F9.1, measuring elapsed time for in-app throttling).

In ASC, answer "No, we do not collect data from this app" for every category.
  • [ ] Step 3: ASC submission checklist

Create docs/appstore/ASC-submission-checklist.md:

# App Store Connect submission checklist — iOS + tvOS (Johannes' steps)

Prepared by SP2. The artifacts (privacy manifest, encryption flag, export config,
build script, screenshots, metadata, privacy answers) are in the repo. These are
the account-bound steps that must be done in App Store Connect / Xcode.

## One-time account setup
- [ ] Apple Developer Program active (Team U9X7M39R56).
- [ ] Apple Distribution certificate present (Xcode → Settings → Accounts → Manage Certificates → +).
- [ ] (Free app) Confirm the Free Apps agreement is active in ASC → Agreements, Tax, and Banking.

## Create the app record (ONE universal record for iOS + tvOS)
- [ ] ASC → Apps → + → New App. Platforms: iOS **and** tvOS. Bundle ID: `de.jkaindl.physarum`.
      If "Physarum" is taken, pick an alternative name (see metadata.md) — the bundle id stays.
- [ ] Set Primary category Entertainment, Age rating 4+ (questionnaire: no objectionable content), Price Free.

## Build + upload
- [ ] Run `PHYSARUM... ./scripts/build-appstore.sh` (or archive in Xcode: Product → Archive for iOS, then tvOS).
- [ ] Upload each .ipa via Xcode Organizer → Distribute App → App Store Connect (or Transporter).
- [ ] Wait for processing; select the build for each platform in ASC.

## Listing
- [ ] Paste name/subtitle/keywords/description/promo from `docs/appstore/metadata.md`.
- [ ] Upload screenshots from `docs/appstore/screenshots/` (iPhone 6.9", iPad 13", tvOS) — replace/add as desired.
- [ ] App Privacy: answer "Data Not Collected" per `docs/appstore/privacy-labels.md`.
- [ ] Support URL + (optional) Marketing URL.

## Submit
- [ ] Add a reviewer note if helpful (mic = audio-reactive visuals; local network = Apple TV control).
- [ ] Submit for Review (both platforms in the one submission).

## Notes / gotchas
- iOS + tvOS share one record because they share the bundle id `de.jkaindl.physarum`.
- macOS is NOT in the App Store — it ships as the notarized Developer-ID DMG (unchanged).
- Export compliance is pre-answered via ITSAppUsesNonExemptEncryption=false.
  • [ ] Step 4: Lint the docs exist + commit

Run: ls -1 docs/appstore/*.md Expected: the three files listed.

git add docs/appstore/metadata.md docs/appstore/privacy-labels.md docs/appstore/ASC-submission-checklist.md
git commit -m "docs(appstore): metadata draft, privacy labels, ASC submission checklist"

Task 6: Final regression + summary

  • [ ] Step 1: Regenerate + build all three platforms

Run:

cd apple/PhysarumApp && xcodegen generate \
 && xcodebuild -scheme Physarum -destination 'platform=macOS' build \
 && xcodebuild -scheme Physarum -destination 'generic/platform=iOS Simulator' build \
 && xcodebuild -scheme Physarum -destination 'generic/platform=tvOS Simulator' build
Expected: ** BUILD SUCCEEDED ** three times (the manifest + encryption flag must not break any platform).

  • [ ] Step 2: Host suite (no regression — SP1 stays green)

Run: xcodebuild test -scheme Physarum -destination 'platform=macOS' Expected: ** TEST SUCCEEDED **, 293 tests, 0 failures.

  • [ ] Step 3: Confirm the developer-id macOS pipeline is untouched

Run: git diff --name-only bf70584..HEAD -- scripts/ExportOptions.plist scripts/build-macos-app.sh apple/PhysarumApp/Physarum.macOS.entitlements Expected: empty (none of the macOS-DMG pipeline files changed).

  • [ ] Step 4: Done

No commit needed if Steps 1-3 are clean (verification only). SP2 deliverables are complete; the remaining work is Johannes' account-bound ASC steps in docs/appstore/ASC-submission-checklist.md.


Done criteria

  • [ ] PrivacyInfo.xcprivacy bundled, valid, declares exactly the 3 audited APIs; iOS+tvOS build green.
  • [ ] ITSAppUsesNonExemptEncryption=false in the built Info.plist.
  • [ ] ExportOptions-appstore.plist + build-appstore.sh created (lint/syntax clean); developer-id pipeline untouched.
  • [ ] Baseline screenshots produced for the available required device sizes (non-black).
  • [ ] metadata.md + privacy-labels.md + ASC-submission-checklist.md written.
  • [ ] 3-platform build + host suite still green (zero regression).