Action Extension

pcsoyeon·2021년 11월 29일
0

Sharing and Actions - Extensions - iOS - Human Interface Guidelines - Apple Developer

먼저 HIG 문서를 살펴보면

사용자는 해당 기능을 통해 앱에서 작업 버튼을 눌러 작업들을 확인할 수 있고 이 중 원하는 Sharing Extension 또는 Action Extension에 접근한다.

Sharing Extension의 경우 앱, 소셜 미디어 계정 및 기타 서비스와 현재 앱의 정보를 공유할 수 있는 편리한 방법을 제공한다.

Action Extension의 경우 해당 기능을 통해 사용자는 책갈피 추가, 링크 복사, 이미지 저장과 같은 현재 서비스의 내용에 맞는 작업을 시작할 수 있다.

Activity View에는 현재 컨텍스트와 관련된 extensions만 표시된다. 예를 들어 동영상을 편집하는 동안에는 텍스트 조작 작업이 표시되지 않는다. Activity View 내에서 Sharing Extension은 Action Extension보다 위에 위치되어 있다.

Action Extension

실제 앱에서 많이 사용하는 Action Extension을 구현하고 싶었으나 일단 구글링 했을 때 많이 나와 있는 TextView에 대한 Action을 구현했다.

구현 방법

File > New > Target 에서 Action Extension을 선택하고 activate를 하면 (QR 위젯을 했을 때처럼) 새로운 파일이 만들어진다.

파일 안의 MainInterface가 Activity View에서 해당 Action을 선택했을 시 보여지는 화면이고 해당 화면과 연결된 구현부는 ActionViewController에서 작성하면 된다. (우리가 자주 사용하는 스보-뷰컨의 관계)

  • 궁금한 점 찾아본 자료에서는 Action Extension의 Info 파일에서 설정해야하는 키 값이 있다고 하는데 .. NSExtensionActivationSupportsText 라는 키를 추가하고 Boolean 타입으로 YES를 할당 .. 뭐 이런거 근데 이거를 안해도 잘 작동이 되는데 업데이트가 되면서 사라진 것인지 .. ?


→ Action 화면에서 보여지는 TextView의 경우 여기서 따로 edit을 하는 것이 아니므로, 오른쪽 인스펙터 창에서 Editable 옵션을 해제한다.

ActionViewController에서 수행하는 작업은 크게 두가지로 나뉘어진다.

  1. 호스트 앱에서 넘겨받은 데이터를 특정한 형태로 가공하는 작업
  2. 가공한 데이터를 다시 호스트 앱에게 넘기는 작업

그리고 이렇게 가공된 작업을 호스트 앱에서 받아 해당 앱의 성격에 맞게 사용하면 된다.

Host App

1. 호스트 앱 → Action

호스트 앱에서 버튼을 누르면 ActivityViewController를 호출하고 이와 동시에 TextView에 있는 데이터를 같이 넣어서 보낸다. 그리고 호출된 ActionViewController는 해당 데이터를 처리할 수 있는 액션을 보여준다. (호스트 앱에서 이미지에 대한 작업을 하고 싶을 때에는 액션에 텍스트 관련 액션이 보여지지 않도록)

먼저 호스트 앱의 UI는 아래와 같이 textview와 button으로 구성되어 있다.


그리고 호스트 앱에서 작성한 코드는 다음과 같다.

import UIKit
import MobileCoreServices

class ViewController: UIViewController {
    
