Declare XSAppIconAssets in the app manifest so actool receives --app-icon and the bundle carries CFBundleIconName, and always pass both Xcode version placeholders to the ReplayKit extension so an empty build number cannot drop CFBundleVersion and fail installd.
143 lines
6.0 KiB
Markdown
143 lines
6.0 KiB
Markdown
# Deploying the managed iOS app
|
||
|
||
VoiceCat's supported iOS client is the .NET 10 UIKit application in
|
||
`clients/apple/VoiceCat.iOS`. The checked-in scripts build its native Opus/RNNoise
|
||
dependency, compile and sign the managed app and Swift ReplayKit extension, stage the bundle,
|
||
install it, and launch it on a paired physical device.
|
||
|
||
## Prerequisites
|
||
|
||
- macOS 27, Xcode 27, .NET SDK 10.0.401, and the matching iOS workload.
|
||
- An unlocked, trusted iPhone connected by USB or visible to Xcode over the network.
|
||
- Apple Development profiles for `me.iamtalon.voicecat` and
|
||
`me.iamtalon.voicecat.broadcast`, both with App Group `group.me.iamtalon.voicecat`.
|
||
|
||
List available devices:
|
||
|
||
```bash
|
||
clients/apple/deploy-ios-device.sh --list
|
||
```
|
||
|
||
## Build, install, and launch
|
||
|
||
Set the Apple developer team explicitly. The .NET host and Xcode-built extension use separate
|
||
build systems, and Xcode needs the team to select the extension profile.
|
||
|
||
```bash
|
||
export VOICECAT_DEVELOPMENT_TEAM=FJV8L966W4
|
||
clients/apple/build-ios-device.sh --configuration Debug
|
||
clients/apple/deploy-ios-device.sh \
|
||
--device "Talon’s iPhone" \
|
||
--configuration Debug \
|
||
--no-build
|
||
```
|
||
|
||
The verified bundle is staged at `dist/ios-managed-device/VoiceCat.iOS.app`. Omit `--no-build`
|
||
to build and deploy in one command. Add `--console` to attach the launch to device logs.
|
||
|
||
The device build first validates the checked-in managed dependency locks, then generates
|
||
`obj/packages.device.lock.json` for the single `ios-arm64` app restore. This keeps a preceding
|
||
simulator restore from making the device build fail with `NU1004`; root-level generated lock
|
||
files under `clients/apple/VoiceCat.iOS` are not inputs to the device build.
|
||
|
||
.NET's iOS linker reports `IL2104` aggregate warnings for the pinned BouncyCastle and
|
||
Google.Protobuf assemblies. The iOS project leaves those aggregate warnings visible but does not
|
||
promote them to errors. All other warnings remain subject to the repository-wide
|
||
`TreatWarningsAsErrors` policy.
|
||
|
||
Confirm that the process stayed alive:
|
||
|
||
```bash
|
||
xcrun devicectl device info processes --device "Talon’s iPhone" | rg VoiceCat
|
||
```
|
||
|
||
## Signing findings
|
||
|
||
The development team ID is `FJV8L966W4`. Do not infer it from the parenthesized suffix in an
|
||
Apple Development certificate name; that value can identify the certificate holder and need
|
||
not equal a profile's `TeamIdentifier`.
|
||
|
||
The extension wrapper uses automatic signing with installed profiles. If a profile is absent
|
||
and Xcode has a valid signed-in developer account, permit Xcode to create or download it:
|
||
|
||
```bash
|
||
export VOICECAT_ALLOW_PROVISIONING_UPDATES=1
|
||
```
|
||
|
||
Leave that variable unset when valid profiles are already installed. A stale command-line Xcode
|
||
account can otherwise make provisioning updates fail even though offline signing can succeed.
|
||
|
||
## Troubleshooting
|
||
|
||
### Stale CMake source path
|
||
|
||
An old CMake cache can report a source-directory mismatch. Move the generated directories
|
||
aside and rebuild:
|
||
|
||
```bash
|
||
mv artifacts/native-build-ios-arm64-cmake /tmp/
|
||
mv artifacts/native-build-iossimulator-arm64-cmake /tmp/
|
||
```
|
||
|
||
### Locked restore reports changed runtime identifiers
|
||
|
||
The managed libraries' lock files must include `ios-arm64` and `iossimulator-arm64`. Regenerate
|
||
them after runtime changes, then verify that dependency versions did not change:
|
||
|
||
```bash
|
||
/usr/local/share/dotnet/dotnet restore \
|
||
clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj \
|
||
-p:VoiceCatIosStatic=true \
|
||
--force-evaluate
|
||
```
|
||
|
||
The device script should not require this command merely because a simulator was built first. If
|
||
it does, confirm that it is using `obj/packages.device.lock.json` for its `ios-arm64` restore.
|
||
|
||
### Command-line signing identity appears missing
|
||
|
||
Xcode being signed in and `security find-identity -v -p codesigning` returning no identities can
|
||
be a shell sandbox or keychain-access artifact. Run the identity check from an ordinary Terminal
|
||
session before changing certificates. A successful build prints the selected Apple Development
|
||
identity, provisioning profile, bundle ID, and app ID before linking.
|
||
|
||
### Install fails with `MissingBundleVersion`
|
||
|
||
CoreDevice error 3002 with `does not have a CFBundleVersion key with a non-zero length string
|
||
value` means the ReplayKit extension was built without `CURRENT_PROJECT_VERSION`. The extension
|
||
manifest expands that placeholder, and an empty value drops `CFBundleVersion` entirely, which
|
||
installd rejects.
|
||
|
||
`build-broadcast-extension.sh` now always passes both version placeholders, defaulting to `1`
|
||
and `0.0.1` to match `ApplicationVersion` and `ApplicationDisplayVersion` in the csproj, so a
|
||
plain Debug deployment needs no release environment variables. If this reappears, confirm the
|
||
script is not making `CURRENT_PROJECT_VERSION` conditional again, and check the staged bundle:
|
||
|
||
```bash
|
||
/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' \
|
||
dist/ios-managed-device/VoiceCat.iOS.app/PlugIns/VoiceCatBroadcast.appex/Info.plist
|
||
```
|
||
|
||
### Install fails on a dropped connection
|
||
|
||
CoreDevice error 4000 with `Connection reset by peer` is a transport failure, not a bundle or
|
||
signing problem. Rerun the deployment with `--no-build`.
|
||
|
||
### Developer disk image cannot be mounted
|
||
|
||
CoreDevice errors 10003 or 12040 mean the phone locked. Unlock it, keep the display awake, and
|
||
rerun deployment with `--no-build`.
|
||
|
||
CoreDevice can also install the app successfully and then reject only the launch with error
|
||
10002 and reason `Locked`. In that case, unlock the phone and rerun the same `--no-build`
|
||
deployment; rebuilding is unnecessary. Occasional CoreDeviceService initialization timeouts while
|
||
listing devices or processes do not imply that an already-confirmed install or launch failed.
|
||
|
||
## Verified hardware result
|
||
|
||
On 2026-09-22, the Debug build completed with .NET 10.0.401 and Xcode 27.0. The host and
|
||
ReplayKit extension were signed under team `FJV8L966W4` with matching versions, installed on
|
||
Talon's iPhone (iPhone 16 Pro Max), and launched as `me.iamtalon.voicecat`; the process was
|
||
confirmed running on the device. Audio, background/lock behavior, Bluetooth, ReplayKit,
|
||
ScreenCaptureKit, and VoiceOver remain separate manual hardware gates.
|