Email me

2026年10月24日の英語版原文から翻訳しました。数字と出典は原文と同じです。 英語の原文を読む →

開発 · · 10分で読めます · Matt Busel

カテゴリ: iOS と Swift アプリ開発 ガイド

Windows で iOS アプリを作る:Mac なしで21本をリリースした方法

Windows で iOS アプリを作る:Mac なしで21本をリリースした方法

要点:Mac の部分を分単位で借りれば、Windows でも iOS アプリを作れます。この方法で、iPhone アプリ21本を、机の上に Mac を置かずに Windows PC から App Store Connect に送りました。最初のアプリでは CI の実行が13回失敗しました。最後の10本はゼロです。パイプラインの全体、コード、そして実行を無駄にしたすべての罠を紹介します。

検索で知りたいのはそこだと思うので、短い答えから。はい、Windows から iPhone アプリをビルドしてリリースできます。Swift は PC で書き、コンパイル、署名、アップロードは一度も触らない macOS マシンが行います。そのマシンは GitHub Actions の macOS ランナーで、パブリックリポジトリなら GitHub は料金を取りません。

正直に言うと落とし穴もあります。Xcode は macOS でしか動かないので、Apple はやはりどこかに Mac を必要とします。有料の Apple Developer Program のメンバーシップ(年 $99)も必要です。それ以外は、ほとんどのガイドが「キーチェーンアクセスが必要」と言う署名証明書も含めて、App Store Connect API を通じて Windows のターミナルからすべて行えます。

2026年9月10日から10月8日までに、このパイプラインは 21個のアプリのリポジトリで App Store 向けビルドを139回実行しました。21本すべての CI ログにアップロードの記録があり、10月16日時点で 8本が米国 App Store で公開中です。仕組みと、どこで壊れたかを説明します。

パイプラインのアニメーション図:Windows PC が gh workflow run を実行し、GitHub の macOS ランナーが Xcode 26 を選んで52秒でコンパイル、署名証明書を1秒でインポート、続いて fastlane が2分31秒でビルドとアップロードを行い、合計4分22秒。
2026年10月8日、Chain 1.2 が Windows 上の1つのコマンドから App Store Connect に届くまで。各ステップの時間は GitHub Actions の実行ログより。

始める前に必要なもの

  • git、Python、GitHub CLI が入った Windows PC。エディタは何でもかまいません。どうせ Xcode をクリックして回ることはできないので、コーディングエージェントがとても役立ちます。
  • 有料の Apple Developer アカウント。
  • GitHub のリポジトリ。パブリックなら macOS の実行時間は無料です。プライベートでも動きますが、お金がかかります(金額は後述)。

人間にしかできないことが2つあります。どちらも一度だけです。

  1. App Store Connect API キーを作成する(「ユーザとアクセス」の「統合」)。.p8 ファイル、キー ID、発行者 ID(issuer ID)が手に入ります。これらは GitHub のシークレットに入れ、ほかのどこにも置きません。
  2. App Store Connect でアプリのレコードを作成する。API でアプリの読み取りや編集はできますが、私たちの POST /v1/apps は 403 が返ってきたので、これだけはウェブ UI でクリックします。それ以外の掲載情報(名前、サブタイトル、説明文、キーワード、スクリーンショット)はすべてスクリプト化しています。

ステップ1:.xcodeproj は絶対にコミットしない

Xcode のプロジェクトファイル(.pbxproj)は自動生成された巨大な塊で、手で編集するのは現実的ではありませんし、Windows では Xcode を開いて代わりに編集してもらうこともできません。だから持たないことにします。XcodeGen が、CI の実行のたびに最初に短い YAML ファイルからプロジェクトを生成します。これがすべてのアプリの出発点になった実際のテンプレートです(templates/project.yml、一部省略)。

name: MyApp
options:
  bundleIdPrefix: com.example
  deploymentTarget:
    iOS: "17.0"
