본문 바로가기
C.W.K.
Stream
Lesson 04 of 06 · published

원본은 project.yml, Xcode 프로젝트는 결과물

~16 min · packages-projects, xcodegen, xcode-project, ios, targets

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"TEST SUCCEEDED. 테스트 2개 실행, 원래 있던 그 두 개. 새 테스트 파일은 어디서도 컴파일되지 않았어."

iOS엔 Xcode 프로젝트가 필요한데, 아무도 손으로 안 고치는 이유

SwiftPM 패키지로는 iOS 을 표현 못 해. 에셋 카탈로그와 앱 아이콘, 앱에 내장하는 공유 확장과 워치 앱, 엔타이틀먼트, 명시적인 Info.plist, 프로비저닝, 테스트 호스트 같은 것들 말이야. 이런 건 .xcodeproj에 들어가는데, 그 안의 project.pbxproj는 사람이 손으로 병합하면 안 되는 파일이야. 이 딜레마를 푸는 게 XcodeGen이야. 사람이 읽을 수 있는 project.yml을 쓰고 xcodegen generate를 돌리면, 거기서 Xcode 프로젝트가 만들어져. 이 가족의 iOS랑 워치 앱은 전부 이렇게 해. YAML이 원본이고, 프로젝트는 결과물이야.

project.yml에 들어가는 것

  • 프로젝트 전체 설정은 절대 어긋나면 안 돼. DEVELOPMENT_TEAM, CODE_SIGN_STYLE: Automatic, SWIFT_VERSION, 그리고 프로젝트 수준의 MARKETING_VERSIONCURRENT_PROJECT_VERSION이 여기 들어가. 그래야 앱과 확장이 늘 같은 버전 쌍을 달아. App Store Connect는 번호가 호스트랑 다른 확장을 거부하거든.
  • packages에는 공유 Swift 패키지를 path로 적어.
  • targets에는 앱(type: application), 확장(app-extension), 워치 앱, 테스트 번들이 들어가. 타깃마다 소스, 의존성, info 속성, entitlements 속성, 스킴이 붙어.

다음 생성 때 사라지는 클릭 두 개

Xcode UI에서 바꾸는 건 전부 생성된 프로젝트를 바꾸는 거고, 다음 xcodegen generate가 그걸 갈아엎어. Signing & Capabilities에서 고른 팀도 다음 생성 전까지만 유효해. 그래서 팀 ID를 project.yml에 적어 두는 거야. 가족이 빌드 번호를 올릴 때도 pbxproj가 아니라 project.yml을 고쳐.

스위트가 한 번도 안 돌린 파일

XcodeGen은 실행되는 순간의 파일 목록을 고정해 둬. 그 뒤에 추가한 .swift 파일은 프로젝트에 아예 없어. 앱 소스라면 "타입을 찾을 수 없음" 에러로 드러나지만, 테스트 파일이면 아무 소리도 안 나. xcodebuild test는 새 테스트를 한 번도 돌리지 않은 채, 프로젝트가 원래 알던 테스트만 돌리고 ** TEST SUCCEEDED **를 찍어. 가족은 이걸 두 겹으로 막았어. 첫째, 테스트 스크립트가 xcodebuild test 전에 xcodegen generate를 돌려. 둘째, 소스와 테스트 폴더의 Swift 파일이 전부 생성된 프로젝트에 들어 있는지 검사해. 그래서 프로젝트가 낡았으면 엉터리 통과를 찍는 대신 요란하게 실패해. (생성된 프로젝트를 커밋할지는 가족 레포마다 달라. 어느 쪽이든 결과를 믿기 전에 다시 생성해.)

Code

SparkMobile: 유니버설 앱 하나, 공유 확장 하나, 테스트 번들 하나·yaml
name: SparkMobile
options:
  bundleIdPrefix: com.example
  deploymentTarget:
    iOS: "17.0"
  createIntermediateGroups: true

settings:
  base:
    DEVELOPMENT_TEAM: EXAMPLE_TEAM        # in source, so regeneration can never drop it
    CODE_SIGN_STYLE: Automatic
    SWIFT_VERSION: "6.0"
    MARKETING_VERSION: 1.0.0
    CURRENT_PROJECT_VERSION: 1            # project-wide: app and extension always match

packages:
  SparkKit:
    path: ../SparkKit

targets:
  SparkMobile:
    type: application
    platform: iOS
    sources: [Sources]
    dependencies:
      - package: SparkKit
        product: SparkCore
      - target: SparkShare
    info:
      path: Resources/Info.plist
      properties:
        CFBundleDisplayName: Spark
        ITSAppUsesNonExemptEncryption: false
        UILaunchScreen: {}
    entitlements:
      path: Resources/SparkMobile.entitlements
      properties:
        com.apple.security.application-groups: [group.com.example.spark]
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.example.spark.mobile
        TARGETED_DEVICE_FAMILY: "1,2"
    scheme:
      testTargets: [SparkMobileTests]

  SparkShare:
    type: app-extension
    platform: iOS
    sources: [ShareExtension]
    entitlements:
      path: ShareExtension/SparkShare.entitlements
      properties:
        com.apple.security.application-groups: [group.com.example.spark]
    info:
      path: ShareExtension/Info.plist
      properties:
        CFBundleDisplayName: Spark
        CFBundleShortVersionString: $(MARKETING_VERSION)
        CFBundleVersion: $(CURRENT_PROJECT_VERSION)
        NSExtension:
          NSExtensionPointIdentifier: com.apple.share-services
          NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.example.spark.mobile.share

  SparkMobileTests:
    type: bundle.unit-test
    platform: iOS
    sources: [Tests]
    dependencies:
      - target: SparkMobile
먼저 다시 생성하고, 프로젝트가 파일을 다 아는지 증명해·bash
xcodegen generate

# Every Swift file on disk must appear in the generated project, or the suite lies.
missing=0
for f in $(find Sources Tests ShareExtension -name '*.swift'); do
  grep -q "path = $(basename "$f");" SparkMobile.xcodeproj/project.pbxproj || { echo "not in project: $f"; missing=1; }
done
[ "$missing" -eq 0 ] || { echo 'run xcodegen generate'; exit 1; }

xcodebuild test -scheme SparkMobile -destination "platform=iOS Simulator,id=$SPARK_SIMULATOR_ID"

External links

Exercise

XcodeGen을 깔아(brew install xcodegen). Sources, ShareExtension, Tests 폴더마다 최소한의 Swift 파일을 하나씩 넣어서 SparkMobile 폴더를 만들어. 팀 값은 네 팀을 가리키는 임시 값으로 채우고, 위 project.yml을 써서 xcodegen generate를 돌려. 이제 다시 생성하지 말고 테스트 파일을 하나 더 추가한 뒤 파일 확인 검사를 돌려봐. 검사가 실패하면서 그 파일 이름을 대는지 봐. 다시 생성하고 나서 통과하는지 확인해. 마지막으로 Xcode UI에서 팀을 바꾸고, 다시 생성해서 네 변경이 어디로 갔는지 봐.
Hint
grep -c NewTests.swift SparkMobile.xcodeproj/project.pbxproj가 다시 생성하기 전엔 0, 후엔 양수를 찍어. UI에서 고른 팀은 다음 generate 전까지만 유지돼. YAML에 적어야 하는 이유가 딱 그거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.