데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      AI전화 watchOS 개발기 #1 (Custom Notification 만들기)

      jylee23 24.11.25
      1,719 7 3
      DEVOTEE 요약
      에이닷 팀에서 Apple Watch용 앱 개발 시 겪었던 어려움과 경험을 공유하는 내용입니다. Apple Watch의 제한적인 하드웨어 스펙과 제한된 개발 자료로 인해, Notification 기능 개발에 여러 도전과 경험을 하였으며, Apple의 샘플 프로젝트와 SwiftUI를 활용한 다양한 문제 해결 방법을 소개하고 있습니다. 중요한 내용 중 하나는 Notification이 어떤 기기로 전송되는지 결정하는 시스템 로직과, watchOS에서 제공하는 Notification Interface의 구현 및 사용 시 주의 사항입니다.
      DEVOTEE 추천 블로그

      👋 안녕하세요!

      저는 AI Comm Client 팀에서 에이닷 통화녹음과 에이닷 전화 iOS 개발을 하고 있는 이준영입니다.

      올해로 2년차 주니어 개발자가 되었는데요, 그간 한 일 중에서 가장 기억 남는 과제는 에이닷 watchOS 앱 개발입니다.


      Apple Watch는 올해로 출시한 지 10년이 되었습니다.

      하지만 부족한 하드웨어 스펙 등으로 인해 iPhone에 비해 제공할 수 있는 기능들이 부족해, 앱 개수도 적고 참고할 만한 개발 자료도 다소 적습니다.

      그렇다보니 개발하면서 여러 어려움을 겪었는데요, 이번 게시글에서는 Notification 관련 기능을 개발하며 겪었던 일들에 대해서 다뤄 보겠습니다.


      👀 들어가기에 앞서..

      이번 게시글에서는, Apple에서 제공하는 Creating a watchOS app 샘플 프로젝트를 사용합니다.

      직접 빌드해서 확인해 보고 싶은 분들은, Apple Developer 사이트에서 해당 프로젝트를 다운로드 받으시면 됩니다.


      그리고 본 게시글은 아래 환경을 기준으로 작성된 내용이므로, 향후 Apple의 OS 업데이트에 따라 동작이 달라질 수 있습니다.

      • iOS 16, iOS 17

      • watchOS9, watchOS 10

      마지막으로, 본 게시글을 이해하기 위해서는 SwiftUI에 대한 기본적인 이해가 필요합니다.

      만약 SwiftUI에 대해 잘 모르신다면, Apple의 Introducing SwiftUI를 먼저 보시는 것을 추천드립니다.


      🔔 Notification 둘러보기

      Notification 목적지 정하기

      Apple 공식 문서에서는, Notification이 iPhone과 Apple Watch 중 어디에 등장하는지에 대해 아래와 같이 설명하고 있습니다.

      Notification 종류

      Source

      Destination

      Local

      iOS 앱

      iPhone과 Apple Watch의 잠금 상태에 따라 결정됨

      Local

      watchOS 앱

      Apple Watch에만 등장함

      Remote

      서버

      iPhone으로 Notification을 전송한 경우, iPhone과 Apple Watch의 잠금 상태에 따라 결정됨.

      watchOS 6 이상에서 Apple Watch로 직접 Notification을 전송한 경우, Apple Watch에만 등장함

      Background

      서버

      iPhone으로 Notification을 전송한 경우, iPhone에만 등장하고 절대 Apple Watch로 전달되지 않음.

      watchOS 6 이상에서 Apple Watch로 직접 Notification을 전송한 경우, Apple Watch에만 등장함

      만약 Notification이 어느 기기로든 전송될 수 있는 경우, 시스템은 기기의 잠금 상태를 사용하여 최적의 목적지를 결정합니다.


      시스템은 다음 규칙을 나열된 순서대로 적용합니다 :

      1. 사용자의 iPhone이 잠금 해제되고 화면이 켜져 있는 경우, 알림은 iPhone으로 전송됩니다.

      2. 그렇지 않으면, Apple Watch가 사용자의 손목에 착용되어 있고 잠금 해제된 경우, 알림은 Apple Watch로 전송됩니다.

      3. 그렇지 않으면, 알림은 기본적으로 iPhone으로 다시 전송됩니다.

      또한, 사용자가 여러 개의 Apple Watch를 가지고 있는 경우, Local Notification은 알림을 예약한 기기에만 나타납니다.


      Remote/Background Notification도 지정된 Apple Watch에만 나타나므로 여러 기기로 Notification을 보낼 필요가 있을 수 있습니다.

      위 설명에서 2번 항목을 보면, Apple Watch가 사용자의 손목에 착용되어 있고 잠금 해제된 경우에 Apple Watch로 알림이 전송된다고 나와 있습니다.

      시스템은 Apple Watch가 손목에 착용되어 있는 것은 어떻게 알 수 있을까요?


      손목 인식과 Notification

      iPhone의 Watch 앱을 보면, 손목 인식이라는 설정이 있습니다.

      image.png

      설명만 읽어보면 암호 설정이 되어 있을 때 Apple Watch를 잠그는 것과 관련 있는 내용으로 보입니다.

      하지만 설명과는 다르게, 암호를 사용하지 않더라도 손목 인식 옵션은 켜고 끄는 것이 가능합니다.

      즉 이 옵션은 시스템이 실시간으로 Apple Watch 착용 여부를 판단하게 해 주는 것입니다.


      앞서 'Apple Watch가 사용자의 손목에 착용되어 있고 잠금 해제된 경우에 Apple Watch로 알림이 전송된다'고 말씀 드렸습니다.

      손목 인식이 꺼져 있으면 어떻게 될까요? 정답은 'iPhone과 Apple Watch에 모두 전송된다'입니다.

      Apple에서 위와 같이 동작하는 지에 대한 설명이 따로 없어, 제가 추론한 내용을 정리했습니다(공식적인 설명이 아니므로, 참고 부탁드립니다).


      Apple에서 말하는 iPhone의 잠금 상태, 그리고 Apple Watch의 잠금상태/손목 착용 여부는 곧 사용자가 현재 어떤 기기를 사용하고 있는지를 판단하기 위한 기준입니다.

      Apple Watch는 iPhone과 달리, 화면이 항상 켜져 있는 경우는 잘 없습니다.

      사용자들은 대부분 Apple Watch를 일반 시계처럼 착용하고 일부 필요한 경우에만 화면을 터치하기 때문입니다.


      따라서 "잠금 상태" 만으로는 Apple Watch의 사용 여부를 판단하기 힘들기 때문에, "손목 착용 여부"도 함께 판단 기준으로 사용합니다.

      그래서 손목 인식이 꺼져 있는 경우에는 Apple Watch의 사용 여부를 판단할 수 없으므로, iPhone과 Apple Watch의 잠금 상태와 무관하게 두 기기에 모두 Notification이 등장합니다.


      이를 정리하면 아래와 같습니다.

      Apple Watch 손목 인식

      iPhone

      Apple Watch

      Notification이 나타나는 기기

      O

      잠금 ⭕

      잠금 ❌

      Apple Watch

      O

      잠금 ⭕

      잠금 ⭕

      iPhone

      O

      잠금 ❌

      잠금 ❌

      iPhone

      O

      잠금 ❌

      잠금 ❌

      iPhone

      X

      무관

      무관

      iPhone, Apple Watch


      Apple Watch에 Notification을 보냈지만?

      서버에서 Apple Watch를 타깃으로 Remote Notification을 보내면, Apple Watch에만 등장한다고 앞서 말씀드렸습니다.

      그런데 여기에는 큰 문제가 하나 있습니다. 바로 Apple Watch에 Notification이 등장하기까지 시간이 다소 걸린다는 것입니다.


      Apple Watch가 iPhone과 연결이 끊어진 상태에서는 Remote Notification이 바로 등장하지만, iPhone과 연결된 상태에서는 약 10-15초 정도 지연되어 등장합니다.

      공식 문서 어디에도 관련된 내용을 찾을 수 없었는데요, Apple 개발자 포럼에서 정답을 알 수 있었습니다.

      If you have both a phone and watch app installed, watchOS and iOS try to coordinate between phone and watch notifications.

      위 내용을 통해 아래와 같이 추정할 수 있었습니다.

      • Apple Watch에 바로 Remote Notification을 보내더라도, iPhone과 Notification 등장을 조율하는 것 같음

        • 동일한 Notification을 iPhone과 Apple Watch에 둘 다 보낼 경우, 동일한 Notification이 두 단말에 모두 등장하는 것을 방지하기 위해 존재하는 것으로 예상됨

      • Apple Watch가 단독으로 존재하는 상황에서는 조율할 대상이 없기 때문에, 지연 없이 바로 Notification이 노출됨

      Apple Watch에 단독으로 Notification을 등장시키기 위해 Remote Notification을 사용하실 때는, 이 점을 반드시 알고 사용하는 게 좋을 것 같습니다.


      ⌚ Apple Watch Notification

      Apple Watch에서 Notification을 UI에 표현하는 방식은 Short-look Interface, Long-look Interface 총 2가지입니다.

      • Short-look Interface

        • Long-look Interface를 노출하기 전, Notification에 대한 간략한 정보를 노출하는 화면

      • Long-look Interface

        • Notification에 대한 세부적인 정보를 노출하는 화면

      Apple Watch가 알림을 수신하면 Short-look Interface가 잠시 노출되고, 이후 약 1-3초가 지난 후 Long-look Interface가 노출됩니다.


      Short-look Interface

      image.png

      Short-look Interface는 watchOS가 자동으로 생성하는 화면이고, 아래 3가지 정보를 노출합니다.

      • 앱 아이콘

      • First Line

      • Second Line

      공식 문서 상에는 First Line에는 앱 이름, Second Line에는 Notification Title이 노출된다고 나와 있습니다.


      실제로는 Notification Payload 값, 그리고 Apple Watch 설정(Watch 앱 > 알림 > 탭하여 전체 알림 보기)에 따라 First Line과 Second Line에 노출되는 값이 바뀝니다.

      image.png

      탭하여 전체 알림 보기

      Notification Payload 구성 요소

      First Line

      Second Line

      ON

      Title만 존재

      Notification Title

      앱 이름

      ON

      Body만 존재

      앱 이름

      Notification Body

      ON

      Title & Body 존재

      Notification Title

      Notification Body

      Off

      Title만 존재

      Notification Title

      앱 이름

      Off

      Body만 존재

      앱 이름

      "알림" 텍스트

      Off

      Title & Body 존재

      Notification Title

      앱 이름

      참고로 “알림” 텍스트는 진짜 말 그대로 “알림”이 노출됩니다.


      Long-look Interface

      image.png

      Long-look Interface는 개발자가 정의하는 화면이고, 아래 정보들을 노출합니다.

      • System-provided Sash

      • App Content

        • SwiftUI View를 노출하기 때문에 Image, Button 등 원하는 형태의 화면 노출 가능

      • App-defined actions

        • 개발자가 정의하는 Action들이 노출

      • System-provided Dismiss Button

        • watchOS가 기본적으로 제공하는 “닫기” 버튼, 임의로 설정 불가

      Long-look Interface는 Short-look Interface와 다르게 개발자가 설정할 수 있는 내용이 많습니다. 아래에서 자세하게 살펴보겠습니다.


      🏗️ Long-look Interface 만들기

      Long-look Interface는 **WKNotificationHostingController**를 통해 정의할 수 있습니다. 한 번 살펴볼까요?

      @MainActor open class WKUserNotificationHostingController<Body> : WKUserNotificationInterfaceController where Body : View {
       
          @MainActor open class var isInteractive: Bool { get }
       
          @MainActor open class var sashColor: Color? { get }
       
          @MainActor open class var wantsSashBlur: Bool { get }
       
          @MainActor open class var titleColor: Color? { get }
       
          @MainActor open class var subtitleColor: Color? { get }
       
          @MainActor open class var coalescedDescriptionFormat: String? { get }
       
          @MainActor open var body: Body { get }
       
          @MainActor override dynamic public init()
      }

      SwiftUI에 익숙하신 분들은 눈치채셨겠지만, WKUserNotificationHostingController는 Long-look Interface 구현 시 WatchKit에서 SwiftUI를 사용하기 위해 정의된 클래스입니다.


      UIKit과 SwiftUI를 같이 써보신 분들은 **UIHostingController**를 알고 계실텐데요,

      WKUserNotificationHostingController도 비슷합니다. WatchKit 기반의 Long-look Interface가 SwiftUI를 사용하기 위해 정의된 클래스입니다.

      watchOS 6부터 SwiftUI를 부분적으로 지원하기 시작했고, watchOS 10 기준으로는 앱 내 모든 View는 SwiftUI를 통해 구현할 수 있습니다.

      하지만 아직 Notification의 경우에는 WatchKit 기반으로 구현해야 합니다.


      물론 Long-look Interface의 App Content에 해당하는 영역,

      즉 실제 노출되는 화면은 모두 SwiftUI로 구성이 가능하기 때문에 Life Cycle이나 Notification 관련 설정만 WatchKit을 통해 구현한다고 생각하시면 될 것 같습니다.

      WKUserNotificationHostingController에서는 UI와 관련된 설정만 정의되어 있고, 나머지는 모두 상위 클래스에 정의되어 있습니다.

      클래스 상속 구조는 아래와 같습니다.

      image.png

      WKUserNotificationInterfaceControllerWKInterfaceController의 경우, 다양한 프로퍼티와 메소드를 정의하고 있습니다.

      이번 게시글에서는 Long-look Interface 구현과 관련된 최소한의 내용만 알아보도록 하겠습니다.


      WKInterfaceController

      // WKInterfaceController
      @MainActor 
      open class WKInterfaceController: NSObject {
          
          // Push나 Modal을 수행한 Controller로부터 전달 받은 Context
          open func awake(withContext context: Any?) 
      
          // Watch Interface가 활성화 되었고, 업데이트 가능할 때 호출됩니다. 
          // UI 상 노출되지 않더라도 호출될 수 있습니다.
          open func willActivate()
      
          // Watch Interface가 UI에 노출될 때 호출됩니다.
          open func didAppear()
      
          // Watch Interface가 더 이상 UI에 노출되지 않을 때 호출됩니다.
          open func willDisappear()
      
          // Watch Interface가 비활성화 되고, 더 이상 업데이트가 불가능할 때 호출됩니다.
          open func didDeactivate()
          
          ...
      }

      WKInterfaceController는 UIKit의 UIViewController 역할을 하는 클래스입니다.

      위 코드 블록에는 Life Cycle을 확인할 수 있는 메소드 위주로 추렸습니다.

      총 5가지의 메소드가 있고, 호출 순서는 **awake → willActivate → didAppear → willDisappear → didDeactivate`**입니다.


      실제로 그런지 확인해 볼까요?

      
      class NotificationController: WKUserNotificationHostingController<NotificationView> {
         ...
          
         override func awake(withContext context: Any?) {
             print("🔧 awake with context: \\(String(describing: context))")
         }
          
         override func willActivate() {
             print("🔧 willActivate")
         }
          
         override func didAppear() {
             print("🔧 didAppear")
         }
          
         override func willDisappear() {
             print("🔧 willDisappear")
         }
          
         override func didDeactivate() {
             print("🔧 didDeactivate")
         }
      }
      
      // Console Output
      🔧 awake with context: nil
      🔧 willActivate
      🔧 didAppear
      🔧 willDisappear
      🔧 didDeactivate
      🔧 willDisappear // <-- willDisappear가 didDeactivate 뒤에 한 번 더 불리는 현상이 있습니다 (watchOS 9, 10 기준)

      Long-look Interface가 등장해서 사라질 때까지 로그를 print로 출력했습니다.

      다 정상적으로 나오는 것 같은데, didDeactivate 뒤에 willDisappear가 한 번 더 불리고 있네요?

      공식 문서상에 willDisappear가 여러 번 호출될 수 있다는 말이 없는 것으로 보아, watchOS 버그로 예상됩니다.


      WKUserNotificationInterfaceController

      @MainActor open class WKUserNotificationInterfaceController : WKInterfaceController {
              ...
           
          // notificationActions는 didRecieve(_:)가 호출되었을 때 한 번만 변경할 수 있습니다.
          open var notificationActions: [UNNotificationAction]
           
          // Notification Interface가 화면에 노출되기 전에 불립니다.
          open func didReceive(_ notification: UNNotification)
           
          open func performNotificationDefaultAction()
       
          open func performDismissAction()
      }

      WKUserNotificationInterfaceControllerWKInterfaceController를 상속해 Notification과 관련된 프로퍼티와 메소드를 추가로 정의하고 있습니다.

      하나씩 살펴보겠습니다.

      우선 notificationActions는 Long-look Interface 하단에 노출되는 App-defined actions에 해당합니다.

      iOS에서, 알림센터에 노출된 배너를 꾹 눌렀을 때 나타나는 Action과 동일합니다.

      하지만 Notification Action 측면에서 iOS와 가장 큰 차이점이 바로 이 부분인데요,

      그 이유는 동적으로 Action을 변경할 수 있기 때문입니다. 예시를 한 번 보겠습니다.

      class NotificationController: WKUserNotificationHostingController<NotificationView> {
          ...
       
          override func didReceive(_ notification: UNNotification) {
              let modelData = ModelData()
       
              let notificationData = notification.request.content.userInfo as? [String: Any]
       
              let aps = notificationData?["aps"] as? [String: Any]
              let alert = aps?["alert"] as? [String: Any]
       
              title = alert?["title"] as? String
              message = alert?["body"] as? String
       
              if let index = notificationData?[landmarkIndexKey] as? Int {
                  landmark = modelData.landmarks[index]
       
                              // index에 따라 다른 Notification Action 노출
                  switch index {
                  case 1...3:
                      let action = UNNotificationAction(identifier: UUID().uuidString, title: "🎉🎉🎉", options: .foreground)
                      self.notificationActions = [action]
                  default:
                      let action = UNNotificationAction(identifier: UUID().uuidString, title: "❤️❤️❤️", options: .destructive)
                      self.notificationActions = [action]
                  }
              }
          }
      }

      샘플 프로젝트 > PushNotificationPayload.apns 파일 내 "landmarkIndex" 값에 따라, 다른 형태의 Notification Action이 노출되도록 설정했습니다. 한번 확인해 봅시다.

      landmarkIndex = 1인 경우

      landmarkIndex = 5인 경우

      Notification_Action_1-2.gif

      Notification_Action_5-2.gif

      "landmarkIndex": 1일 때는 🎉🎉🎉가 노출되고, "landmarkIndex": 5일 때는 ❤️❤️❤️가 노출되는 것을 확인할 수 있습니다.

      보신 것처럼 동일한 Long-look Interface에 대해 Notification Action을 다르게 노출할 수 있습니다.

      사용자 타입에 따라 다른 Action을 노출하는 등에 활용할 수도 있겠네요.

      여기서 중요한 점이 2가지 있습니다.

      첫 번째는, 반드시 **didReceive(_:)**를 구현해야 한다는 것입니다. 만약 구현하지 않으면 아래와 같은 오류를 Console에서 볼 수 있는데요.

      … doesn't implement didReceiveNotification: or didReceiveNotification:withCompletion:. Just dropping the notification

      **didReceive(_:)**를 구현하지 않았기 때문에, Long-look Interface를 등장시키지 않고 그냥 무시하게 됩니다.

      물론 Notification이 아예 등장하지 않는 것은 아니고, Long-look Interface가 아닌 watchOS가 생성하는 일반 Notification의 형태로 등장합니다

      (Long-look Interface로 설정하지 않은 모든 알림은 watchOS가 생성하는 일반 Notification의 형태를 따릅니다).

      따라서 이 점을 반드시 명시하셔야 합니다.

      두 번째는 반드시 notificationActionsdidReceive(_:) 내에서만 변경해야 한다는 것입니다.

      다른 메소드 (didAppear 등)에서는 변경할 수 없습니다.


      App-defined Action 사용 시 주의 사항

      여기까지만 보면, watchOS에서 제공하는 App-defined Action이 매력적으로 보입니다.

      동일한 Notification에 대해서도, Notificaion Payload 값에 따라 다른 Notification Action을 등장시킬 수 있기 때문입니다.

      아쉽게도 하나 문제점이 있습니다. 바로 일반 Notification과 Long-look Interface를 앱 내에서 함께 사용할 때입니다.

      (ex. 통화 요약 알림은 일반 Notification으로, 전화 수신 알림은 Long-look Interface로 제공)

      2024-11-2108.08.15-ezgif.com-video-to-gif-converter.gif

      보신 것처럼 일반 Notification이 등장한 상태에서 Long-look Interface를 위한 Notification을 수신하는 경우,

      일반 Notification에 Long-look Interface의 Notification Action이 노출되는 버그가 있습니다.

      정상 동작은 일반 Notification을 Dismiss한 이후 Long-look Interface가 등장하고, Long-look Interface의 Notification Action이 일반 Notification에 노출되지 않는 것입니다.

      따라서 일부 Notification만 Long-look Interface로 등장시키고 싶으시다면, 이 부분을 염두에 두시고 진행하시면 좋을 것 같습니다.


      DefaultAction, DismissAction

      이번에는 performNotificationDefaultActionperformDismissAction에 대해 알아보겠습니다.

      UNNotificationAction은 서로 다른 Action을 구분하기 위해 identifier: String을 프로퍼티로 가집니다.

      이 중 identifier의 값이 UNNotificationActionIdentifier를 가지는 Action이 바로 defaultAction에 해당합니다.

      그러면 이 defaultAction은 개발자가 지정할 수 있을까요?

      정답은 “아니다”입니다. 이미 watchOS에서 ‘화면에 노출된 Notification 그 자체를 누르는 경우’로 정의하고 있기 때문에, 개발자가 별도로 지정할 수는 없습니다.

      단순히 Notification을 눌러서 앱에 진입한 경우와, 특정한 Notification Action에 해당하는 버튼을 눌러서 앱에 진입한 경우를 구분하고 싶을 때 사용할 수 있을 것 같네요.

      그러면 performNotificationDefaultAction을 수행하면 어떻게 될까요? 한번 확인해 봅시다.

      class NotificationController: WKUserNotificationHostingController<NotificationView> {
          ...
           
          override func didAppear() {
                  // 화면에 노출되고 나서 2초 뒤에 실행
              DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
                  guard let self else { return }
       
                  self.performNotificationDefaultAction()
              }
          }
      }

      2024-11-2108.10.11-ezgif.com-video-to-gif-converter.gif

      보신 것처럼, 화면에 노출되고 2초 뒤에 앱이 실행됩니다.

      그러면 '특정 Notification에 대해 앱이 즉시 실행되게 할 수 있겠네?'라는 생각이 자연스럽게 드실 수 있습니다.

      아쉽지만 그렇게는 동작하지 않습니다. 일정 시간 딜레이를 주지 않고 performNotificationDefaultAction을 수행하면, Default Action이 수행되지 않습니다.

      performDismissAction은 System-defined dismiss action, 즉 Long-look Interface에 기본 제공되는 “닫기” 동작을 수행합니다.

      이 역시 performNotificationDefaultAction와 유사하게, Long-look Interface가 화면에 노출되고 일정 시간 후부터 정상 동작하게 됩니다.


      여러 개의 Long-look Interface 제공하기

      Long-look Interface의 장점 중 하나는, 여러 개의 Notification에 대해 제공할 수 있다는 것입니다.

      Long-look Interface는 Notification Category에 맞게 등장합니다.

      UNNotificationCategory.identifier가 바로 Notification Category를 식별할 수 있는 값입니다.

      Remote Notification의 경우에는 APNs Payload 내 **aps["category"]`**필드가 해당합니다.

      // 샘플 프로젝트 > PushNotificationPayload.apns
      {
          "aps": {
              ...
              "category": "LandmarkNear", <-- CategoryIdentifier
          },
             ...
      }

      watchOS 앱이 Notification을 수신하면, 해당 Notification의 Category를 확인한 다음 그에 맞는 Long-look Interface를 등장시키게 됩니다.

      이를 위해선 몇 가지 작업이 필요한데요, 한번 확인해 보겠습니다.

      우선 Long-look Interface를 구성할 WKUserNotificationHostingController를 정의해야 합니다.

      샘플 프로젝트의 NotificationController가 여기에 해당하는데요, 앞서 살펴보았으니 여기에선 넘어가겠습니다.

      이제 원하는 WKUserNotificationHostingController와 Notification Category를 연결해야 합니다.

      이를 위해선 WKNotificationScene을 선언해 줘야 합니다. 이름에서 알 수 있듯이 Scene이기 때문에, SwiftUI의 App 내에 정의해 줘야 합니다.

      샘플 프로젝트의 LandmarksApp.swift 파일을 살펴볼까요?

      @main
      struct LandmarksApp: App {
          ...
       
          var body: some Scene {
              ...
       
              #if os(watchOS)
              WKNotificationScene(controller: NotificationController.self, category: "LandmarkNear")
              #endif
          }
      }

      보시면 controllerNotificationController.self, category로 **"LandmarkNear"**를 전달하고 있습니다.

      이를 통해 watchOS는 **"LandmarkNear"**를 Category Identifier로 가지는 Notification을 수신하면 NotificationController로 구성된 Long-look Interface를 띄울 수 있습니다.


      예시로 간단한 텍스트를 보여주는 Long-look Interface를 추가로 정의했습니다.

      // HelloWorldNotificationController.swift
      class HelloWorldNotificationController: WKUserNotificationHostingController<HelloWorldView> {
          private var maxTimerCount: CGFloat = 0
       
          override var body: HelloWorldView {
              HelloWorldView()
          }
           
          override func didReceive(_ notification: UNNotification) {
              super.didReceive(notification)
          }
      }
       
      // HelloWorldView.swift
      struct HelloWorldView: View {
          var body: some View {
              VStack {
                  Text("🌏")
                      .font(.largeTitle)
                   
                  Text("Hello, World!")
                      .font(.body)
                      .bold()
              }
          }
      }
       
      // HelloWorldNotification.apns
      {
          "aps": {
              "alert": {
                  "title": "Hello, World",
              },
              "category": "HelloWorld",
              "thread-id": "777"
          },
       
          "Simulator Target Bundle": "com.example.apple-samplecode.Landmarks.watchkitapp"
      }
       
       
       
       
      //LandmarksApp.swift
      @main
      struct LandmarksApp: App {
          ...
       
          var body: some Scene {
              ...
              #if os(watchOS)
              WKNotificationScene(controller: NotificationController.self, category: "LandmarkNear")
              WKNotificationScene(controller: HelloWorldNotificationController.self, category: "HelloWorld")
              #endif
          }
      }

      2024-11-2108.50.44-ezgif.com-video-to-gif-converter.gif


      ✏️ 로그 수집

      watchOS 앱 로그 보기

      watchOS 앱 개발을 시작하면서, 가장 당황스러웠던 점은 로그가 보이지 않는 것이었습니다.

      물론 print로 출력하는 내용은 Xcode 콘솔에 잘 보였지만, Logger 등을 통해 출력한 로그는 전혀 보이지 않았습니다.


      iPhone과 다르게, Apple Watch에서 로그를 보기 위해서는 Sysdiagnose 프로파일을 설치해야 합니다.

      해당 프로파일은 Apple에 버그 리포트 등을 하기 위한 시스템 로그 수집할 때 사용하는 프로파일인데요, 이유는 모르겠지만 Apple Watch는 해당 프로파일을 반드시 설치해야 로그를 출력할 수 있었습니다.


      해당 프로파일은 Apple Developer 사이트에서 설치하실 수 있습니다.

      프로파일 다운로드 후, 로그 수집을 원하는 Apple Watch와 연결된 iPhone에서 해당 프로파일을 설치하시면 됩니다.

      image.png

      Apple Watch 로그 수집하기

      watchOS 앱 개발을 하다보면, 디버깅을 위한 로그 수집도 매우 중요합니다.

      로컬에 파일을 저장하고, 해당 파일을 추출하는 방식 등으로 구현하실 수 있을 것 같네요.

      그러나 개발한 앱 로그뿐만 아니라, 시스템이 남긴 로그도 봐야 하는 경우가 종종 있습니다.

      iOS 앱 등을 개발하시는 분들이라면 Mac과 연결한 다음 Console 앱을 통해서 시스템 로그를 보면 된다고 생각하실 수 있습니다.

      문제는 이 방식이 Apple Watch에서는 다르게 동작한다는 점입니다.


      Apple Watch의 경우 Mac에서 로그를 보려면 반드시 iPhone과 연결된 상태에서만 가능하고, iPhone과 연결이 끊어진 상태에서는 볼 수 없습니다.

      하지만 앞서 말씀드린 Sysdiagnose 기능을 사용하면, iPhone과의 연결과 상관 없이 로그를 수집할 수 있습니다.

      그리고 앞서 말씀드린 것처럼, 본래 Sysdiagnose는 Apple에 버그 리포트 등을 위한 기능이기 때문에 앱 뿐만 아니라 OS가 남기는 로그까지 함께 볼 수 있습니다.


      Apple Watch에서 Sysdiagnose를 수집하는 방법은 아래와 같습니다.

      1. Apple Watch의 Digital Crown과 측면 버튼을 동시에, 약 2초간 누르기 (손가락을 버튼에서 뗀 이후, 햅틱 피드백이 오면 로그 수집 시작됩니다)

        image.png

      2. iPhone > Watch 앱 > 일반 > 진단 로그 > SYSDIAGNOSE 섹션에서 수집된 로그 파일 다운로드

        image.png

      3. 확인을 원하는 파일 탭 → 좌측 하단 공유하기 버튼을 통해 Mac 등으로 공유 후 파일 압축 해제

        image.png

      4. system_logs.logarchive 파일 확인

        image.png

      위 방법을 이용하면, iPhone과 연결 여부와 상관없이 Apple Watch의 시스템 로그를 수집할 수 있습니다.

      이처럼 Apple Watch 단독 상태에서 로그를 보고 싶으실 때, Sysdiagnose를 참고하시면 좋을 것 같습니다.


      🤔 아니 이게 안 된다고?

      watchOS 앱을 개발하면서, 정말 별의별 버그들을 본 것 같습니다.

      아직 제가 초보 개발자라서 그런 것도 있겠지만, iOS 앱이랑 비교하면 정말 쉽지 않았습니다.

      팀에 시니어 개발자분은 옛날 iOS를 보는 듯한 느낌이라고 하셨습니다 (Obj-c 시절 iOS 개발자분들.. 항상 존경합니다).

      개발자 문서에서 나와 있지 않은 내용도 많았던 것 같고, 참고할 만한 코드도 iOS에 비하면 턱 없이 모자랐습니다.

      결국 직접 시도해 보고, 현상으로 유추하면서 알아낸 것들도 많았습니다.

      그래도 덕분에 이것저것 많은 것들을 시도해 보면서, 공부도 많이 되고 실력도 조금은 늘어난 것 같아서 뿌듯한 경험이었습니다.


      긴 글 읽어주셔서 감사드리며, 다음에 다른 글로 또 찾아 뵙겠습니다 🙇

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      jylee23 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기