最新资讯
dev.to 部署 Underdog Bark 3.0 Pro:逐字转写、自定义后端与分块设置 dev.to 为艰难对话构建 AI:五个 Agent、一条安全正则与一位盲评监督者 techcrunch.com Palo Alto Networks 洽谈以 5 亿美元收购 AI Agent 安全初创公司 Clutch theinformation.com 黄仁勋被要求就涉华间谍指控向国会作简报 www.infoq.com Edera 推出 Object Capability Bounded Authority,试图将凭证与 AI Agent 工作负载隔离 wechat 这波杀疯了!旗舰级模型不限量、1个月视频额度免费领 bloomberg.com AI 抛售蔓延至信用市场,软银领跌亚洲美元债 dev.to 如何在专用 GPU 服务器上用 OpenAI 兼容 API 部署 LLM dev.to 审计 153 份 Claude Code 与 Codex 会话记录:User-Agent 与 URL 中的邮箱泄露 dev.to 你的 Agent Harness 应具备的 17 项能力 dev.to 离线 AI 记忆中 0.8 余弦阈值为何失效 dev.to Kimi K3.1 Pro 开放权重:自托管 2.8T 模型前的开发者清单
多元动态
登录 注册
智能体 dev.to · @emi

在没有 Xcode 的情况下构建和测试 iOS 应用

原文 我的收藏

作者介绍如何通过编码 agent 在无 Xcode 的 Linux VM 上构建原生 SwiftUI iOS 应用:利用 macOS 命令行工具做类型检查,用 Docker 中的 Linux Swift 运行不依赖 UIKit 的 Foundation 逻辑测试,在托管 macOS runner 上从 YAML 生成 Xcode 工程、运行 XCTest、上传 .xcresult 与截图,并用 App Store Connect API key 自动签名上传 TestFlight,最后轮询直到 Apple 确认构建有效。文章同时说明了各机器的分工以及机器无法验证的部分。

AI推荐理由 这是一篇开发者实践记录,具体说明了在无 Xcode 的 Linux VM 上通过编码 agent 构建 SwiftUI iOS 应用的机器分工:本地用 macOS 命令行工具做类型检查、用 Docker 中的 Linux Swift 跑 Foundation 逻辑测试、在托管 macOS runner 上生成 Xcode 工程并运行 XCTest、签名上传 TestFlight,并明确指出“机器无法证明什么”。信息密度高、步骤可验证,对 AI 编码 agent 在移动开发中的落地边界有参考价值;局限是个人博客、无量化效果数据,且未提供实际发布结果。

72

正文