settings:
  base:
    DEVELOPMENT_TEAM: ${DEVELOPMENT_TEAM}
    CODE_SIGN_STYLE: Manual
    MARKETING_VERSION: "1.0.0"
targets:
  MyApp:
    type: application
    platform: iOS
    sources:
      - path: Sources
    resources:
      - path: Resources
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.example.myapp
        GENERATE_INFOPLIST_FILE: YES
        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon

チーム ID は環境変数から渡すので、リポジトリに置かれることはありません。git にプロジェクトファイルがないということは、pbxproj のマージコンフリクトもないということで、エージェントがコードを編集するようになると、これが地味にかなり効いてきます。

ステップ2:Mac を分単位で借りる

Mac に関することはすべて1つのワークフローファイル .github/workflows/appstore.yml に収まっていて、Windows から1つのコマンドで起動します。

gh workflow run appstore.yml -f action=dry_run   # or compile, or release

compile はビルドして止まります。dry_run はビルド、署名をして App Store Connect にアップロードしますが、提出はしません。release は審査への提出まで行います。ランナーの罠2つで実際に実行を無駄にしました。どちらも今はファイル内にコメントとして書いてあります。

罠1:そこにない Xcode。macos-15 イメージでは /Applications/Xcode_26.app が存在するものの、中に Contents/Developer がない空の抜け殻でした。ls では普通に表示されるのに、選択すると失敗します。対策として、実際にツールチェーンを持っている最新の Xcode 26 を選びます。

chosen=""
for app in $(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -Vr); do
  if [ -d "$app/Contents/Developer" ]; then chosen="$app"; break; fi
done
sudo xcode-select -s "$chosen/Contents/Developer"

罠2:入れ替わるシミュレーター。デバイス名をハードコードしたら6回は動き、7回目に「Unable to find a device matching the provided destination specifier」で失敗しました。ランナー群が毎回同じシミュレーターを持っているとは限らないからです。今は利用可能な中で最新の iPhone を選びます。

name=$(xcrun simctl list devices available \
       | grep -oE "iPhone [0-9]+ Pro Max" | sort -Vr | head -1)
echo "SIM_NAME=$name" >> "$GITHUB_ENV"

費用。GitHub の料金ページによると、標準のホステッドランナーでの Actions はパブリックリポジトリなら無料です。プライベートリポジトリでは、3コアまたは4コアの macOS ランナーが1分 $0.062 です。私たちの macOS での実行322回(App Store、Build、審査用動画の各ワークフロー)は、キュー投入から完了までで合計約2,573分になり、プライベートならおよそ $160 かかった計算です。アプリのリポジトリはパブリックなので、支払いは $0 でした。Chain 1.2 のようなリリース1回(4分22秒)なら約27セントです。

ステップ3:Mac なしでの署名(壊れたのはここ)

コード署名は、ビルドがあなたから来たものだと Apple が確認する仕組みです。証明書(Apple が署名した公開鍵)とその秘密鍵、そしてそれらをアプリに結び付けるプロビジョニングプロファイルで構成されます。Mac なら Xcode がすべて隠してくれます。CI ランナーでは、最初のアプリが午前中をまるごと費やした場所でした。

2026年9月10日、GitHub Actions 上の Glyphstorm の App Store ワークフロー実行のスクリーンショット:実行10は午前5時49分に成功、実行11と実行13〜19は失敗、実行12はキャンセル、実行20は米東部夏時間の午前6時38分に成功。
2026年9月10日、午前5時49分〜6時38分(米東部時間)の Glyphstorm の App Store 実行(github.com/Mattbusel/glyphstorm、10月16日にキャプチャ)。

