Implementing Handoff in Your App

Panther·2021년 7월 28일
0

https://developer.apple.com/documentation/foundation/task_management/implementing_handoff_in_your_app

"Create, send, and receive user activities directly."

사용자 활동을 직접 생성, 전송, 수신합니다.

Overview

사용자가 iOS, watchOS, macOS 기기에서 시작한 활동을 다른 기기로 전송하기 위해 핸드오프를 사용할 수 있습니다. 예를 들어 macOS에 있는 벡터 그래픽스 앱은 아이폰에서 편집을 지속할 수 있도록 사용자의 아이폰에 편집중인 액션에 대한 세부사항을 보낼 수 있습니다.

앱에서 다음을 구현해 핸드오프를 사용할 수 있습니다.

  • NSUserActivity의 인스턴스로써 사용자 활동을 나타낼 수 있습니다.
  • 앱에서 사용자가 액션을 수행할 때 활동 인스턴스를 업데이트할 수 있습니다.
  • 다른 기기에서 앱에 핸드오프로부터 활동을 수신할 수 있습니다.

Important
다른 플랫폼에 대한 앱 사이의 핸드오프를 하려면, 앱은 같은 개발자 팀 ID를 공유해야 합니다. 이는 앱 스토어를 통해 앱을 배포해야 하거나 동일한 자격증명으로 서명해야 한다는 것을 의미합니다.

Declare Handoff Activities in Your App’s Info.plist

어떤 활동이 핸드오프를 통해 사용할 수 있는지를 확인하는 것에서 시작합니다. 어떤 모양을 그리거나 문서 속성을 편집하는 것과 같은 특정한 시점이 있는 경우처럼 사용자가 하고 있었던 행동을 나타낼 수 있는 활동을 선택해야 합니다. com.example.app.activity-name처럼 reverse-DNS pattern을 사용해 각각의 활동에 고유한 아이덴티파이어 스트링을 선택해야 합니다.

앱이 핸드오프로부터 활동을 수신할 수 있다는 것을 선언하기 위해 Info.plist 파일을 사용할 수 있습니다. 키 NSUserActivityTypes와 타입 배열을 포함해 이 파일에 새로운 상위 수준 엔트리를 생성해야 합니다. 배열의 각 멤버는 값이 활동 아이덴티파이어인 스트링이어야 합니다. 아래 예시는 앱이 계속 할 수 있는 세 가지 활동을 선언한 NSUserActivityTypes 엔트리의 Info.plist XML 소스를 보여주고 있습니다.

<key>NSUserActivityTypes</key>
<array>
    <string>com.example.myapp.create-shape</string>
    <string>com.example.myapp.edit-shape</string>
    <string>com.example.myapp.edit-document-properties</string>
</array>

앱이 모든 플랫폼에서 동일한 집합의 아이덴티파이어를 보내거나 받을 필요는 없습니다. 예를 들어 큰 규모의 macOS 앱과 작은 규모의 iOs 앱이 존재하는 경우입니다. 이 경우 macOS 앱은 각 iOS 앱이 활동들의 하위집합을 처리하는 반면 macOS는 모든 활동들을 처리할 것입니다. 또한, watchOS가 사용자 활동을 보낼 수 있는 반면 받지는 못합니다. 그렇기에 watchOS는 NSUserActivityTypes 속성을 선언하지 않습니다.

앱이 핸드오프에 보내려는 세부사항이 각기 다른 많은 할동을 갖고 있을 수도 있습니다. 수신하는 기기에 활동을 재생성해야 하는 정보를 확인해야 합니다. 영구적으로 저장해야 하는 정보가 아닌 사용자의 일시적은 세부사항만 포함해야 함을 기억해야 합니다. 예를 들어 만약 사용자가 문서를 작업하고 있다면, 활동은 사용자가 편집하고 있는 문서(문서의 일부분도 가능)를 나타내야 합니다. 활동의 일부로 문서 자체를 포함하지 않아야 합니다. 사용자가 앱 아이콘을 탭하거나 클릭함으로써 핸드오프 없이 앱을 launch하길 원할 수도 있기 때문입니다. 대신 사용자의 기기 사이에서 문서를 공유할 수 있는 아이클라우드 드라이브와 같은 테크닉을 사용하시기 바랍니다.

