FTStore Manager puts an app on a device through the same three stages FTStore does, and had nothing to show for any of it once the screen was off. It has a Live Activity now: the same widget the app carries, embedded as its own extension under its own identifier, naming the app, filling a bar while the bytes arrive, adding the rate and the time left, then Preparing and Installing. It is the app's island with the pause button taken out. This download manager has no pause and no screen to offer one from, so there are no intents to compile into the extension and no paused state in the payload. The island is read-only on every version rather than only on the older ones. It ends when the installer answers, which is the part the app had to learn the hard way. Open launches what was just installed and leaves the sheet standing behind it, so ending on dismissal left the island describing an install that had finished, until somebody came back and closed a receipt they were done with. Dismissal still ends it, which is what covers a flow that never gets an answer at all. A dropped connection is now one of those answers. It used to remove the download and tell nobody, leaving the flow spinning with no way to find out; that was merely invisible before and would now be an island counting bytes that stopped arriving. None of this is visible from a build: an IPA whose extension is missing, misnamed or misfiled installs, runs, and simply shows no island. Both of the Manager's release workflows therefore run the same check the app's do against the built IPA, and the ad-hoc signing step seals the nested extension. Two products now build an extension of the same name out of projects that share their identifiers, so the separate derived data paths carry more weight than before, and the notes that explain them say so.
82 lines
4.7 KiB
Markdown
82 lines
4.7 KiB
Markdown
# FTStoreActivity — Live Activity / Dynamic Island (Manager)
|
|
|
|
This folder holds the **Live Activity** widget extension that shows the
|
|
download → sign → install progress in the Dynamic Island and on the lock screen
|
|
while FTStore Manager puts FTStore (or FTStore Admin) onto the device.
|
|
|
|
It is the app's island, forked the way the rest of `manager/` was: same shape,
|
|
same files, minus what the Manager does not have. See
|
|
`ios/FTStoreActivity/README.md` for the reasoning behind the design — this file
|
|
only records where the two differ and what is true here.
|
|
|
|
## What is different from the app's
|
|
|
|
- **No pause button, and no `isPaused` anywhere.** The Manager's `Download` has
|
|
no pause: there is nothing to hold a download with and no screen to offer it
|
|
from. So `InstallActivityIntents.swift` does not exist here, the extension
|
|
needs no `AppIntents`, and the island is read-only on every iOS version rather
|
|
than only below 17.
|
|
- **One install in practice, the same shape regardless.** The Manager tracks two
|
|
products and nothing stops both being started, so `ContentState` still
|
|
describes a set. It costs nothing and it is the only shape iOS renders well:
|
|
a second *activity* collapses the island to its minimal presentation.
|
|
- Bundle id: **`$(FTSTORE_PRODUCT_BUNDLE_IDENTIFIER).activity`** →
|
|
`app.ftstore.manager.activity`, which sits under the Manager's own
|
|
`app.ftstore.manager` exactly as iOS requires.
|
|
|
|
## Facts about the target
|
|
|
|
- The widget-extension **target exists in `FTStore.xcodeproj`** (added by hand to
|
|
`project.pbxproj`, synchronized-folder style, ids prefixed `57AC11…` — the
|
|
same ids the app's tree uses, because this project is a fork of that one down
|
|
to its GUIDs and keeping them equal is what keeps the two files diffable).
|
|
- This folder is a synchronized root group owned by the `FTStoreActivity`
|
|
target; `Info.plist` and `README.md` are membership exceptions.
|
|
- `FTStore/Backend/Observable/InstallActivityAttributes.swift` is shared into the
|
|
extension via an exception set on the `FTStore` folder: the widget needs the
|
|
attributes type to render. **It must stay in both targets.**
|
|
- iOS Deployment Target **16.1** (the app is 16.0; ActivityKit needs 16.1, and
|
|
`LiveActivityManager` uses the 16.2 `ActivityContent` API).
|
|
- The appex is **iOS-only**: the embed build file and the target dependency
|
|
carry `platformFilter = ios`, so Mac Catalyst builds skip it entirely.
|
|
- The app's `Info.plist` sets **`NSSupportsLiveActivities = YES`**.
|
|
- The Makefile ad-hoc codesign uses `--deep` so the nested unsigned appex
|
|
doesn't make sealing fail; on-device signing re-signs PlugIns anyway.
|
|
|
|
## How it's driven
|
|
|
|
- `LiveActivityManager` (in the app target) starts/updates/ends the activity.
|
|
- `AppInstallManager` calls it across the lifecycle: `start` when the download
|
|
begins, `update(.signing)` while signing, `update(.installing)` at install
|
|
hand-off, and `end` when the installer answers, on cancel, and on failure.
|
|
- **The island ends when the installer answers, not when the sheet closes.**
|
|
Tapping *Open* launches the app that was just installed and leaves the sheet
|
|
standing behind it, so ending on dismissal left the island saying "Installing"
|
|
until somebody came back and closed a receipt they were done with. Dismissal
|
|
still ends it, which is what covers a flow that never gets an answer at all.
|
|
- Everything is best-effort and availability-gated: with no widget target (or on
|
|
iOS < 16.2), all calls are safe no-ops and the rest of the app is unaffected.
|
|
|
|
## Don't let another product share this build's derived data
|
|
|
|
`manager/` is a fork of `ios/` down to the pbxproj GUIDs, and Xcode keys its
|
|
build database on those. When both Makefiles pointed `-derivedDataPath` at
|
|
`$(TMPDIR)/FTStore`, the app's build and this one swept each other's
|
|
intermediates; the app shipped an `.appex` left over from an older build and its
|
|
island showed nothing. Both Makefiles key the path on the product directory, and
|
|
now that *both* products carry an extension, that separation is the only thing
|
|
keeping them apart.
|
|
|
|
The symptom is invisible from the app side, so the release workflows run
|
|
`ios/scripts/verify-live-activity.sh` against the built IPA — this one included —
|
|
and fail on it. What it checks, and how to ask a device why the island is dead,
|
|
is documented in `ios/FTStoreActivity/README.md`; both hold here unchanged,
|
|
except that the log predicate is `subsystem BEGINSWITH "app.ftstore.manager"`.
|
|
|
|
## Verifying
|
|
|
|
Run on a device or simulator with iOS 16.2+. Tap **Install FTStore** on the home
|
|
screen: the Dynamic Island (iPhone 14 Pro and newer) and the lock-screen banner
|
|
should show the app name, a progress bar while downloading, then "Preparing" and
|
|
"Installing", and dismiss shortly after the install sheet answers.
|