fortunto2/solo-factory/ios-dev

Claude Code and agent skill by fortunto2/solo-factory

Trust tier 2
  • community
  • ext:[{"type":"hook","description":"Pre/post lifecycle hooks"}]
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Build iPhone/iOS apps — native SwiftUI or Kotlin-Multiplatform hybrid. Use when user says "build an iOS app", "add a SwiftUI screen", "run it on my iPhone", "set up the Xcode project", or when scaffolding an iOS app, writing SwiftUI/ARKit/MapLibre features, wiring the Claude Code ↔ Xcode workflow, building or installing on a physical device, or setting up a project so it survives its first App Store upload (product name, orientations, encryption, dSYM, xcodeproj tracking). For TestFlight, submi…

Install

npx vskill@latest install fortunto2/solo-factory/ios-dev

Semi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard

SKILL.md

iOS / iPhone app

Reference + checklist for building and shipping iPhone apps. Read the stack, copy the example's patterns, and run the publishing checklist BEFORE the first App Store upload — it prevents the rejections below that each cost a re-archive.

Stack (solopreneur templates)

  • Native Swift: ~/startups/solopreneur/solo-factory/templates/stacks/ios-swift.yaml

— Swift 6 + SwiftUI, SPM, xcodegen (project.yml → generated .xcodeproj, do not commit the `.xcodeproj`), SwiftData (not Core Data), StoreKit 2, .xcstrings string catalogs, SwiftLint + swift-format, lefthook, App Store Connect CLI asc (brew install asc).

  • KMP hybrid (shared logic, cross-platform): ~/startups/solopreneur/solo-factory/templates/stacks/kotlin-multiplatform.yaml

— Kotlin Multiplatform + Compose Multiplatform, Gradle version catalog, expect/actual for platform code.

Pick native Swift for iOS-only; KMP when Android shares the domain/UI. The ios-swift.yaml carries the full detail (house patterns, asc CLI commands, Xcode MCP tools, on-device-AI packages) — read it when scaffolding; the sections below promote the highest-leverage bits.

House conventions (native Swift, Swift 6)

Match these so generated code fits the codebase (from ios-swift.yaml patterns, learned via SoloGraph):

  • `@Observable @MainActor final class` view models — NOT ObservableObject. @State private var vm = MyVM() in the view; inject services via init (DI).
  • `actor` for heavy/ML/data services (own isolation); @Observable @MainActor final class for UI-facing services (recording, audio). A protocol for every service (Services/Protocols/FooServiceProtocol.swift) → mocking + swapping.
  • SwiftData `@Model` for persistence (NOT Core Data, NOT Firebase). Plain structs for transient data/config/API responses. Local-first.
  • async/await everywhere, no Combine for new code. Timer callbacks → Task { @MainActor in … }.
  • Permissions: requestPermission() async -> Bool in the protocol; request before use; "Open Settings" alert on denied; #if os(iOS) around AVAudioSession/UIApplication for macOS compat.
  • Dir layout: App/ Models/ Views/ ViewModels/ Services/ Services/Protocols/ Extensions/ Resources/. MVVM.
  • i18n: String Catalog (.xcstrings, Xcode 16). Lint: SwiftLint + swift-format; hooks via lefthook. Tests: Swift Testing (@Test) new, XCTest legacy. IAP: StoreKit 2. Analytics: superduper-analytics via the SuperDuperAnalytics SPM package; crashes from App Store Connect + MetricKit, no third-party SDK.

Where an async function actually runs (Swift 6.2)

Reported, not measured here — from the write-swift skill (~/.agents/src/emilkowalski-skills, MIT). Verify against the compiler before relying on it in a shipped change; it is the section that skill calls "the one agents get wrong most often", and our own conventions above are silent on it.

The conventions say "actor for heavy services" and "async/await everywhere". Neither tells you where the work lands, and the default is not the one most people assume:

  • `async` alone does NOT mean off-main. An async function runs on the caller's actor.