Create User Activity Objects

런타임에 앱의 각 활동에 대한 NSUserActivity의 인스턴스를 생성해야 합니다. 어떤 활동이 앱에서 지속되어야 하는지를 나타낼 수 있도록 Info.plist에 사용된 동일한 아이덴티파이어 스트링을 사용해야 합니다.

NSUserActivity는 다른 기기에서도 활동을 재생성 하는 데 사용할 수 있도록 해주는 userInfo 딕셔너리를 포함하고 있습니다. 활동 타입은 활동이 복구가 가능한 상태일 수 있도록 해주는 최소한의 딕셔너리 키 집합 생성을 위한 requiredUserInfoKeys 속성을 갖고 있습니다. 활동에는 개발 시 설정해줘야 하는 user-readable 제목 속성 또한 포함합니다. 만약 활동이 검색도 지원한다면, 시스템은 검색 결과에 이 제목을 표시합니다.

let activity = NSUserActivity(activityType: "com.example.myapp.create-shape")
activity?.isEligibleForHandoff = true
activity?.requiredUserInfoKeys = ["shape-type"]
activity.title = NSLocalizedString("Creating shape", comment: "Creating shape activity")

NSResponder (macOS)와 URResponder (iOS) 클래스는 userActivity 속성을 정의합니다. NSViewControllerUIViewController는 이 리스폰더 타입의 서브클래스이기 때문에 컨트롤러가 다루는 활동을 표현하기 위해 이 속성을 설정할 수 있습니다. 앱은 여러 뷰 컨트롤러에 걸쳐 하나의 활동을 공유할 수 있습니다. 역으로 만약 단일 뷰 컨트롤러가 여러 활동을 지원한다면, 필요한 경우 뷰 컨트롤러의 userActivity를 다른 NSUserActivity 인스턴스들에 재할당할 수 있습니다.

Update Activities While the User is Active

사용자가 앱과 상호작용할 때, 해당 상호작용의 상태를 저장할 수 있도록 사용자 활동을 업데이트해야 합니다. 리스폰더의 userActivity 속성 집합을 갖고 있다면, 시스템은 자동으로 이 속성 집합의 ipdateUserActivityState(:) (iOS) 혹은 updateUserActivityState(:) (macOS) 메소드를 호출합니다. 활동의 userInfo 딕셔너리에 새로운 값을 써서 이 메소드를 오버라이드하시기 바랍니다.

userInfo에서 사용하는 키와 값은 NSArray, NSData, NSDate, NSDictionary, NSNull, NSNumber, NSSet, NSString, NSURL (혹은 스위프트 브릿지에 동등한) 타입을 사용해야 합니다. 다른 기기에서 활동이 재생성될 필요가 있는 모든 데이터에 딕셔너리를 생성해야 합니다. 그리고 활동을 업데이트하기 위해 addUserInfoEntries(from:)을 호출해야 합니다. 사전 자체를 버전화할 수 있는 키-값 쌍을 제공하는 것도 좋은 방법입니다. 이 방법의 경우 활동의 사전 표현을 이후에 변경시킬 수 있고, 호환되지 않는 버전을 감지할 수 있습니다.

override func updateUserActivityState(_ activity: NSUserActivity) {
    if activity.activityType == "com.example.myapp.create-shape" {
        let updateDict:  [AnyHashable : Any] = [
            "shape-type" : currentShapeType(),
            "activity-version" : 1
        ]
        activity.addUserInfoEntries(from: updateDict)
    }
}

전체 크기는 3KB 아래를 유지하면서 가능한 userInfo은 작은 페이로드를 전송해야 합니다. 이보다 더 큰 데이터를 전송해야 한다면, 두 기기를 직접 연결하기 위한 continuation streams를 사용해야 합니다(Working with Continuation Streams를 살펴보시기 바랍니다).