9月10日の赤い実行が実際に何と言っていたか、ログからそのまま載せます。

  • Your team has no devices from which to generate a provisioning profile. 自動署名はアーカイブ中に開発用プロファイルを要求し、開発用プロファイルには登録済みの iPhone が必要です。App Store 向けビルドにはデバイスは一切いらないのに、それでもこう言われます。
  • Could not find the newly generated certificate installed。fastlane の証明書ステップが、ランナー上で新しい証明書を作ってしまいました。
  • MAC verification failed during PKCS12 import (wrong password?)。パスワードは正しかったのです。間違っていたのはファイル形式でした。
  • Error resolving conflict between options: 'adhoc' and 'development'。これらのフラグを false で渡しても、渡したことになってしまいます。

2つ目が高くつく失敗です。CI が証明書を作ると、秘密鍵は数分後に消されるマシンの上で生まれます。Apple の側には、もう誰も使えない証明書が残り、しかも Apple は1つのアカウントが持てる配布用証明書の数を制限しています。誰かが気づく前に、失敗した3回の実行で、すでに3つの証明書が孤児になっていました。

CI に証明書を作らせてはいけません。鍵は自分のマシンで一度だけ作り、CI にはインポートだけをさせましょう。

そこで鍵は Windows で作ります。Store/signing.py が Python の cryptography ライブラリで鍵を生成し、Apple の証明書作成エンドポイントに署名リクエストを送り、.p12 を書き出します(一部省略)。

key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
csr = (x509.CertificateSigningRequestBuilder()
       .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Glyphstorm Distribution")]))
       .sign(key, hashes.SHA256()))
made = call("POST", "/v1/certificates", {"data": {"type": "certificates", "attributes": {
    "certificateType": "DISTRIBUTION",
    "csrContent": csr.public_bytes(serialization.Encoding.PEM).decode()}}})

# Legacy PKCS#12 (3DES + SHA-1). macOS `security import` can't read the AES-256 form
# and reports it as "MAC verification failed ... (wrong password?)".
encryption = (PrivateFormat.PKCS12.encryption_builder()
              .key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC)
              .hmac_hash(hashes.SHA1())
              .build(P12_PASSWORD.encode()))

スクリプトは base64 化した .p12 をそのまま gh secret set にパイプするので、コミットされうる場所に置かれることはありません。API の呼び出しには、.p8 で署名した短命の ES256 トークンを使います。Apple は有効期間が20分を超えるトークンを拒否するので、呼び出しのたびに新しく発行します。

ランナー側のインポートは、使い捨てのキーチェーンと、CI の署名ガイドなら太字で書くべき1行です。

security import "$RUNNER_TEMP/dist.p12" -k "$keychain" \
  -P "$DIST_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
# Without the partition list, codesign blocks on a GUI prompt that
# nobody can answer and the job hangs until it times out.
security set-key-partition-list -S apple-tool:,apple:,codesign: \
  -s -k "" "$keychain" >/dev/null

(すべてのフラグを知りたい方は security コマンドのリファレンスをどうぞ。)そのあとビルドは get_provisioning_profile で App Store 用プロファイルを直接要求して手動で署名します。これで、デバイスのないチームでは成功しようがない開発用プロファイルのステップを飛ばせます。格闘を始めてから8回の失敗と37分を経て、午前6時38分の実行が成功し、9月10日午前6時43分(米東部時間)にログが Successfully submitted the app for review! を出力しました。そのアプリの最初のコミットは午前4時06分でした。

ステップ4:退屈な部分は fastlane に任せる

fastlane は、Apple のビルドとアップロードの手順を「レーン」にまとめる Ruby 製のツールです。app_store_connect_api_key でシークレットから API キーを読むので、Apple ID のパスワードや2ファクタ認証のプロンプトはどこにも出てきません。ここにも罠がもう1つあります。ビルド番号です。同じビルド番号を2回アップロードすると Apple に拒否されるので、ビルドのたびに App Store Connect に最新の番号を問い合わせて1を足します(fastlane/Fastfile)。

