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 で公開中です。仕組みと、どこで壊れたかを説明します。

始める前に必要なもの
- git、Python、GitHub CLI が入った Windows PC。エディタは何でもかまいません。どうせ Xcode をクリックして回ることはできないので、コーディングエージェントがとても役立ちます。
- 有料の Apple Developer アカウント。
- GitHub のリポジトリ。パブリックなら macOS の実行時間は無料です。プライベートでも動きますが、お金がかかります(金額は後述)。
人間にしかできないことが2つあります。どちらも一度だけです。
- App Store Connect API キーを作成する(「ユーザとアクセス」の「統合」)。
.p8ファイル、キー ID、発行者 ID(issuer ID)が手に入ります。これらは GitHub のシークレットに入れ、ほかのどこにも置きません。 - 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 ランナーでは、最初のアプリが午前中をまるごと費やした場所でした。

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 です。
これがすべて揃うと、リリースは退屈になります。それこそが目標です。

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 上の小さなスクリプトがそれぞれに見出しを載せます。

ここで1つ意見を。ストアに出すスクリーンショットはリポジトリにコミットし、release レーンでは撮り直しません。Apple が見る画像は人間が確認したものとまったく同じになり、署名の問題でリトライするたびに10分短く済みます。
139回の実行から学んだこと
GitHub Actions API からすべての実行を取得すると、きれいな学習曲線が見えます。21個のリポジトリで App Store ワークフローの実行は139回、成功81回、失敗43回、キャンセル15回でした。成功した実行の中央値は、キュー投入から完了まで約8分です。

最初の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本はどちらも受けました。アプリの短い画面収録があれば答えになりますし、それも同じランナーで録画できます。
今日、自分のプロジェクトでやること
- git から .xcodeproj を削除し、XcodeGen 用の
project.ymlを書く。生成は CI で行う。 - 証明書は自分のマシンで API を通じて作成し、レガシー形式の
.p12として保存して、GitHub のシークレットに入れる。CI がすでに作ってしまったものがあれば、一覧を出して孤児を失効させる。 - CI では:使い捨てのキーチェーン、インポート、set-key-partition-list。最後の1つを飛ばすと確実に固まります。
- App Store 用プロファイルで手動署名する。持っていないデバイスを自動署名に探させないこと。
- ビルド番号 = 最新の TestFlight ビルド + 1、ビルド設定として渡す。
- Xcode のパスやシミュレーター名をハードコードしない。実行時に、実在する最新のものを選ぶ。
-shot起動引数を追加して、CI が App Store のスクリーンショットを撮れるようにする。- 掲載内容が正しく見えるまでは
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 キット。
次に読む
- 開発 · 2026年10月19日 · 15分で読めますApple の 4.3 スパム判定は30秒のバッチ処理。22件のリジェクトがそれを証明している
- 開発 · 2026年10月18日 · 29分で読めますWindows での iOS 開発:完全ガイド(アプリ21本、Mac なし)
- 開発 · 2026年10月18日 · 13分で読めますApp Store の審査時間:実際の判定64件、中央値73時間