Email me

Übersetzt aus dem englischen Original vom 24. Okt. 2026. Zahlen und Quellen entsprechen dem Original. Englisches Original lesen →

Dev · · 10 Min. Lesezeit · Matt Busel

Abgelegt unter iOS und Swift App-Entwicklung Anleitungen

Eine iOS-App unter Windows bauen: wie 21 Apps ohne Mac veröffentlicht wurden

Eine iOS-App unter Windows bauen: wie 21 Apps ohne Mac veröffentlicht wurden

Kurz gesagt: Du kannst eine iOS-App unter Windows bauen, wenn du den Mac-Teil minutenweise mietest. 21 iPhone-Apps sind so von einem Windows-PC zu App Store Connect gelangt, ohne Mac auf dem Schreibtisch. Die erste App brauchte 13 fehlgeschlagene CI-Läufe. Die letzten zehn brauchten keinen einzigen. Hier ist die komplette Pipeline, der Code und jede Falle, die einen Lauf gekostet hat.

Zuerst die kurze Antwort, denn danach hast du gesucht: Ja, du kannst eine iPhone-App unter Windows bauen und veröffentlichen. Du schreibst das Swift auf deinem PC, und eine macOS-Maschine, die du nie anfasst, übernimmt Kompilieren, Signieren und Hochladen. Diese Maschine ist ein macOS-Runner von GitHub Actions, und bei einem öffentlichen Repo berechnet GitHub dafür nichts.

Der ehrliche Haken: Apple braucht trotzdem irgendwo einen Mac, denn Xcode läuft nur unter macOS. Außerdem brauchst du eine kostenpflichtige Mitgliedschaft im Apple Developer Program (99 $ pro Jahr). Alles andere, inklusive des Signaturzertifikats, für das laut den meisten Anleitungen die Schlüsselbundverwaltung nötig ist, lässt sich aus einem Windows-Terminal über die App Store Connect API erledigen.

Zwischen dem 10. September und dem 8. Oktober 2026 hat diese Pipeline 139 App-Store-Builds über 21 App-Repos ausgeführt. Jede der 21 Apps hat einen Upload in ihrem CI-Log, und 8 sind live im US App Store, Stand 16. Oktober. So funktioniert es, und hier ist es gebrochen.

Animiertes Diagramm der Pipeline: Ein Windows-PC führt gh workflow run aus, ein macOS-Runner von GitHub wählt Xcode 26 und kompiliert in 52 Sekunden, importiert das Signaturzertifikat in 1 Sekunde, dann baut fastlane und lädt in 2 Minuten 31 Sekunden hoch, insgesamt 4 Minuten 22 Sekunden.
Wie Chain 1.2 am 8. Okt. 2026 mit einem einzigen Befehl unter Windows zu App Store Connect kam. Schrittzeiten aus dem Laufprotokoll von GitHub Actions.

Was du brauchst, bevor du loslegst

  • Einen Windows-PC mit git, Python und der GitHub CLI. Einen beliebigen Editor. Ein Coding-Agent hilft sehr, da du dich ohnehin nicht durch Xcode klicken kannst.
  • Einen kostenpflichtigen Apple-Entwickler-Account.
  • Ein GitHub-Repo. Öffentlich heißt kostenlose macOS-Minuten. Privat geht auch, kostet nur Geld (Zahlen unten).

Zwei Dinge kann nur ein Mensch erledigen, jeweils einmal:

  1. Einen App Store Connect API-Key erstellen (Benutzer und Zugriff, Integrationen). Du bekommst eine .p8-Datei, eine Key-ID und eine Issuer-ID. Die kommen in GitHub Secrets und nirgendwo sonst hin.
  2. Den App-Eintrag anlegen in App Store Connect. Die API kann Apps lesen und bearbeiten, aber unser POST /v1/apps kam mit 403 zurück, also ist das ein Klick in der Weboberfläche. Jedes andere Listing-Feld (Name, Untertitel, Beschreibung, Keywords, Screenshots) ist geskriptet.

Schritt 1: Committe nie ein .xcodeproj

Eine Xcode-Projektdatei (.pbxproj) ist ein riesiger generierter Blob, den du nicht vernünftig von Hand bearbeiten kannst, und unter Windows kannst du auch kein Xcode öffnen, das es für dich tut. Also behalte keine. XcodeGen baut das Projekt zu Beginn jedes CI-Laufs aus einer kurzen YAML-Datei. Das ist die echte Vorlage, mit der jede App angefangen hat (templates/project.yml, gekürzt):

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

Die Team-ID kommt aus einer Umgebungsvariable, liegt also nie im Repo. Keine Projektdatei in git heißt auch keine Merge-Konflikte in einer pbxproj, und das ist ehrlich gesagt ziemlich wichtig, sobald Agents am Code arbeiten.