Working with Continuation Streams
https://velog.io/@panther222128/NSUserActivity

Receive User Activities in the Application Delegate

사용자가 다른 기기에서 핸드오프로부터 앱을 launch할 때, 앱은 application delegate에 있는 메소드에 대한 콜백을 받습니다. 활동을 허용하기 위해 이 메소드들을 구현해야 하고, 앱에서 상태를 복구할 수 있도록 해야 합니다.

앱 launch 이후 핸드오프는 UIApplicationDelegateapplication(:willContinueUserActivityWithType:) (iOS) 메소드 혹은 NSApplicationDelegateapplication(:willContinueUserActivityWithType:) 메소드를 호출합니다. UI 업데이트를 함으로써 사용자에게 다른 기기로부터 활동을 수신중이라는 것을 나타낼 수 있도록 이 메소드를 구현하시기 바랍니다. 어떠한 이유로 핸드오프가 실패하면, 시스템은 application(:didFailToContinueUserActivityWithType:error:) (iOS) 혹은 application(:didFailToContinueUserActivityWithType:error:) (macOS)를 호출합니다.

Note
watchOS는 NSUserActivity 객체를 생성할 수 있고 이 객체를 다른 기기에 전송할 수 있지만, 핸드오프는 watchOS 앱을 launch할 수 없습니다.

핸드오프는 application(:continue:restorationHandler:) (iOS) 혹은 application(:continue:restorationHandler:) (macOS) 딜리게이트 메소드에서 앱에 활동을 제공할 수 있습니다. 활동에 대한 업데이트가 필요한 뷰 컨트롤러의 배열을 생성함으로써 메소드를 구현하시기 바랍니다. 그리고 이 배열을 컴플리션 핸들러에 제공하시기 바랍니다. 구현이 활동을 성공적으로 처리할 경우 true를 반환하고 그렇지 않은 경우 false를 반환합니다. 아래 예시는 앱 딜리게이트가 자신의 상위 뷰 컨트롤러를 찾는 것과 이 상위 뷰 컨트롤러에 커믚ㄹ리션 핸들러를 제공하는 iOS 앱 딜리게이트를 보여주고 있습니다.

func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard let topNav = application.keyWindow?.rootViewController as? UINavigationController,
        let shapesVC = topNav.viewControllers.first as? MyShapesViewController else {
            return false
    }
    restorationHandler([shapesVC])
    return true
}

Continue the Activity in Your App

이전 단계에서 컴플리션 핸들러에 제공된 각각의 뷰 컨트롤러는 restoreUserActivityState(:) (iOS) 혹은 restoreUserActivityState(:) (macOS) 메소드에 대한 호출을 받습니다. 기존 기기의 상태에 일치할 수 있도록 하는 뷰 컨트롤러의 상태를 업데이트하려면 이 메소드를 사용하시기 바랍니다. 몇 가지 타입의 활동을 갖고 있는 경우 activityType을 사용해 어떤 활동을 처리해야하는지 결정할 수 있도록 해야 합니다. 그러면 뷰 컨트롤러의 상태를 업데이트하기 위해 활동의 userInfo 딕셔너리로부터 값을 가져옵니다.

override func restoreUserActivityState(_ userActivity: NSUserActivity) {    super.restoreUserActivityState(userActivity)
    guard userActivity.activityType == "com.example.myapp.create-shape",
        let type = userActivity.userInfo?["shape-type"] as? String,
        let version = userActivity.userInfo?["activity-version"] as? Int,
        version >= 1 else {
            return
    }
    
    createShape(type: type)
}

userInfo 딕셔너리에서 전송된 URL의 경우 우선 startAccessingSecurityScopedResource()를 호출해고, 이 메소드는 URL 접근할 수 있기 전에 true를 반환해야 합니다. 또한, 수신하는 기기에서 같은 문서를 가리킬 수 있도록 시스템이 아이클라우드 문서에 가리키고 있는 file: URL을 수정한다는 것을 알고 있어야 합니다.

0개의 댓글