본문 바로가기
C.W.K.
Stream
Lesson 02 of 07 · published

워치 타깃: 품어 넣고, 명시하고, 아카이브에서 확인하기

~16 min · on-the-wrist, watchos, xcodegen, info-plist, simulator

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"빌드 설정은 의도를 말할 뿐이고, 번들이 사실이야."

번들 하나, 산출물 셋

가족 워치 앱은 App Store Connect의 별도 앱이 아니야. iOS 앱의 Watch 폴더 안에 들어가는 watchOS 애플리케이션 타깃이고, 컴플리케이션은 그 워치 앱 안에 품은 WidgetKit 확장이야. 아카이브도 빌드 번호도 TestFlight 업로드도 하나씩이고, 워치 앱은 폰을 거쳐 설치돼. XcodeGen으로 옮기면 타깃 셋에 embed: true 의존성 둘이야. 번들 식별자는 폰 것 뒤에 이어 붙여서 워치 앱은 …mobile.watchkitapp, 컴플리케이션은 거기에 또 붙인 …watchkitapp.widgets야. 워치 앱 Info.plist는 WKCompanionAppBundleIdentifier로 짝 앱을 가리켜. 이 레슨을 쓰면서 아이폰 시뮬레이터용으로 빌드해보니 산출물이 딱 그 모양이었어. SparkMobile.app/Watch/Spark.app이 있고, 그 안에 SparkWatchWidgets.appex가 들어 있었지.

올리고 나서야 거절된 업로드

일지 앱에 워치 앱을 넣은 첫 TestFlight 빌드는 업로드까진 성공했는데, 그 뒤에 App Store Connect가 거절했어. 워치 애플리케이션에 아이콘이 없고, Info.plist에 CFBundleIconName이 빠졌다고. 로컬에선 전부 안심되는 신호뿐이었어. 빌드는 초록불이었고, 워치 번들엔 Assets.car가 있었고, 아이콘을 가리키는 빌드 설정도 켜져 있었어. 근데 GENERATE_INFOPLIST_FILE은 watchOS 타깃에 CFBundleIconName을 안 써줘. 빌드된 워치 앱에 PlistBuddy를 돌려보면 바로 확인돼. 해결책은 두 가지야. 하나는 CFBundleIconName, WKApplication, 짝 앱 식별자를 담은 워치 타깃 전용 Info.plist고, 다른 하나는 업로드 관문의 아카이브 검사야. 이 검사는 워치 앱이 들어 있으면 그 최상위 키와 컴파일된 카탈로그, 컴플리케이션 확장이 실제로 있어야 통과시켜.

워치 없이 보기

네이티브 Pippa에서 워치 스킴만 -sdk watchsimulator로 빌드하니까 컴파일에 들어가기도 전에 "Multiple commands produce …/Pippa.app/Pippa"로 실패했어. -sdk는 스킴이 건드리는 모든 타깃에 적용돼서, iOS 앱도 워치 SDK로 빌드됐고 같은 제품 이름의 워치 앱과 부딪혔거든. 그러니까 iOS 스킴을 아이폰 시뮬레이터용으로 빌드해. 안에 든 워치 앱은 simctl install로 켜져 있는 워치 시뮬레이터에 바로 설치되고, 페어링도 필요 없어. 인터페이스를 보기엔 이걸로 충분하지만, 받아쓰기가 걸린 일엔 모자라. 그건 진짜 워치에서만 증명돼.

케이블로 연결한 진짜 워치도 빌드 확인일 뿐이야

진짜 워치에 케이블로 설치하려면 관문 셋을 따로 넘어야 했어. 첫째는 워치 자체 설정에서 켜는 개발자 모드야. 둘째는 워치 타깃 스킴인데, 이게 있어야 xcodebuild가 워치를 빌드 대상으로 지정해 등록할 수 있어. 셋째로 Xcode 기기 계층이 워치를 알아봐야 하는데, xcrun xcdevice list에 available로 뜨면 된 거야. 이 중 어느 것도 배포는 아니야. 배포는 TestFlight가 맡아. 릴리스처럼 서명해서 폰을 거쳐 워치 앱을 설치하지.

Code