    @IBOutlet weak var textView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func touchUpChangeButton(_ sender: UIButton) {
				// ✅ action으로 전달할 문자열 배열 선언
        var objectsToShare = [String]()
        
				// ✅ 호스트 앱의 데이터를 전달할 문자 데이터에 append
        if let text = textView.text {
            objectsToShare.append(text)
        }
        
				// ✅ UIActivityViewController를 통해 해당 앱의 데이터에 맞는 action 제시 + 현재 화면에서 present
        let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
        activityVC.excludedActivityTypes = [UIActivity.ActivityType.airDrop, UIActivity.ActivityType.addToReadingList]
        self.present(activityVC, animated: true, completion: nil)
        
				// ✅ UIActivityViewController가 dismiss 되고 난 뒤에 실행되는 코드
        activityVC.completionWithItemsHandler =
        { (activityType, completed, returnedItems, error) in
            
            if returnedItems!.count > 0 {
                
                let textItem: NSExtensionItem =
                returnedItems![0] as! NSExtensionItem
                
                let textItemProvider =
                textItem.attachments![0] as! NSItemProvider
                
                if textItemProvider.hasItemConformingToTypeIdentifier(
                    kUTTypeText as String) {
                    
                    textItemProvider.loadItem(
                        forTypeIdentifier: kUTTypeText as String,
                        options: nil,
                        completionHandler: {(string, error) -> Void in
                            let newtext = string as! String
                            DispatchQueue.main.async {
                                self.textView.text = newtext
                            }
                        })
                }
            }
        }
        
    }
}

위의 코드에서 ?? 한 부분들을 위주로 정리하자면,

.excludedActivityTypes

action 기능 중에서 제외할 기능이 있다면 [] 형태로 적으면 된다. 위에서는 UIActivity.ActivityType.airDrop , UIActivity.ActivityType.addToReadingList 이렇게 두가지를 제외한 것이다. 해당 설정은 optional이므로 호스트 앱의 성격에 따라서 다르게 설정하면 된다.

completionWithItemsHandler

위의 주석에 적은 것과 같이 Action이 실행되고 나서 action view controller에서의 작업이 done 되었을 때 호출된다.

파라미터는 4가지로 액티비티 타입을 받는 activityType, 액션의 성공 여부를 알 수 있는 completed (이 값이 false인 경우는 action에서 아무런 작업을 하지 않고 넘겼을 때 등으로 설정할 수 있다.), 가공된 데이터를 받는 returnedItems (수정 및 가공된 데이터를 받는 배열로 호스트 앱의 기존 데이터를 바꿀 때 사용할 수 있다. 만약 기존 호스트 앱의 데이터를 수정하고 싶지 않다면 nil값을 받아도 된다.), activityError (activity의 에러 여부를 묻는 파라미터로 일반적으로 완료되었다면 nil값을 받는다.)이다.

좀 더 자세히 분기처리를 하고 싶다면

//Completion handler
activityController.completionWithItemsHandler = { (activityType: UIActivity.ActivityType?, completed:
Bool, arrayReturnedItems: [Any]?, error: Error?) in
    if completed {
        print("share completed")
        return
    } else {
        print("cancel")
    }
    if let shareError = error {
        print("error while sharing: \(shareError.localizedDescription)")
    }
}

→ 이렇게 completed와 error의 경우를 나눠서 작성할 수 있다.

4가지 파라미터를 받았다면 다음과 같이 호스트 앱에서의 작업을 구현할 수 있다.

// ✅ Action에서 가공된 데이터가 있다면
if returnedItems!.count > 0 {
                
								// ✅ Extension 작업을 통해서 얻은 item을 textItem에 넣어주고
                let textItem: NSExtensionItem =
                returnedItems![0] as! NSExtensionItem
                
								// ✅ NSItemProvider를 통해 extension으로 전달 받은 값들을 저장한다.
                let textItemProvider =
                textItem.attachments![0] as! NSItemProvider
                
								// ✅ 각 값에 대해서 
                if textItemProvider.hasItemConformingToTypeIdentifier(
                    kUTTypeText as String) {
                    
										// ✅ 마지막으로 호스트 앱에서 보여줄 데이터를 작성한다.
                    textItemProvider.loadItem(
                        forTypeIdentifier: kUTTypeText as String,
                        options: nil,
                        completionHandler: {(string, error) -> Void in
                            let newtext = string as! String
                            DispatchQueue.main.async {
                                self.textView.text = newtext
                            }
                        })
                }
            }

코드를 좀 더 자세시 살펴보자.

NSExtensionItem

An immutable collection of values representing different aspects of an item for an extension to act upon.

extension이 적용될 item의 여러 값을 갖고 있는 불변 값들의 모임이다.

NSItemProvider

An item provider for conveying data or a file between processes during drag and drop or copy/paste activities, or from a host app to an app extension.

