Przejdź do głównej zawartości

Receptury natywne: iOS i Android

Natywny rozwój aplikacji na iOS i Androida z agentem AI działa wtedy, gdy każda platforma ma jeden skrypt, który buduje aplikację, uruchamia testy na przypiętym symulatorze lub emulatorze i zapisuje wyniki jako dowody. Claude Code, Codex i Cursor iterują względem tego skryptu, a serwer MCP, taki jak MobileBuildMCP lub mobile-mcp, pokazuje agentowi działającą aplikację.

Prosisz agenta o nowy ekran ustawień w SwiftUI. Agent pisze czysty kod w Swifcie, ogłasza „gotowe”, a projekt się nie buduje, bo nowy plik nigdy nie trafił do targetu Xcode. Po stronie Androida agent przy każdej zmianie odpala ./gradlew clean build, czeka minutami na wynik i wkleja do własnego kontekstu tysiące linii logu Gradle. Żaden z nich nie spojrzał na ekran, który zbudował. Ta strona zamyka te trzy luki: informację zwrotną z builda, szybkość i dowód.

  • Dwa skrypty do skopiowania, scripts/ios-check.sh i scripts/android-check.sh. Każdy uruchamia przypięte urządzenie, odpala testy, wypisuje agentowi krótkie podsumowanie i zapisuje pełne dowody w build/evidence/.
  • Blok instrukcji do AGENTS.md, CLAUDE.md lub reguły Cursora, który definiuje „gotowe” jako „skrypt zakończył się kodem 0”.
  • Zweryfikowane komendy instalacji dwóch mobilnych serwerów MCP oraz wtyczek Codeksa dla iOS i Androida, osobno dla każdego narzędzia.
  • Zasady skracające wewnętrzną pętlę agenta: wąski wybór testów, rozgrzane urządzenia i cache.
  • Receptury na ekran w SwiftUI, ekran w Jetpack Compose i reprodukcję crasha, każda zakończona dowodem z testów UI zamiast diffem do przeczytania.

Dlaczego aplikacje natywne potrzebują innej pętli agenta

Dział zatytułowany „Dlaczego aplikacje natywne potrzebują innej pętli agenta”

Agent webowy dostaje tanią informację zwrotną: serwer deweloperski przeładowuje się w sekundę, a przeglądarkowy MCP pokazuje stronę. Projekt natywny daje tę informację wolniej, z trzech powodów:

  1. Plik projektu jest częścią builda. Xcode decyduje, co się kompiluje, na podstawie przynależności do targetu w project.pbxproj, a Gradle na podstawie układu modułów. Agent, który zapisze poprawny plik w złym miejscu, dostaje błąd builda albo, co gorsze, plik, który po cichu nigdy się nie kompiluje.
  2. Buildy są wolne i głośne. Czysty xcodebuild albo build Gradle wypisuje tysiące linii. Wpuszczone do kontekstu agenta kosztują tokeny i wypychają z pola widzenia instrukcje zadania.
  3. UI działa w osobnym procesie. Symulator i emulator to osobne programy. Bez runnera testów albo serwera MCP agent nie ma jak sprawdzić, czy ekran się renderuje. Ty też nie.