next_build = latest_testflight_build_number(
  api_key: api_key,
  app_identifier: APP_IDENTIFIER,
  initial_build_number: 0
) + 1

番号はビルド設定(CURRENT_PROJECT_VERSION=#{next_build})として渡し、increment_build_number は使いません。こちらは Apple の agvtool を動かすもので、生成したプロジェクトでは Cannot find "MyApp.xcodeproj/../YES" で失敗しました。そのあと upload_to_app_store がバイナリ、テキスト、スクリーンショットを送ります。submit_for_review は release なら true、dry_run なら false です。

これがすべて揃うと、リリースは退屈になります。それこそが目標です。

Chain 1.2 の GitHub Actions ジョブのスクリーンショット:Xcode 26 の選択 4秒、xcodegen と fastlane のインストール 7秒、シミュレーターの選択 25秒、コンパイル 52秒、署名鍵の書き出し 0秒、署名証明書のインポート 1秒、ビルド・アップロード・提出 2分31秒。ジョブは4分9秒で成功。
Chain 1.2、2026年10月8日:ジョブ時間4分9秒、キューを含めて4分22秒。ステップ名は Build, upload and submit ですが、これは dry_run だったのでアップロード後に止まっています。

Mac なしで App Store のスクリーンショットを撮る

Apple は6.9インチサイズ(1320 × 2868)のスクリーンショットを求めます。Mac がなければシミュレーターをクリックして回ることもできないので、アプリが起動引数を受け取り、デモデータで該当画面を開くようにします。Chain の Sources/App.swift からの抜粋です。

func applyShotArgs(_ s: Store) {
    let a = ProcessInfo.processInfo.arguments
    guard let i = a.firstIndex(of: "-shot"), i + 1 < a.count else { return }
    switch a[i + 1] {
    case "year": tab = .year
    case "stats": tab = .stats
    case "rescue": rescue = s.rescues().first
    case "themes": shop = true
    default: break
    }
}

Build ワークフローは新しいシミュレーターを作り、ステータスバーをおなじみの 9:41、バッテリー満タンに設定してから、ループを回します。

for shot in today widgets year detail rescue themes week stats edit paywall onboarding done; do
  xcrun simctl terminate "$udid" com.mattbusel.chainhabits || true
  xcrun simctl launch "$udid" com.mattbusel.chainhabits -shot $shot
  sleep 8
  xcrun simctl io "$udid" screenshot "shots/$(printf %02d $i)-$shot.png"
  i=$((i+1))
done

新しいシミュレーターを作るのが大事です。ランナーのデフォルトのシミュレーターを起動したら、15分以上固まったことがありました。生の PNG はビルド成果物として戻ってきて、Windows 上の小さなスクリプトがそれぞれに見出しを載せます。

Chain の App Store スクリーンショット4枚:Five seconds a day、Watch your year fill in、Missed a day? Save the chain、Know your best days。どれもダークなアプリ UI にライムグリーンの習慣マスが並んでいる。
Chain の App Store スクリーンショット10枚のうち4枚:CI のシミュレーターで撮った生のキャプチャに、Windows 上のスクリプトで見出しを追加。

ここで1つ意見を。ストアに出すスクリーンショットはリポジトリにコミットし、release レーンでは撮り直しません。Apple が見る画像は人間が確認したものとまったく同じになり、署名の問題でリトライするたびに10分短く済みます。

139回の実行から学んだこと

GitHub Actions API からすべての実行を取得すると、きれいな学習曲線が見えます。21個のリポジトリで App Store ワークフローの実行は139回、成功81回、失敗43回、キャンセル15回でした。成功した実行の中央値は、キュー投入から完了まで約8分です。

アプリごとの App Store ワークフローの失敗回数をローンチ順に並べた棒グラフ:Glyphstorm 13、Clear the Strait 13、Pocket Beings 5、Minder 3、Fairway Ledger 2、Baseline Ledger 2、Ironbook 2、Race Fuel 1、Chain 1、Quiver 0、Rooms 1、続いて Clockout、Odometer、Curve、Pricebook、Pawprint、Chores、Climbbook、Cutoff、Parked、Race Pace はすべて0。
アプリごとの App Store ワークフローの失敗回数(全期間)、アプリは最初の実行順(2026年9月10日〜10月8日)。出典:GitHub Actions API、2026年10月16日取得。

最初の2本、Glyphstorm と Clear the Strait はそれぞれ13回失敗し、その大半が9月10日でした。1日遅れで始めた Pocket Beings は合計5回。9月24日の一群ではほぼゼロになり、最後の10本は App Store 向けの実行で一度も失敗せずに出荷できました。Climbbook、Cutoff、Parked、Race Pace は、9月27日にそれぞれ最初の実行で App Store Connect に届いています。同じパイプライン、ファイル内の同じコメント。どの修正も、次のアプリが読む場所に書き残してありました。

ただし、どんでん返しは本当にあります。失敗ゼロだったアプリこそ、App Review に押し返されたアプリなのです。10月5日、Apple はそのうち11本を Guideline 4.3(スパム)と 4.2.6 でリジェクトしました。私の解釈:出荷にコストがかからなくなると、ボトルネックは CI から「このアプリは掲載に値するほど違っているか」に移ります。パイプラインはその問いには答えられません。答えるのはプロダクトです。

新しいアカウントは、最初の提出で Guideline 2.1 の「Information Needed(情報が必要)」が来ることも想定しておきましょう。私たちの最初の2本はどちらも受けました。アプリの短い画面収録があれば答えになりますし、それも同じランナーで録画できます。

今日、自分のプロジェクトでやること

  1. git から .xcodeproj を削除し、XcodeGen 用の project.yml を書く。生成は CI で行う。
  2. 証明書は自分のマシンで API を通じて作成し、レガシー形式の .p12 として保存して、GitHub のシークレットに入れる。CI がすでに作ってしまったものがあれば、一覧を出して孤児を失効させる。
  3. CI では:使い捨てのキーチェーン、インポート、set-key-partition-list。最後の1つを飛ばすと確実に固まります。
  4. App Store 用プロファイルで手動署名する。持っていないデバイスを自動署名に探させないこと。
  5. ビルド番号 = 最新の TestFlight ビルド + 1、ビルド設定として渡す。
  6. Xcode のパスやシミュレーター名をハードコードしない。実行時に、実在する最新のものを選ぶ。
  7. -shot 起動引数を追加して、CI が App Store のスクリーンショットを撮れるようにする。
  8. 掲載内容が正しく見えるまでは dry_run を使い、それから release。

この方法で SwiftUI アプリを作るなら、Minder のスプリングアニメーションの記事が、このパイプラインから生まれたアプリの1つです。また、コーディングエージェントに CI のループを回させるなら、先にコーディングエージェントにテストを書き換えさせない方法と1台の PC で20体のエージェントを動かして壊れたものを読んでください。朝6時に署名を直そうとしているエージェントは、喜んでチェックのほうを「直して」しまうからです。

パイプライン一式(すべての罠をコメントしたワークフロー、Fastfile、signing.py、App Store Connect 用のスクリプト、手順を案内するエージェントスキル)は Ship iOS from Windows キットとしてまとめています。アプリそのものを誰かに作ってほしい場合は、こちらが私たちの固定価格です。

まだ Mac が必要だと思う Apple の手順は何ですか。教えてもらえれば、API でできるかどうか確かめます。

2026年10月16日に tensorust.com で初公開。X(@TensorustAI)と dev.to にも掲載しています。関連:このパイプラインをあなたのリポジトリにセットアップします:$299、固定価格の iPhone アプリ開発、iPhone アプリ一覧、Ship iOS from Windows キット。

次に読む

すべての記事 →

日本語のほかのページ

日本語: Tensorust →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