Schritt 2: Miete einen Mac minutenweise

Der gesamte Mac steckt in einer einzigen Workflow-Datei, .github/workflows/appstore.yml, die du unter Windows mit einem Befehl startest:

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

compile baut und hört auf. dry_run baut, signiert und lädt zu App Store Connect hoch, ohne einzureichen. release reicht zusätzlich zum Review ein. Zwei Runner-Fallen haben echte Läufe gekostet, und beide stehen jetzt als Kommentare in der Datei.

Falle 1: das Xcode, das nicht da ist. Auf dem Image macos-15 existierte /Applications/Xcode_26.app, war aber ein Platzhalter ohne Contents/Developer darin. Die Auswahl schlug also fehl, während ls es fröhlich auflistete. Der Fix wählt das neueste Xcode 26, das tatsächlich eine Toolchain hat:

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"

Falle 2: Simulatoren, die wandern. Ein fest eingetragener Gerätename funktionierte sechs Läufe lang und scheiterte beim siebten mit "Unable to find a device matching the provided destination specifier", weil die Runner-Flotte nicht jedes Mal dieselben Simulatoren an Bord hatte. Jetzt gewinnt das neueste verfügbare iPhone:

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

Was es kostet. Laut GitHubs Abrechnungsseite sind Actions für öffentliche Repos auf Standard-Hosted-Runnern kostenlos. Bei einem privaten Repo kostet ein macOS-Runner mit 3 oder 4 Kernen 0,062 $ pro Minute. Unsere 322 macOS-Läufe (Workflows für App Store, Build und Review-Video) summieren sich auf etwa 2.573 Minuten von Warteschlange bis Ende, was privat ungefähr 160 $ wären. Wir haben 0 $ bezahlt, weil die App-Repos öffentlich sind. Ein einzelnes Release wie Chain 1.2 (4 min 22 s) läge bei etwa 27 Cent.

Schritt 3: Signieren ohne Mac (der Teil, der gebrochen ist)

Code-Signierung ist die Art, wie Apple weiß, dass ein Build von dir stammt: ein Zertifikat (von Apple signierter öffentlicher Schlüssel) plus dessen privater Schlüssel und ein Provisioning-Profil, das beides an die App bindet. Auf einem Mac versteckt Xcode das alles. Auf einem CI-Runner hat die erste App genau damit ihren Vormittag verbracht.

Screenshot der App-Store-Workflow-Läufe von Glyphstorm in GitHub Actions, 10. Sep. 2026: Lauf 10 grün um 5:49 Uhr, Lauf 11 und die Läufe 13 bis 19 fehlgeschlagen, Lauf 12 abgebrochen, Lauf 20 grün um 6:38 Uhr EDT.
Die App-Store-Läufe von Glyphstorm am 10. Sep. 2026, 5:49 bis 6:38 Uhr ET (github.com/Mattbusel/glyphstorm, aufgenommen am 16. Okt.).

Das stand in diesen roten Läufen vom 10. September tatsächlich, direkt aus den Logs:

  • Your team has no devices from which to generate a provisioning profile. Automatisches Signieren verlangt beim Archivieren ein Development-Profil, und ein Development-Profil braucht ein registriertes iPhone. Ein App-Store-Build braucht überhaupt kein Gerät, aber diese Meldung bekommst du trotzdem.
  • Could not find the newly generated certificate installed. Der Zertifikatsschritt von fastlane hat auf dem Runner ein frisches Zertifikat erzeugt.
  • MAC verification failed during PKCS12 import (wrong password?). Das Passwort war in Ordnung. Das Dateiformat nicht.
  • Error resolving conflict between options: 'adhoc' and 'development'. Auch wer diese Flags als false übergibt, übergibt sie.

Der zweite Punkt ist der teure Fehler. Wenn die CI ein Zertifikat erstellt, entsteht der private Schlüssel auf einer Maschine, die Minuten später gelöscht wird. Apple behält ein Zertifikat, das niemand je wieder nutzen kann, und Apple begrenzt, wie viele Distributionszertifikate ein Account haben darf. Drei fehlgeschlagene Läufe hatten schon drei davon verwaist, bevor es jemand bemerkte.

Lass die CI niemals ein Zertifikat erstellen. Erzeug den Schlüssel einmal auf deiner eigenen Maschine und lass die CI ihn nur importieren.

Also wird der Schlüssel unter Windows erzeugt. Store/signing.py generiert ihn mit der Python-Bibliothek cryptography, schickt eine Signieranfrage an Apples Endpunkt create certificate und schreibt eine .p12 (gekürzt):

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()))

Das Skript leitet die base64-kodierte .p12 direkt in gh secret set, sie landet also nie irgendwo, wo sie committet werden könnte. Die API-Aufrufe nutzen ein kurzlebiges ES256-Token, das mit der .p8 signiert ist. Apple lehnt Tokens ab, die länger als 20 Minuten gültig sind, daher wird pro Aufruf ein frisches erzeugt.