project.yml: 폰 앱에 품은 워치 앱, 워치 앱에 품은 컴플리케이션·yaml
  SparkMobile:
    # ...
    dependencies:
      - target: SparkWatch               # the watch app rides inside the iOS bundle
        embed: true
        copy:
          destination: productsDirectory
          subpath: "$(CONTENTS_FOLDER_PATH)/Watch"

  SparkWatch:
    type: application
    platform: watchOS
    deploymentTarget: "10.0"
    scheme: {}                           # a watch build destination, for cable checks only
    sources:
      - path: Watch
    dependencies:
      - target: SparkWatchWidgets        # the complication rides inside the watch app
        embed: true
    settings:
      base:
        PRODUCT_NAME: Spark
        PRODUCT_BUNDLE_IDENTIFIER: com.example.spark.mobile.watchkitapp
        TARGETED_DEVICE_FAMILY: "4"
        GENERATE_INFOPLIST_FILE: NO      # it would not write CFBundleIconName for watchOS
        INFOPLIST_FILE: Watch/Info.plist # CFBundleIconName, WKApplication, WKCompanionAppBundleIdentifier
        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
        SKIP_INSTALL: YES

  SparkWatchWidgets:
    type: app-extension
    platform: watchOS
    deploymentTarget: "10.0"
    sources:
      - path: WatchWidgets
    info:
      path: WatchWidgets/Info.plist
      properties:
        CFBundleShortVersionString: $(MARKETING_VERSION)
        CFBundleVersion: $(CURRENT_PROJECT_VERSION)
        NSExtension:
          NSExtensionPointIdentifier: com.apple.widgetkit-extension
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.example.spark.mobile.watchkitapp.widgets
        TARGETED_DEVICE_FAMILY: "4"
        SKIP_INSTALL: YES
한 번 빌드해서 안에 든 번들을 확인하고, 시뮬레이터에서 워치 앱 돌리기·bash
# Build the iOS scheme for an iPhone Simulator. Do NOT add -sdk watchsimulator: -sdk overrides
# every target in the scheme, and an iOS app and a watch app with the same product name then
# collide with "Multiple commands produce".
xcodebuild -project SparkMobile.xcodeproj -scheme SparkMobile \
  -destination 'generic/platform=iOS Simulator' -derivedDataPath build/dd build

app=$(find build/dd -path '*Debug-iphonesimulator*' -name SparkMobile.app | head -1)
ls "$app/Watch"                                                   # Spark.app
ls "$app/Watch/Spark.app/PlugIns"                                 # SparkWatchWidgets.appex
/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$app/Watch/Spark.app/Info.plist"   # AppIcon

# The embedded watch app installs straight onto a booted watch simulator; no pairing needed.
xcrun simctl install "$WATCH_SIMULATOR_ID" "$app/Watch/Spark.app"
xcrun simctl launch "$WATCH_SIMULATOR_ID" com.example.spark.mobile.watchkitapp
sleep 4 && xcrun simctl io "$WATCH_SIMULATOR_ID" screenshot build/watch.png
# A clock face in the screenshot is not a crash: read the log before deciding.

External links

Exercise

SparkMobile project.ymlSparkWatchSparkWatchWidgets를 추가하고 프로젝트를 생성한 뒤, iOS 스킴을 아이폰 시뮬레이터용으로 빌드해. 워치 앱과 그 확장이 번들 안에 들어갔는지 확인하고, 워치 앱의 CFBundleIconName을 출력해. 다음으로 워치 타깃 카탈로그에 AppIcon 에셋을 둔 채 GENERATE_INFOPLIST_FILE: YES로 바꿔서 다시 빌드하고, 그 키를 한 번 더 출력해. 마지막으로 워치 시뮬레이터에 워치 앱을 설치하고 돌고 있는 화면을 스크린샷으로 남겨.
Hint
켜자마자 찍은 스크린샷에 시계 화면이 나오면 앱을 다시 띄우고 몇 초 뒤에 또 찍어. 크래시라고 단정하기 전에 xcrun simctl spawn <워치> log show --last 3m부터 읽어봐. 워치 아이콘 키 검사는 기억에 맡기지 말고 파이프라인의 업로드 관문에 넣어.

Progress

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

댓글 0

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

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