Called from an @Observable @MainActor final class view model, it runs on the main actor, and the await only yields — it does not move the work. This is the trap our own house style walks into: every view model here is @MainActor.

  • `@concurrent` is what moves it to the background pool. Write it on the function that does

the heavy part, not on the caller.

  • `nonisolated` runs wherever it is called — the right default for a service type that should

not inherit anybody's isolation.

  • Profile before you offload. Hopping actors costs; a fast function on main is better than a

slow hop for a job that takes microseconds.

  • Actors are not FIFO. They run the highest-priority waiter first, so "I awaited first" says

nothing about ordering.

  • `await` breaks atomicity. State can change across a suspension, so re-read it after. For a

transactional change, mutate actor state in a synchronous method, which cannot be interleaved.

swift
nonisolated struct PhotoProcessor { // inherits nobody's isolation
@concurrent // and this part is genuinely off-main
func process(_ data: Data) async -> ProcessedPhoto { … }
}

Depth: the write-swift skill — Sendable, task groups, async let vs withTaskGroup, cancellation, the global-variable ladder (let → @MainActor → Mutex → nonisolated(unsafe)).

Swift Testing, concretely

Our convention line says "Swift Testing (@Test) new, XCTest legacy". What that buys, beyond the attribute:

  • `#expect(a == b)` captures the subexpressions, so a failure prints both sides without an

assertion message.

  • `try #require(x)` unwraps and stops the test — the replacement for XCTUnwrap plus a guard.
  • `@Test(arguments: […])` runs each case as its own test, in parallel, and names the failing

argument. Not a loop inside one test.

  • A test is a struct, fresh per test, and tests run in parallel by default. Shared mutable

fixtures break here in a way they did not under XCTest.

  • Traits carry the reason: .disabled("why"), .bug(url), .timeLimit(…), .tags(…), and

withKnownIssue { } for a failure you have already diagnosed and do not own.

Keep XCTest for UI automation, performance metrics, and Objective-C interop.

Claude Code ↔ Xcode workflow

  • Xcode MCP bridge (Xcode 26.3+, xcrun mcpbridge; enable in Xcode → Settings → Intelligence → MCP Server):

Claude Code can build/test/render natively — BuildProject, RunAllTests/RunSomeTests, `RenderPreview` (SwiftUI preview → image, visual verification without a full run), DocumentationSearch (Apple docs + WWDC), ExecuteSnippet (Swift REPL), GetBuildLog. Prefer these when available.

  • Simulator visual smoke test (no device needed for non-AR/non-camera screens):

``bash xcrun simctl boot 'iPhone 16' 2>/dev/null || true xcrun simctl install booted <App.app>; xcrun simctl launch booted <bundle.id> xcrun simctl io booted screenshot /tmp/sim.png xcrun simctl spawn booted log stream --style compact --timeout 10 ``

  • ARKit/camera/real-GPS features can't be tested in the Simulator — build to verify compile, test on device.

The feedback loop: XcodeBuildMCP (install this first)

Without a loop the agent writes blind. In the browser it opens a page; on iOS it needs the simulator. Two MCP servers cover it, and they complement rather than overlap.

XcodeBuildMCP (npx -y xcodebuildmcp@latest mcp) — headless build, run, test and UI automation. The part that matters most: snapshot_ui returns a semantic tree with element references, and tap takes a reference rather than a coordinate:

e244|tap|button|Make a montage from the selected period
e189|tap|button|Days e247|tap|button|Audio

That removes the whole class of failure a coordinate-driven walker suffers — describe-ui-style dumps include views belonging to sheets underneath, off to the side of the screen, and a tap aimed there dismisses whatever is on top. Also worth having: wait_for_ui with a predicate instead of polling loops, record_sim_video, coverage straight out of xcresult, and launch_app_sim capturing runtime + os_log to files on its own.