Dlatego receptury na tej stronie nie zaczynają się od promptu z funkcją. Zaczynają się od dania agentowi jednej, cichej i niezawodnej komendy na platformę, a agenta ocenia się po tym, co ta komenda udowadnia.

  1. Przypnij jeden symulator i jeden emulator. Dostępne urządzenia wypiszesz przez xcrun simctl list devices available i emulator -list-avds (binarka emulator leży w $ANDROID_HOME/emulator). Wybierz po jednym i wpisz ich nazwy do skryptów poniżej. Przypięte urządzenie oznacza, że czerwony test mówi coś o twoim kodzie, a nie o tym, który telefon agent akurat uruchomił.

  2. Dodaj scripts/ios-check.sh. Skrypt trzyma DerivedData w repozytorium, zapisuje pełny log do pliku i wypisuje tylko linie potrzebne agentowi:

    #!/usr/bin/env bash
    # scripts/ios-check.sh — build, test and collect evidence on one pinned simulator.
    # Usage: scripts/ios-check.sh [TestTarget/TestClass ...] (no argument = all tests)
    set -euo pipefail
    SCHEME="${SCHEME:-App}"
    SIM="${SIM:-iPhone 17}"
    OUT="build/evidence/ios"
    rm -rf "$OUT" && mkdir -p "$OUT"
    ONLY=()
    for t in "$@"; do ONLY+=("-only-testing:$t"); done
    xcrun simctl bootstatus "$SIM" -b >/dev/null # boots the simulator if needed, waits until ready
    set +e
    xcodebuild test \
    -scheme "$SCHEME" \
    -destination "platform=iOS Simulator,name=$SIM" \
    -derivedDataPath build/DerivedData \
    -resultBundlePath "$OUT/tests.xcresult" \
    ${ONLY[@]+"${ONLY[@]}"} > "$OUT/xcodebuild.log" 2>&1
    STATUS=$?
    set -e
    xcrun xcresulttool get test-results summary --path "$OUT/tests.xcresult" > "$OUT/summary.json" 2>/dev/null || true
    grep -E "error:|\*\* (BUILD|TEST) (SUCCEEDED|FAILED)|failed" "$OUT/xcodebuild.log" | tail -40
    echo "exit=$STATUS evidence=$OUT"
    exit $STATUS

    Zapis ${ONLY[@]+...} jest istotny: macOS nadal dostarcza Basha 3.2, w którym pusta tablica przy set -u przerywa skrypt. xcresulttool get test-results wymaga Xcode 16 lub nowszego.

  3. Dodaj scripts/android-check.sh. Skrypt korzysta z już działającego emulatora, czyści logcat, żeby log obejmował tylko ten przebieg, i kopiuje raporty testów Gradle do katalogu z dowodami:

    #!/usr/bin/env bash
    # scripts/android-check.sh — unit + instrumented tests on one pinned emulator, evidence collected.
    # Usage: scripts/android-check.sh [com.example.app.SomeUiTest] (no argument = all tests)
    set -euo pipefail
    AVD="${AVD:-Pixel_8_API_35}"
    MODULE="${MODULE:-app}"
    OUT="build/evidence/android"
    rm -rf "$OUT" && mkdir -p "$OUT"
    if ! adb get-state >/dev/null 2>&1; then
    emulator -avd "$AVD" -no-window -no-audio -no-boot-anim > "$OUT/emulator.log" 2>&1 &
    adb wait-for-device
    until [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" = "1" ]; do sleep 2; done
    fi
    adb logcat -c
    FILTER=()
    if [ $# -gt 0 ]; then FILTER=("-Pandroid.testInstrumentationRunnerArguments.class=$1"); fi
    set +e
    ./gradlew ":$MODULE:testDebugUnitTest" ":$MODULE:connectedDebugAndroidTest" \
    ${FILTER[@]+"${FILTER[@]}"} > "$OUT/gradle.log" 2>&1
    STATUS=$?
    set -e
    adb logcat -d > "$OUT/logcat.txt"
    cp -R "$MODULE/build/reports" "$OUT/reports" 2>/dev/null || true
    cp -R "$MODULE/build/outputs/androidTest-results" "$OUT/androidTest-results" 2>/dev/null || true
    grep -E "FAILED|BUILD (SUCCESSFUL|FAILED)|e: " "$OUT/gradle.log" | tail -40
    echo "exit=$STATUS evidence=$OUT"
    exit $STATUS

    Opcjonalny argument zawęża do jednej klasy tylko testy instrumentacyjne. Testy jednostkowe zawsze idą w całości, bo są tanie.

  4. Przekaż agentowi zasady. Wklej ten blok do AGENTS.md dla Codeksa, do CLAUDE.md dla Claude Code albo do reguły projektu w Cursorze. Claude Code czyta AGENTS.md, gdy w repozytorium nie ma CLAUDE.md (od v2.1.277, na kanale wydań latest według stanu na 2026-09-26; użytkownicy kanału stable nadal potrzebują pliku CLAUDE.md, który odsyła do AGENTS.md), więc jeden plik może obsłużyć oba CLI. Jeśli nie pisałeś jeszcze pliku instrukcji, zacznij od artykułu AGENTS.md i CLAUDE.md:

    ## Native build and test loop
    - iOS: run `scripts/ios-check.sh`, optionally with a test class such as `AppUITests/SettingsUITests`.
    Never call `xcodebuild` without `-derivedDataPath build/DerivedData`.
    - Android: run `scripts/android-check.sh`, optionally with a fully qualified test class.
    Never run `./gradlew clean` unless the user asks.
    - A task is done only when the relevant script prints `exit=0`. Quote its summary lines in your final message.
    - Every control a test touches gets a stable ID: `.accessibilityIdentifier(...)` in SwiftUI,
    `Modifier.testTag(...)` in Compose. Never locate elements by visible text alone.
    - Never edit snapshot references (`__Snapshots__/`, `src/test/snapshots/`) or weaken an existing
    assertion to make a test pass. Stop and report the failure instead.
    - New Swift files go inside an existing synchronized folder; do not edit `project.pbxproj` by hand.
  5. Zacommituj skrypty i uruchom je raz samodzielnie. Skrypt, który pada na czystym checkoucie, uczy agenta, że można go ignorować. Uruchom oba, zajrzyj do build/evidence/ i dopisz build/ do .gitignore, jeśli jeszcze go tam nie ma.

Testy dowodzą zachowania, które opisałeś. Do pracy eksploracyjnej (czy ekran wygląda dobrze, co się stanie po dotknięciu tutaj) agent musi widzieć urządzenie i nim sterować. Obsługują to dwa serwery MCP. Jeśli nigdy nie dodawałeś serwera MCP, przeczytaj najpierw wprowadzenie do MCP:

  • MobileBuildMCP (wcześniej XcodeBuildMCP, utrzymywany przez Sentry) buduje, uruchamia i debuguje projekty iOS i macOS na symulatorach i urządzeniach. Wymaga macOS 14.5+ i Xcode 16+. Najczęściej używane narzędzie to build_run_sim: buduje schemat, instaluje aplikację na uruchomionym symulatorze, startuje ją i przechwytuje logi.
  • mobile-mcp (Mobile Next) steruje symulatorami iOS, emulatorami Androida i fizycznymi urządzeniami przez jeden zestaw narzędzi: mobile_list_elements_on_screen do zrzutu drzewa dostępności, mobile_take_screenshot, mobile_click_on_screen_at_coordinates, mobile_get_device_logs, mobile_get_crash i inne. Wersja 1.0.5 udostępnia 32 narzędzia.

Popularność: 2026-09-26 MobileBuildMCP miał 6,4 tys. gwiazdek na GitHubie i pakiet npm mobilebuildmcp 2.7.1 (opublikowany 2026-09-23); 2026-09-28 mobile-mcp miał 7958 gwiazdek na GitHubie i pakiet npm @mobilenext/mobile-mcp 1.0.5 (opublikowany 2026-09-23). Źródła: repozytoria getsentry/MobileBuildMCP i mobile-next/mobile-mcp na GitHubie oraz rejestr npm.

Dodaj serwery z terminala. Domyślny zakres local nie wpuszcza ich do sesji współpracowników, dopóki nie udostępnisz ich przez -s project:

Okno terminala
claude mcp add mobilebuild -- npx -y mobilebuildmcp@latest mcp
claude mcp add mobile-mcp -- npx -y @mobilenext/mobile-mcp@latest

Aplikacja desktopowa Claude Code na macOS ma też panel iOS Simulator (beta od lipca 2026), więc symulator widzisz obok sesji. Żeby agent nie ruszał referencji snapshotów, dodaj reguły deny do .claude/settings.json:

{
"permissions": {
"deny": [
"Edit(**/__Snapshots__/**)",
"Edit(**/src/test/snapshots/**)"
]
}
}

Który wybrać. MobileBuildMCP, gdy problemem jest sam build w Xcode (schematy, podpisywanie, makro w Swifcie, crash przy starcie). mobile-mcp, gdy problemem jest zachowanie na ekranie, na dowolnej platformie. Włączaj tylko serwer potrzebny w danej sesji: definicja każdego udostępnionego narzędzia zajmuje kontekst w każdej turze. mobile-mcp 1.0.5 udostępnia od razu wszystkie 32 narzędzia. MobileBuildMCP domyślnie wystawia tylko workflow simulator; poszerzaj go zmienną środowiskową MOBILEBUILDMCP_ENABLED_WORKFLOWS (na przykład simulator,ui-automation,debugging,logging) dopiero wtedy, gdy zadanie wymaga automatyzacji UI albo debuggera. W Claude Code wygląda to tak: claude mcp add mobilebuild -e MOBILEBUILDMCP_ENABLED_WORKFLOWS=simulator,ui-automation -- npx -y mobilebuildmcp@latest mcp. Wtyczka Codeksa build-ios-apps ustawia zamiast tego XCODEBUILDMCP_ENABLED_WORKFLOWS tylko dlatego, że uruchamia stary pakiet xcodebuildmcp@latest; mobilebuildmcp 2.7.1 ignoruje tę nazwę.

Praca w Xcode zamiast w terminalu. Dokumentacja klientów MobileBuildMCP (sprawdzona 2026-09-26) opisuje, jak Xcode 26.3 i nowsze wersje uruchamiają agentów Codex i Claude Code w Xcode Settings > Intelligence. Ci agenci startują z okrojonym PATH, więc serwer uruchamiany przez npx często się nie podnosi; dokumentacja opakowuje komendę w /bin/zsh -lc z Homebrew i nvm w PATH. Skrypty kontrolne z tej strony działają tak samo w obu miejscach.

Wolna pętla zmienia zachowanie agenta: między buildami zbiera wiele edycji naraz, a błąd ma potem wiele możliwych przyczyn. Utrzymuj trzy pętle, każdą z własnym kosztem:

PętlaCo się uruchamiaKomendaKiedy agent ją odpala
WewnętrznaTesty jednostkowe zmienionego modułu (Swift Testing lub XCTest; JUnit)xcodebuild test ... -only-testing:AppTests/CartTests · ./gradlew :feature:cart:testDebugUnitTest --tests '*CartViewModelTest'Po każdej edycji
ŚrodkowaJedna klasa testów UI na przypiętym urządzeniuscripts/ios-check.sh AppUITests/CartUITests · scripts/android-check.sh com.example.cart.CartScreenTestZanim ogłosi „gotowe”
ZewnętrznaWszystkie zestawy, weryfikacja snapshotów, obie platformyTe same dwa skrypty bez argumentów, w CIPrzy każdym pull requeście

Zasady, które utrzymują szybką pętlę wewnętrzną i środkową:

  • Nie czyść odruchowo. Zapisz agentowi w pliku instrukcji, że clean jest zakazany, chyba że o to poprosisz. Większość odruchów „naprawmy to czystym buildem” bierze się z nieaktualnego DerivedData albo cache Gradle, a lista awarii poniżej podaje dla nich punktową naprawę.
  • Trzymaj urządzenia rozgrzane. Oba skrypty uruchamiają urządzenie tylko wtedy, gdy nie działa. Zostaw symulator i emulator włączone przez całą sesję.
  • Włącz cache Gradle. Dodaj org.gradle.caching=true i org.gradle.configuration-cache=true do gradle.properties. Configuration cache pomija ponowne wykonywanie skryptów builda, gdy nic się w nich nie zmieniło.
  • Rozdziel build i test na iOS przy powtórkach. xcodebuild build-for-testing, a potem xcodebuild test-without-building ponawia niestabilny test bez rekompilacji. Używaj tego tylko do diagnozy: po zmianie kodu agent musi zbudować projekt od nowa.
  • Zmierz, zanim zaczniesz stroić. xcodebuild -showBuildTimingSummary pokazuje, gdzie poszedł czas, a ./gradlew --profile zapisuje lokalny raport HTML w build/reports/profile/. Poproś agenta, żeby przeczytał raport i zaproponował jedną zmianę, a potem zmierz ponownie.

Poniższy prompt najpierw prosi o zachowanie zapisane jako testy, a metą czyni kod wyjścia skryptu.

Zrzuty ekranu trafiają do build/evidence/ios/tests.xcresult, obok testu, który je wykonał. Kod załącznika, który agent powinien wygenerować, wygląda tak:

let app = XCUIApplication()
func snap(_ name: String) {
let shot = XCTAttachment(screenshot: app.screenshot())
shot.name = name
shot.lifetime = .keepAlways // keep it even when the test passes
add(shot)
}
func testEnablingReminderEnablesTimePicker() {
app.launchArguments = ["-uiTesting"]
app.launch()
app.buttons["settings.open"].tap()
snap("1-settings-open")
app.switches["settings.reminderToggle"].tap()
snap("2-reminder-on")
XCTAssertTrue(app.datePickers["settings.reminderTime"].isEnabled)
snap("3-time-picker-enabled")
}

Sprawdź jedną rzecz, którą agent często psuje: argument startowy -uiTesting musi zmieniać zachowanie aplikacji (zestaw UserDefaults w pamięci, brak onboardingu). Jeśli nic w aplikacji go nie czyta, test UI zależy od stanu zostawionego przez poprzedni przebieg i będzie niestabilny.

Receptura: ekran w Jetpack Compose z dowodem z testów UI

Dział zatytułowany „Receptura: ekran w Jetpack Compose z dowodem z testów UI”

Poprawny test w Compose szuka węzłów po tagu, nie po tekście, więc zmiana copy go nie psuje:

@get:Rule val composeRule = createAndroidComposeRule<MainActivity>()
@Test
fun incrementUpdatesTotal() {
composeRule.onNodeWithTag("cart.line.0.increment").performClick()
composeRule.onNodeWithTag("cart.total").assertTextEquals("Total: 24.00")
}

Dowód wizualny na Androidzie bez emulatora da biblioteka testów zrzutów ekranu, na przykład Paparazzi: ./gradlew :app:recordPaparazziDebug nagrywa obrazy referencyjne, a ./gradlew :app:verifyPaparazziDebug pada, gdy renderowanie się zmieni. Na iOS tę samą rolę pełni swift-snapshot-testing z assertSnapshot(of: view, as: .image). Obie biblioteki nagrywają referencje przy pierwszym uruchomieniu, więc te referencje przejrzyj i zacommituj sam.

Raport crasha ze stack trace’em to specyfikacja czerwonego testu. Przekaż go agentowi z podłączonym serwerem urządzeń:

Kolejność ma znaczenie. Test, który przed poprawką był czerwony, a po niej jest zielony, to dowód. Poprawka bez wcześniejszego czerwonego testu to zgadywanie, które akurat się skompilowało.

Jak zweryfikować natywną pracę agenta bez czytania każdej linii

Dział zatytułowany „Jak zweryfikować natywną pracę agenta bez czytania każdej linii”

Skrypty zamieniają każdą zmianę w ten sam zestaw artefaktów, a przegląd zaczyna się od tego zestawu, nie od diffa:

DowódGdzie leżyCo sprawdza recenzent
Werdykt testówOstatnie linie wyjścia skryptu, summary.json, JUnit XML w androidTest-results/exit=0 na obu platformach; liczba testów rośnie, nigdy nie maleje
Zrzuty ekranu UIZałączniki w tests.xcresult (otwierane w Xcode), obrazy Paparazzi lub snapshot-testingEkrany zgadzają się ze specyfikacją; różnice w snapshotach są zamierzone
Logi z działaniaxcodebuild.log, logcat.txtBrak nowych crashy, ANR-ów i zalewu błędów podczas testów UI
Zmiany w testachPliki testów w diffieNowe testy sprawdzają kryteria akceptacji; żadna asercja nie została osłabiona, żaden snapshot nie został przenagrany bez powodu

Trzy zasady sprawiają, że można temu ufać:

  1. CI uruchamia te same skrypty. Zielony wynik w sesji agenta to deklaracja; ten sam skrypt w CI na czystym runnerze macOS to dowód. Trzymaj skrypty w repozytorium, żeby te dwa przebiegi nie mogły się rozjechać.

  2. Agent nie może edytować wyroczni. Referencje snapshotów i testy kodujące kryteria akceptacji są chronione, tak jak opisuje to artykuł o ochronie wyroczni testowej. Claude Code dostaje reguły deny dla ścieżek (zakładka Claude Code wyżej). W Codeksie i Cursorze ta strona opiera się zamiast tego na zabezpieczeniu niezależnym od narzędzia, które przy okazji wspiera też Claude Code: CODEOWNERS na katalogach z referencjami oraz krok CI, który pada, gdy zmieniają się one bez etykiety.

    # .github/CODEOWNERS
    **/__Snapshots__/ @your-org/mobile-leads
    **/src/test/snapshots/ @your-org/mobile-leads
    # CI job step (check out with fetch-depth: 0 so origin/main is available)
    - name: Block unlabelled snapshot changes
    if: ${{ !contains(github.event.pull_request.labels.*.name, 'snapshot-update') }}
    run: |
    if git diff --name-only origin/main... | grep -E '__Snapshots__/|src/test/snapshots/'; then
    echo "Snapshot references changed without the snapshot-update label"; exit 1
    fi
  3. Konkretna osoba zatwierdza dowody. Deweloper, który zlecił zadanie, przegląda zestaw dowodów i diff testów. Diff kodu człowiek czyta w całości tylko tam, gdzie wymaga tego ryzyko: podpisywanie, entitlements, płatności i wszystko, co dotyka uprawnień w Info.plist lub AndroidManifest.xml.

Kontrakt pull requesta dla tego zestawu opisuje artykuł o pakiecie dowodów, a uzasadnienie przeglądu dowodów zamiast diffów — artykuł czytanie dowodów zamiast kodu.

Zacznij od domyślnego modelu swojego narzędzia i podnieś poziom effort, zanim zmienisz model; aktualne ustawienia domyślne i ceny są w przeglądzie modeli. Zadania natywne częściej ogranicza informacja zwrotna z builda niż model, więc najpierw napraw pętlę.