iPhoneアプリ開発
SwiftUIアプリを空のリポジトリから実機で動作する署名済みビルドにまで仕上げるためのエンドツーエンドの規約です。
前提: 小規模アプリ、無料の個人Appleアカウント、App Store配信なしを想定しています。有料の開発者チームの場合、以下のルールの一部が変わります — 署名セクションで該当箇所を明示します。
アーキテクチャのデフォルト
- SwiftUI + SwiftData、ローカルファースト。 小規模アプリはサーバーに常時接続している必要はありません。データはデバイス上のSwiftDataに保存し、ネットワーク呼び出しは公開APIのみに使用します(https、ATS例外なし)。アプリが既存のバックエンドのシンクライアントとして始まった場合、"サーバーを廃止できるか?"という方向転換が発生することを想定してください — サーバーのロジックを、SwiftData配列に対する純粋なSwift関数に移植し、一台の電話のためにサーバーを生かし続ける必要をなくします。
- SwiftDataモデリング: 整数の外部キーではなく、実際のオブジェクト間リレーションシップを使用します。自然キーには
@Attribute(.unique)を使用します。SwiftDataのto-many配列は順序を保持しないため、順序が重要な場合はsortOrderフィールドとsortedX計算プロパティを追加します。日付は文字列ではなくDate型を使用します。EnumはStringローのCodableを使用します。 - 動作中のインストールでのスキーマ変更: モデルやオプショナルプロパティの追加は、次回起動時に軽量マイグレーションが静かに実行されます。リネーム、削除、型変更を
SchemaMigrationPlanなしで行うと、ストアを開けなくなり、デプロイ済みアプリはユーザーのデータを保持したまま起動時にクラッシュします(データは使用できません)。再デプロイ(同じバンドルID)ではデータコンテナは決して削除されません。アプリを削除する場合のみ削除されます。ユーザーが実際のデータを蓄積する前に、エクスポート/バックアップのパスを計画してください。 - Viewは直接
@Query+modelContextにアクセスします。 本当にキャッシュが必要な場合以外は、Storeやリポジトリ層は使用しません。純粋なロジック(マッチング、パース、インポート)はenum名前空間(enum Foo { static func … })に配置し、UIなしでテスト可能にします。 - デプロイメントターゲット: 必要なAPIが特に指定しない限り、現在のメジャーバージョンの1つ前をターゲットにします。ターゲットは1つ、
TARGETED_DEVICE_FAMILY: "1"、ポートレートを基本とし、アプリが必要とする場合のみ回転を許可します。
プロジェクト生成 — XcodeGen、.xcodeprojはコミットしない
ios/project.yml を信頼できる唯一の情報源とし、.xcodeproj バンドルは生成してgitignoreします(ios/build/ および生成された Info.plist もgitignore)。ファイルの追加や削除後に再生成します — xcodegenはソースディレクトリをグロブするため、既存ファイルの編集には再生成は不要です。
- xcodegenはGitHubリリースのzipからインストールします(
PREFIX=~/.local ./install.sh) — Homebrewは不要で、制限されたMacでも動作します。 - 単体テストターゲットには
GENERATE_INFOPLIST_FILE: YESが必要です。これがないとテストバンドルが署名に失敗します。 project.ymlでスキームを定義し、gatherCoverageData: trueとテストターゲットをアタッチします。そうしないとxcodebuild testが実行するものがありません。
開発ループ — CLIのみ、デフォルトはシミュレータ
すべてのiOSプロジェクトには以下のMakefileターゲットがあります。シミュレータ名、デバイスID、チームIDは ?= 変数でパラメータ化し、マシン固有の値がハードコードされないようにします:
| ターゲット | 内容 |
|---|---|
ios-project | xcodegen generate + Xcodeで開く |
ios-sim | シミュレータ用に署名なしビルド、シミュレータ起動、インストール、起動 — デフォルトのループ。署名不要、パスワードプロンプトなし |
ios-sim-seed | ios-simを実行後、--seed-demo で再起動 |
ios-test | シミュレータで xcodebuild test + xccov によるターゲット別カバレッジレポート |
ios-deploy | 接続された実機用の署名済みビルド + devicectl でのインストール + 起動 |
ios-test は実行ごとにカバレッジを表示します — ハウスルールとして、make test は常に数値を表示しますが、ハードなしきい値は設定しません。
シミュレータビルド: generic/platform=iOS コンパイルチェックでは CODE_SIGNING_ALLOWED=NO で問題ありません。シミュレータの宛先には署名はまったく不要です。シミュレータと実機では別々のデータベースがあることを忘れないでください。
常に役立つデバッグの便利機能
- サンプルデータシーダー(
DemoData.swift、#if DEBUG): 有効期限/日付フィールドが.nowを基準に計算されたキュレーションされたデータセットで、べき等(再実行しても何も追加されない)。3つの方法で公開 — 設定アプリの「デベロッパ」セクションボタン、ProcessInfo.processInfo.argumentsでチェックされる--seed-demo起動引数、およびios-sim-seedターゲット。シードがアプリの主要な状態をカバーしていることを確認するテストを追加し、静かに壊れるのを防ぎます。 - ホイールピッカーよりも検索可能なピッカー — リストが数十エントリを超える可能性がある場合(インポートされたデータセットではほぼ確実)。
- カメラ/スキャナー機能:
DataScannerViewController.isSupported && .isAvailableをチェックし、手動入力のフォールバックを同梱します — シミュレータでも正常に動作します。
テスト
- シミュレータでXCTestを実行。他のスタックからロジックを移植する場合は、参照テストスイートも移植し、言語間でセマンティクスを固定します。
- 共有ヘルパー:
isStoredInMemoryOnly: trueでModelContainerを構築するmakeInMemoryContext()。@MainActorテストクラスとsetUp() async throws。 - 結果バンドルから合格/不合格を読み取る:
xcrun xcresulttool get test-results summary --path build/test.xcresult。
署名とデプロイ — 落とし穴(いずれも実際に時間を費やしました)
- チームIDは
security find-identityの括弧内の値ではありません — それは証明書自身のIDです。実際のチームIDはプロビジョニングプロファイルから取得します(security cms -D -i <profile>→application-identifierプレフィックス)またはXcodeのAccountsペインから取得します。間違ったIDを使用すると、「No Account for Team」という誤解を招くエラーが発生します。 -allowProvisioningUpdatesはプレーンなターミナルからApple IDセッションを認識できません。 プロビジョニングプロファイルはXcode GUI(Signing & Capabilities → Team設定 → Try Again)で一度作成します。その後、純粋なCLIビルドはDEVELOPMENT_TEAM=<id> CODE_SIGN_STYLE=Automaticで動作します。- 初回デバイス設定の流れ: 実機でDeveloper Modeを有効にする(設定 → プライバシーとセキュリティ;このトグルはXcodeがデバイスと通信した後にのみ表示されます;再起動が必要)→ コンピュータを信頼する → 初回インストール後、開発者証明書を信頼する(設定 → 一般 → VPNとデバイス管理)。Xcodeの「your team has no devices」エラーは通常、デバイスの登録試行が、デバイスの再起動中またはロック中に行われたことを示します — 実機を実行宛先として選択し、再試行してください。
- ビルドごとにキーチェーンプロンプトが表示される: codesignキーチェーン画面で一度常に許可をクリックすると、ビルドごとのパスワードプロンプトが表示されなくなります。
- 無料個人チームの制限: インストールは7日で期限切れになります(再デプロイして再署名)。最大3つのサイドロードアプリ、プッシュ通知なし、App Groupsなし。有料チームはこれら4つすべてを解除します — アプリにプッシュ通知やApp Groupsが必要な場合、それが有料チームを取得するトリガーです。
- デプロイ/起動CLI:
xcrun devicectl list devices、xcrun devicectl device install app --device <id> <path>.app、… device process launch --device <id> <bundle-id>。
新品Macのセットアップ(ヘッドレスでも対応)
- App StoreからXcodeをインストール(
macappstore://apps.apple.com/app/id497799835を開く)。 sudo xcode-select -s /Applications/Xcode.app && sudo xcodebuild -license accept && sudo xcodebuild -runFirstLaunch— 管理者権限が必要です。標準アカウントの場合は、まず管理者メンバーシップを修正します(su admin -c 'sudo dseditgroup -o edit -a <user> -t user admin')。xcodebuild -downloadPlatform iOS— iOS SDKとシミュレータランタイムは、Xcode 15以降、別の数GBのダウンロードになります。これがないと、「iOS X.Y is not installed」というエラーでビルドが失敗します。DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developerをコマンドごとのオーバーライドとして使用し、xcode-selectが修正される前に使用できます。- リリースzipからxcodegenを
~/.localにインストール(上記参照)。
既存サーバーアプリのコンパニオンクライアント — 同期階層
iPhoneアプリが既存のサーバー + Webアプリを持つアプリに加わる場合(サーバーが単一の情報源であり続ける)、電話のユースケースが許す最も安価な階層を選択します。まず「電話はサーバーのネットワーク外でも動作する必要があるか?」と「キャプチャか、完全な編集か?」を尋ねます:
- シンクライアント — ローカルストアなし。電話は既存のAPIを呼び出します。接続が確実な場合に適しています。
- スナップショットミラー — 完全なJSONプルからSwiftDataキャッシュを一括で再構築します(シングルユーザーDBは小さい)。マージなし → トゥームストーンなし、行ごとのタイムスタンプなし、競合エンジンなし。サーバー側の削除/編集は無料で反映されます。写真はURLキャッシュを介して遅延ロードします。
- スコープ付きアウトボックス — オフラインでの作成を、すでに存在するレコードの子(既知の親に対するログ、メモ、写真)に制限します。親は実際のサーバーIDを持つため、リプレイは単純なAPI呼び出しになります — 整数PKスキーマにはUUIDマイグレーションは不要です。同期イベント = アウトボックスをプッシュ、次にスナップショットをプル(自分のキャプチャは確認済みで戻ってくる)。404-孤児(「N は同期できませんでした」)を表示し、決して静かにドロップしないでください。保留中バッジ + 最終同期タイムスタンプを表示します。ユーザーが固定したサーバーアドレスが応答する場合にのみ自動同期します — 外部ネットワークをプローブしないでください。
- 完全双方向同期(UUID、トゥームストーン、デルタエンドポイント、LWW/CRDT) — 階層1〜3で不十分な証拠がある場合にのみ使用します。この再作業はサーバー側(クライアントが発行可能なUUID、べき等なアップサート、
changes?since=+ トゥームストーン)から始まり、Swiftコードを書く前に行います。
階層2と3はうまく組み合わさります。読み取り用のスナップショットミラーとキャプチャ用のスコープ付きアウトボックスで、「自宅で動作し、どこでもキャプチャ」を階層4のメカニズムなしでカバーできます。
プロジェクトの衛生状態
- マシン固有およびデバイス固有の情報(チームID、デバイスUDID、シミュレータ名)は、Makefileの
?=変数またはプロジェクト自身のメモに記述します — スクリプトにハードコードしたり、このスキルに記述したりしないでください。 - 進行中の作業スライスごとに
plans/<topic>.mdドキュメントを保持し、リリースした内容を追跡するためにトップレベルのCHANGELOG.mdを維持します。スライスが完了したら、plans/done/<topic>.mdに移動し、「Delivered in <バージョン> — <日付>」ヘッダーを追加します。 - 各スライスのサイズは、実機またはシミュレータで実行可能なもので終わるように設定します。起動できないiOSスライスはレビューできません。