Auf dem Runner besteht der Import aus einem Wegwerf-Schlüsselbund plus einer Zeile, die jede Anleitung zum CI-Signieren fett drucken sollte:

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

(Die Referenz zum Befehl security, falls du jedes Flag sehen willst.) Danach fordert der Build das App-Store-Profil direkt mit get_provisioning_profile an und signiert manuell. So entfällt der Schritt mit dem Development-Profil, der bei einem Team ohne Geräte nie gelingen kann. Acht fehlgeschlagene Läufe und 37 Minuten nach Beginn des Kampfs wurde der Lauf von 6:38 Uhr grün, und um 6:43 Uhr ET am 10. September stand im Log Successfully submitted the app for review!. Der erste Commit dieser App war um 4:06 Uhr.

Schritt 4: fastlane erledigt den langweiligen Teil

fastlane ist ein Ruby-Tool, das Apples Build- und Upload-Schritte in "Lanes" verpackt. Es liest den API-Key per app_store_connect_api_key aus dem Secret, es gibt also nirgends ein Apple-ID-Passwort oder eine Zwei-Faktor-Abfrage. Hier lauert noch eine Falle: Build-Nummern. Lädst du dieselbe Build-Nummer zweimal hoch, lehnt Apple ab, also fragt jeder Build App Store Connect nach der neuesten und addiert eins (fastlane/Fastfile):

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

Sie wird als Build-Setting übergeben (CURRENT_PROJECT_VERSION=#{next_build}), nicht über increment_build_number, das Apples agvtool ansteuert und bei einem generierten Projekt mit Cannot find "MyApp.xcodeproj/../YES" scheiterte. Dann schickt upload_to_app_store das Binary, die Texte und die Screenshots, mit submit_for_review auf true für release und false für dry_run.

Wenn all das steht, ist ein Release langweilig, und genau das ist das Ziel:

Screenshot des GitHub-Actions-Jobs für Chain 1.2: Xcode 26 auswählen 4s, xcodegen und fastlane installieren 7s, Simulator wählen 25s, kompilieren 52s, Signaturschlüssel schreiben 0s, Signaturzertifikat importieren 1s, bauen, hochladen und einreichen 2m 31s. Job erfolgreich in 4m 9s.
Chain 1.2, 8. Okt. 2026: 4m 9s Job-Zeit, 4m 22s mit Warteschlange. Der Schritt heißt Build, upload and submit; dies war ein dry_run, daher hörte er nach dem Upload auf.

App-Store-Screenshots ohne Mac

Apple will Screenshots in 6,9 Zoll (1320 mal 2868). Ohne Mac kannst du dich nicht durch einen Simulator klicken, also nimmt die App ein Startargument entgegen und öffnet den richtigen Screen mit Demodaten. Das stammt aus Sources/App.swift von Chain:

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
    }
}

Der Build-Workflow erstellt dann einen frischen Simulator, setzt die Statusleiste auf die klassische 9:41 mit vollem Akku und läuft in einer Schleife:

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

Ein frischer Simulator ist wichtig: Das Booten des Standard-Simulators auf dem Runner hing einmal über 15 Minuten. Die rohen PNGs kommen als Build-Artefakt zurück, und ein kleines Skript unter Windows setzt über jedes eine Überschrift.

Vier App-Store-Screenshots von Chain: Fünf Sekunden am Tag, Sieh zu, wie sich dein Jahr füllt, Einen Tag verpasst? Rette die Kette, und Kenne deine besten Tage, jeweils mit der dunklen App-Oberfläche und limettengrünen Gewohnheitskästchen.
Vier der zehn App-Store-Screenshots von Chain: Rohaufnahmen aus dem CI-Simulator, Überschriften per Skript unter Windows hinzugefügt.

Eine Meinung dazu: Die Screenshots, die veröffentlicht werden, sind ins Repo committet, und die release-Lane nimmt sie nie neu auf. Die Bilder, die Apple sieht, sind exakt die, die ein Mensch angeschaut hat, und jeder erneute Versuch bei einem Signierproblem ist zehn Minuten kürzer.

Was uns 139 Läufe gelehrt haben

Wenn du jeden Lauf aus der GitHub Actions API ziehst, ergibt sich eine saubere Lernkurve. Über 21 Repos gab es 139 App-Store-Workflow-Läufe: 81 erfolgreich, 43 fehlgeschlagen, 15 abgebrochen. Der mittlere erfolgreiche Lauf dauerte etwa 8 Minuten von Warteschlange bis Ende.