Enable the workflows you need — the default is only 24 tools. UI automation and the device workflow are off unless you ask:

json
"xcodebuild": {
"command": "npx",
"args": ["-y", "xcodebuildmcp@latest", "mcp"],
"env": {
"XCODEBUILDMCP_ENABLED_WORKFLOWS":
"session-management,simulator,simulator-management,ui-automation,device,utilities,project-discovery"
}
}

With that it registers 44 tools instead of 24. The variable is not in --help; it is XCODEBUILDMCP_ENABLED_WORKFLOWS, found by grepping the package.

Call session_set_defaults once (project, scheme, simulator id, bundle id, persist: true) — it writes .xcodebuildmcp/config.yaml in the repo, and every later call can go with empty arguments. Call session_show_defaults before the first build of a session; the server asks for this explicitly.

Xcode's own bridge (xcrun mcpbridge, Xcode 26.3+) — advertised as 20 tools over XPC, including rendering a SwiftUI Preview without building and running the whole app, plus a Swift REPL. It needs Xcode running with the project open, so it is no use in a background or CI run.

Verify it answers before planning around it. On Xcode 26.6 with the project open, initialize replies (serverInfo: {name: xcode-tools}) and then tools/list never answers and the pipe closes — nothing usable. Probe it in ten seconds rather than assuming either way:

bash
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | xcrun mcpbridge

Keep one definition, enabled per project. A copy per repo drifts: five repos measured here carried .mcp.json entries without the mcp subcommand, so the server exited on start (CONNECTION_CLOSED), and three more sat at "Pending approval" because project-scope servers need a one-time approval nobody gave. A user-scope entry is the opposite mistake: 44 iOS tools in every web and Python session. What works is a tiny local plugin that holds the server once, installed but disabled at user level and switched on where it belongs:

<marketplace>/xcodebuildmcp/.claude-plugin/plugin.json # name, version
<marketplace>/xcodebuildmcp/.mcp.json # the block above
~/.claude/settings.json "enabledPlugins": {"xcodebuildmcp@<marketplace>": false}
<project>/.claude/settings.local.json "enabledPlugins": {"xcodebuildmcp@<marketplace>": true}

The marketplace can be a local directory ("source": "directory"); Apple's exported Xcode plugin can live in the same one. claude plugin install turns the plugin on at user scope, so set it back to false afterwards. Check with claude mcp list from a Swift project (one xcodebuild line) and from a non-Swift one (none).

Apple RAG MCP (official Swift docs and HIG over RAG) is a nice-to-have — context7 already answers most API questions, and HIG comes up once a day, not once a minute.

Driving the Simulator, and trusting the numbers

Learned the hard way on a video app; the traps are not app-specific.

Pick the reader by where the code runs, not by benchmark. Measured on one screen (Xcode 26.6, iOS 26 simulator, Aug 2026):

ReaderReturnedUse it
MCP snapshot_ui (XcodeBuildMCP)442 nodes, each with elementRef, role, label, available actioninside an agent session — tap by elementRef, no coordinates, and batch several taps on one screen
axe describe-ui --udid48 nodes, 21 labelled, with framesshell scripts, which cannot reach MCP
idb ui describe-all1 empty element; every tap answers Mach port invalid, device disconnectednothing, currently

idb was the fast one and is now broken against current simulators — idb kill && idb connect does not revive it. Its "0.27s tap" is the speed of the error, not of a tap; it does at least exit non-zero, so a idb … || axe … fallback still works and merely wastes a call. Re-measure before trusting any of these three, including this table — the tool that was right last release is the one most likely to be quietly wrong now.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
19
Repository forks
3
7-day trend score
15

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Extension points

  • hookPre/post lifecycle hooks

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/fortunto2/solo-factory/ios-dev/badge)](https://verified-skill.com/skills/fortunto2/solo-factory/ios-dev)

Something wrong with this skill or its page? Tell us.