TL;DR I build a native swiftui iOS app through a coding agent on a linux VM with no xcode. it ships to testflight from a hosted macos runner. this is how the work splits across machines, and what a machine cannot prove. type-check locally by compiling for macos. swiftui, foundation, observation, and security exist on macos, so almost all of an iOS app type-checks with only the command line tools. iOS-only APIs get signature-only shims the real target never sees. run foundation-only logic under linux swift in docker. view models, parsers, and geometry with no uikit dependency run as ordinary swiftpm tests. build and test for real on a hosted macos runner. generate the xcode project from YAML, pick whatever simulator the runner has, run the xctest target, upload the .xcresult and rendered screenshots. sign and upload from the same runner with an app store connect API key and automatic signing, then poll app store connect until Apple says the build is valid. leave the phone to the human, and report anything the machine could not run as "blocked", NEVER "passed". building and testing an iOS app without xcode for anyone whose iOS code is written where xcode is not: a linux VM, a CI box, a mac with only the command line tools, a coding agent on a server. assumes a paid apple developer account, github actions, and swift 6. 0. what each layer proves question what answers it where it can run is this swift syntactically valid? swiftc -frontend -parse anywhere with a swift compiler, linux included is it well-typed, swift 6 concurrency included? swiftc -typecheck against a real SDK macos command line tools, or linux for foundation-only files does this logic produce the right values? xctest or swift test linux for foundation-only code, a macos runner for anything touching uikit or swiftui does the app build, link, and pass its tests? xcodebuild test in a simulator hosted macos runner does a screen look right? rendered images attached to tests rendered on the runner, reviewed anywhere does it work on a phone, with voiceover, at the largest text size? a human with a device only there put the layer name next to every acceptance criterion. "CI is green" then means exactly what it proved and nothing more. 1. type-checking locally by compiling for macos the command line tools ship a swift 6 compiler and the macos SDK but no iOS SDK, so xcodebuild cannot build an iOS target. swiftc -typecheck for macos can still check nearly all of it. a script copies app and test sources to a staging directory, blanks every #Preview { ... } block line by line so diagnostics keep their line numbers (the macro needs an xcode-only plugin), drops @testable import App because tests compile into the same module, rewrites import XCTest to import Foundation, and runs: swiftc -typecheck \ -target arm64-apple-macos14.0 \ -swift-version 6 \ -strict-concurrency=complete \ -enable-upcoming-feature ExistentialAny \ staging/App/**/*.swift \ staging/TypecheckShims/*.swift \ staging/Tests/**/*.swift \ staging/TestTypecheckShims/*.swift Enter fullscreen mode Exit fullscreen mode the shims. one file declares what macos lacks - NavigationBarItem.TitleDisplayMode, EditMode and its environment key, the text-input modifiers. another declares XCTestCase and XCTAssert* as signatures only. neither is in the project spec, so the iOS build never sees them. 2 rules: mirror the real signature exactly, and shim only where no cross-platform API exists — otherwise change the app (imageio instead of UIImage, for example). a file that exists because of a uikit type, like a UIViewControllerRepresentable photo picker, gets excluded rather than shimmed. use swift 6 language mode, not swift 5 with strict concurrency. the first thing this caught was a shared static let ISO8601DateFormatter, a non-Sendable reference type used across concurrent decodes. swift 6 rejects it; swift 5 only warns, and a warning in a script nobody watches is no signal. what it cannot tell you. nothing is compiled to a binary, linked, or run. a wrong shim is a false pass until the first real xcodebuild, and preview bodies are unchecked. 2. foundation-only logic under linux swift with no mac at all, docker gives you a real swift toolchain: docker run --rm -v "$PWD":/work -w /work swift:6.2.3-noble \ swiftc -frontend -parse -swift-version 6 ios/App/**/*.swift Enter fullscreen mode Exit fullscreen mode that is a parse check only, since there is no swiftui on linux. for actual tests, make a throwaway swiftpm package holding only the files that import nothing beyond foundation: models, formatters, view models, geometry helpers, and their tests. this only works if view models do not import swiftui, so make that a rule from the start. 3. a generated xcode project without xcode you cannot open a .pbxproj. describe the project in ios/project.yml and generate it with xcodegen on whatever machine builds: name: App options: deploymentTarget: iOS: '17.0' settings: base: SWIFT_VERSION: '6.0' SWIFT_STRICT_CONCURRENCY: complete SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY: true ENABLE_USER_SCRIPT_SANDBOXING: true targets: App: type: application platform: iOS configFiles: Release: Release.xcconfig Enter fullscreen mode Exit fullscreen mode gitignore the generated .xcodeproj. build settings become a reviewable YAML diff, and the type-check script reads the same YAML for its flags. the trap: any setting changed through xcode's UI is lost on the next xcodegen generate. 4. build and test on a hosted macos runner github's macos-15 runners have xcode: ios-simulator: runs-on: macos-15 timeout-minutes: 30 steps: - uses: actions/checkout@v5 - run: brew install xcodegen - run: xcodegen generate working-directory: ios - name: Select an available iPhone simulator run: | udid="$(xcrun simctl list devices available --json \ | jq -r '[.devices[] | .[] | select(.isAvailable and (.name | startswith("iPhone")))] | first | .udid')" [[ -n "$udid" && "$udid" != "null" ]] || { xcrun simctl list devices available; exit 1; } echo "SIMULATOR_UDID=$udid" >> "$GITHUB_ENV" - name: Build and run iOS tests run: | xcodebuild test -project ios/App.xcodeproj -scheme App -configuration Debug \ -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \ CODE_SIGNING_ALLOWED=NO \ -resultBundlePath build/AppTests.xcresult - uses: actions/upload-artifact@v4 if: always() with: name: AppTests-${{ github.run_id }} path: build/AppTests.xcresult retention-days: 14 Enter fullscreen mode Exit fullscreen mode 3 details matter. pick the simulator dynamically, because the device list changes with runner images. set CODE_SIGNING_ALLOWED=NO so the test job needs no credentials. always upload the .xcresult — without local xcode it is the only way to see a failure. 5. tests that produce pictures "does it look right" needs a channel. render swiftui views in tests with ImageRenderer, assert the geometry, attach the image: let view = HeroView(item: fixture) .frame(width: 390) .environment(\.dynamicTypeSize, .accessibility5) .environment(\.colorScheme, .dark) let renderer = ImageRenderer(content: view) renderer.scale = 1 let image = try XCTUnwrap(renderer.uiImage) XCTAssertEqual(image.size.height, 390 * 5 / 4, accuracy: 3, "poster keeps 4:5") attach(image, name: "hero-dark-ax5") Enter fullscreen mode Exit fullscreen mode a CI step exports the attachments with xcrun xcresulttool export attachments and uploads them, so someone downloads a zip of PNGs and looks. render each screen in light and dark, at default and the largest accessibility text size, against a fixture environment pointed at an unreachable host. this is how I found a variable font whose named weights were silently falling back to the system font — no error anywhere, only the picture. 6. localhost HTTP in debug only, with 1 Info.plist the simulator needs an app transport security exception for http://localhost and a release build must not carry one, but xcodegen generates 1 Info.plist per target. make the value a build setting: NSAppTransportSecurity: NSExceptionDomains: localhost: NSExceptionAllowsInsecureHTTPLoads: $(ATS_ALLOW_LOCALHOST_HTTP) NSIncludesSubdomains: true Enter fullscreen mode Exit fullscreen mode with ATS_ALLOW_LOCALHOST_HTTP: YES in Debug and NO in Release. xcode materializes it as a real boolean, so release carries an exception that permits nothing. a check script asserts NSAllowsArbitraryLoads appears nowhere and every insecure-loads flag is Debug-gated. 7. deployment identity outside the repo a tracked Release.xcconfig contains 1 line: #include? "Release.local.xcconfig" Enter fullscreen mode Exit fullscreen mode the ? matters. xcodegen refuses to generate when a config file is missing, so pointing straight at a gitignored file breaks every fresh clone. with the optional include, an absent local file leaves DEVELOPMENT_TEAM, API_BASE_URL, and ASSOCIATED_DOMAIN undefined and the archive does not sign. do NOT commit a default; it ships unedited exactly once. 1 xcconfig trap: // inside a value starts a comment, so https://host truncates to https:. write https:/$()/host. 8. signing and uploading to testflight from the runner you need 4 things from Apple: a paid developer account, an app store connect API key (key ID, issuer ID, .p8 private key), the app record in app store connect, and your team ID. store them as repository secrets. the workflow, in order: fail early if a secret is missing, the bundle ID does not match the project, or a URL is not HTTPS. select a release xcode with sudo xcode-select --switch, because runners ship several and testflight rejects archives from betas. write Release.local.xcconfig from secrets and run xcodegen generate. install the .p8 under $RUNNER_TEMP, normalizing the CRLF and literal \n sequences password managers introduce, and verify it with openssl pkey -check without echoing it. then: xcodebuild archive -project ios/App.xcodeproj -scheme App -configuration Release \ -destination 'generic/platform=iOS' -archivePath build/App.xcarchive \ -allowProvisioningUpdates \ -authenticationKeyPath "$ASC_KEY_PATH" -authenticationKeyID "$ASC_KEY_ID" -authenticationKeyIssuerID "$ASC_ISSUER_ID" \ DEVELOPMENT_TEAM="$APPLE_TEAM_ID" PRODUCT_BUNDLE_IDENTIFIER="$APPLE_BUNDLE_ID" \ CURRENT_PROJECT_VERSION="$GITHUB_RUN_NUMBER" CODE_SIGN_STYLE=Automatic Enter fullscreen mode Exit fullscreen mode before uploading, read Info.plist out of the archive with PlistBuddy and check the API URL and build number, then curl the API's health endpoint. that catches a testflight build pointing at localhost. export with method: app-store-connect and destination: upload, same key flags. then poll. a short node script signs an ES256 JWT with the .p8, looks up the app by bundle ID, and polls builds?filter[version]= every 30 seconds for up to 15 minutes. uploads that fail processing are invisible otherwise. the github run number as CFBundleVersion gives every build a unique, increasing number with 0 bookkeeping. 9. reporting what could not run criteria that need xcode cannot run on the linux VM. report them as blocked, not failed and not passed. failed makes the run permanently red and people stop reading red. passed is a trusted tick next to something nobody ran. each automated criterion declares the toolchain it needs, and the verifier probes for it first: - [ ] **AC-1.8** — Ten concurrent 401s trigger exactly one refresh call. - verify: auto - cmd: `xcodebuild test -scheme App -only-testing:AppTests/APIClientRefreshTests` - requires: xcode Enter fullscreen mode Exit fullscreen mode the xcode probe is xcodebuild -version exiting zero, which fails with only the command line tools. the swift probe is swiftc --version plus xcrun --show-sdk-path, enough for the macos type-check. a blocked criterion never counts as passed. decide separately which criteria are genuinely human. "voiceover reads the hero in the right order" is. "no hard-coded colors remain in feature views" is a grep. when it doesn't work hosted green read as done. hosted screenshots got treated as device verification when no device had been touched. keep "hosted evidence" and "device evidence" as separate words in your tracking. Apple's certificate limit blocked a run before upload. automatic signing from CI can create certificates only within the account's limits and cannot revoke anything. check Certificates in the developer portal before your first run. the font fell back silently. a variable TTF's named weights did not register and the body rendered in the system font with no error. a check now fails the build if a requested face does not register. shims drift, and preview bodies are unchecked. every new iOS-only API is a decision: shim it, exclude the file, or switch to a cross-platform API. prefer the third. anything only exercised in a #Preview stays unverified until a mac builds it. the stack xcodegen. swift 6 language mode with strict concurrency. view models that do not import swiftui. xctest plus ImageRenderer attachments. github macos-15 runners for building, testing, and signing. an app store connect API key with automatic signing. a node script with 0 dependencies for type-check staging, toolchain probing, and testflight polling, so it runs before npm install on any machine.

来源报道 · 1 篇 · 以下链接均前往原始媒体

← 返回精选动态 原文

更多内容