Balkendiagramm der fehlgeschlagenen App-Store-Workflow-Läufe pro App in Launch-Reihenfolge: 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, danach Clockout, Odometer, Curve, Pricebook, Pawprint, Chores, Climbbook, Cutoff, Parked und Race Pace alle 0.
Fehlgeschlagene App-Store-Workflow-Läufe pro App, gesamter Zeitraum, Apps in der Reihenfolge ihres ersten Laufs (10. Sep. bis 8. Okt. 2026). Quelle: GitHub Actions API, abgerufen am 16. Okt. 2026.

Die ersten beiden Apps, Glyphstorm und Clear the Strait, verschlangen je 13 Fehlschläge, die meisten davon am 10. September. Pocket Beings, einen Tag später gestartet, hatte insgesamt 5. Beim Batch vom 24. September war es meist null, und die letzten zehn Apps gingen ohne einen einzigen fehlgeschlagenen App-Store-Lauf raus. Climbbook, Cutoff, Parked und Race Pace gingen am 27. September jeweils mit ihrem ersten Lauf zu App Store Connect. Gleiche Pipeline, gleiche Kommentare in der Datei. Jeder Fix wurde dort aufgeschrieben, wo die nächste App ihn lesen würde.

Die Wendung gibt es allerdings wirklich. Genau die Apps ohne Fehlschläge sind die, bei denen App Review zurückgeschossen hat. Am 5. Oktober lehnte Apple 11 davon nach Guideline 4.3 (Spam) und 4.2.6 ab. Meine Einschätzung: Sobald Veröffentlichen nichts mehr kostet, wandert der Engpass von der CI zur Frage "Ist diese App unterschiedlich genug, um ein Listing zu verdienen?". Die beantwortet dir keine Pipeline. Das muss dein Produkt tun.

Neue Accounts sollten bei der ersten Einreichung außerdem mit einem Guideline 2.1 "Information Needed" rechnen. Unsere ersten beiden Apps bekamen beide eins. Eine kurze Bildschirmaufnahme der App beantwortet es, und derselbe Runner kann auch die aufnehmen.

Mach das heute in deinem Projekt

  1. Lösch das .xcodeproj aus git und schreib eine project.yml für XcodeGen. Generiere das Projekt in der CI.
  2. Erstell das Zertifikat auf deiner eigenen Maschine über die API, speichere es als .p12 im Legacy-Format und leg es in ein GitHub Secret. Wenn die CI schon welche erstellt hat, liste die verwaisten auf und widerrufe sie.
  3. In der CI: Wegwerf-Schlüsselbund, Import, set-key-partition-list. Wer den letzten Schritt weglässt, bekommt garantiert einen Hänger.
  4. Signiere manuell mit einem App-Store-Profil. Lass das automatische Signieren nicht nach einem Gerät suchen, das du nicht hast.
  5. Build-Nummer = neuester TestFlight-Build + 1, übergeben als Build-Setting.
  6. Trag nie den Xcode-Pfad oder den Simulatornamen fest ein. Wähl zur Laufzeit das neueste, das wirklich existiert.
  7. Füge ein Startargument -shot hinzu, damit die CI deine App-Store-Screenshots aufnehmen kann.
  8. Nutze dry_run, bis das Listing passt, dann release.

Wenn du SwiftUI-Apps auf diese Weise baust: Der Artikel über die Spring-Animation in Minder stammt von einer der Apps, die aus dieser Pipeline kamen. Und wenn ein Coding-Agent die CI-Schleife für dich steuert, lies vorher wie du Coding-Agents davon abhältst, deine Tests zu bearbeiten und was kaputtging, als 20 Agents auf einem PC liefen, denn ein Agent, der um 6 Uhr morgens das Signieren repariert, "repariert" stattdessen gern die Prüfung.

Die komplette Pipeline (der Workflow mit jeder kommentierten Falle, das Fastfile, signing.py, die App-Store-Connect-Skripte und ein Agent-Skill, der dich durchführt) gibt es als Ship iOS from Windows Kit. Wenn du lieber jemanden die App selbst bauen lässt, hier sind unsere Festpreise.

Welcher Apple-Schritt braucht deiner Meinung nach immer noch einen Mac? Sag es uns, und wir prüfen, ob die API das kann.

Zuerst hier auf tensorust.com erschienen am 16. Okt. 2026. Auch auf X (@TensorustAI) und dev.to. Passend dazu: Wir richten diese Pipeline in deinem Repo ein: 299 $, iPhone-Apps zum Festpreis, die iPhone-Apps, Ship iOS from Windows Kit.

Weiterlesen

Alle Artikel →

Mehr auf Deutsch

Deutsch: Tensorust →

Up next

Most read on the site this weekcrates.io download count: 80% of ours arrived with a new versionWhat the crates.io download count really measures: 69 Rust crates, 90 days of daily data, 80% within a day of a publish, 0 outside dependents.Read it next →

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