드래그/드롭 또는 복사/붙여넣기 작업 동안 또는 호스트 앱에서 앱 extension으로 데이터 또는 파일을 전달하기 위한 item provider이다.

Action

2. Action → 호스트 앱

그리고 ActionViewController의 completionWithItemsHandler 를 통해 해당 Action에서 작업한 뒤 가공된 데이터를 return 값으로 받는다. 그리고 받은 데이터를 호스트앱에 보여준다.

전체 코드는 아래와 같다.

import UIKit
import MobileCoreServices

class TextviewActionController: UIViewController {
    
    @IBOutlet weak var textView: UITextView!
    
    var convertedString: String?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let textItem = self.extensionContext!.inputItems[0] as! NSExtensionItem
        let textItemProvider = textItem.attachments![0]
        
        if textItemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
            textItemProvider.loadItem(forTypeIdentifier: kUTTypeText as String,
                                      options: nil,
                                      completionHandler: { (result, error) in
                self.convertedString = result as? String
                
                if self.convertedString != nil {
                    self.convertedString = self.convertedString!.appending("💖")
                    self.convertedString = self.convertedString?.uppercased()
                    DispatchQueue.main.async {
                        self.textView.text = self.convertedString!
                    }
                }
                
            })
        }
    }
    
    @IBAction func done() {
        let returnProvider =
        NSItemProvider(item: convertedString as NSSecureCoding?,
                       typeIdentifier: kUTTypeText as String)
        
        let returnItem = NSExtensionItem()
        
        returnItem.attachments = [returnProvider]
        returnItem.attributedContentText
        self.extensionContext!.completeRequest(
            returningItems: [returnItem], completionHandler: nil)
    }
    
}

앞서 말한 것과 같이 크게 두가지 작업으로 나뉜다.

  1. 호스트 앱에서 데이터를 전달 받았을 때,
    var convertedString: String?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
    				// ✅ 호스트 앱에서 넘어온 데이터를 받고  
            let textItem = self.extensionContext!.inputItems[0] as! NSExtensionItem
            let textItemProvider = textItem.attachments![0]
            
    				// ✅ 각 값에 대해 다시 전달할 데이터의 형태로 가공한다.
            if textItemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
    						// 호스트 앱으로 반환될 항목으로 string -> NSExtensionItem 형태로 바꾼다.
                textItemProvider.loadItem(forTypeIdentifier: kUTTypeText as String,
                                          options: nil,
                                          completionHandler: { (result, error) in
    
                    self.convertedString = result as? String
                    
                    if self.convertedString != nil {
    										// 마지막에 💖 이모지를 붙이고 대문자로 변경한다.
                        self.convertedString = self.convertedString!.appending("💖")
                        self.convertedString = self.convertedString?.uppercased()
                        DispatchQueue.main.async {
                            self.textView.text = self.convertedString!
                        }
                    }
                    
                })
            }
        }
  1. 가공한 뒤 다시 전달할 때,
    @IBAction func done() {
    				// ✅ 가공된 데이터와 해당 타입으로 새로운 NSItemProvider 인스턴스를 만든다.
            let returnProvider =
            NSItemProvider(item: convertedString as NSSecureCoding?,
                           typeIdentifier: kUTTypeText as String)
            
    				// ✅ NSExtesionItem 인스턴스를 생성한 뒤 NSItemProvider 객체를 할당한다.
            let returnItem = NSExtensionItem()
            returnItem.attachments = [returnProvider]
            returnItem.attributedContentText
    
    				// ✅ returningItems 인자로 NSExtensionItem 인스턴스를 전달하며 호출한다. 
            self.extensionContext!.completeRequest(
                returningItems: [returnItem], completionHandler: nil)
    }

위의 action에서는 미리 어떻게 가공될 지 보여주고 done 버튼을 누르면 가공된 데이터가 넘어가도록 구현했다.

앱의 전체적인 흐름은 다음과 같다.
1. 호스트 앱

  1. 버튼을 눌렀을 때 Activity View에 보이는 Textview Action

  2. 호스트 앱에서 넘어온 데이터를 가공

  3. 다시 호스트 앱으로 해당 데이터 전송

profile
Slowly But Surely

0개의 댓글