Things take time

[SWIFT] 공유하기! 방법 => Share Extension 본문

iOS (기능)

[SWIFT] 공유하기! 방법 => Share Extension

겸손할 겸 2018. 2. 2. 16:56

[개요]


http://g-y-e-o-m.tistory.com/91 에서 기본 익스텐션의 개념 및 액션 익스텐션에 대해 간단한 예제를 소개했다.

이번에는 쉐어 익스텐션이란 것의 예제이다.


글 내용이나 사진 등을 공유하길 눌렀을때, 호출되는 카카오톡이 대표적인 쉐어 익스텐션인데, 말 그대로 SNS와 같은 앱에 쉽게 공유할 수 있도록 지원한다.



[예제]


기본적으로 제공하는 템플릿 쉐어 익스텐션과 이를 조금더 바꿔서 카카오톡처럼 커스터마이징을 통한 쉐어 익스텐션을 만들어볼 것이다. + 데이터 공유

사실 기본 템플릿은 내가 필요한 기능이 아니라서, 기본만 보고 넘어간다.


Share Extension을 만들고 나면 하나의 스토리보드와 하나의 뷰 컨트롤러가 보인다. 이 상태로 그냥 시뮬레이터로 사파리를 기본으로 잡고 실행해보자




그리고 네이버 페이지에서 한 기사를 롱클릭하여 쉐어를 하게 되면

아래처럼 팝업이 나온다



이게 기본적으로 제공하는 템플릿 UI이며 기능이다.

그리고 ShareViewController를 아래처럼  변경해보자.

import UIKit
import Social

class ShareViewController: SLComposeServiceViewController {

    override func isContentValid() -> Bool {
        // Do validation of contentText and/or NSExtensionContext attachments here
        
        // contentText : 유저가 공유하기 창을 눌러 넘어온 문자열 값(상수)
        if let currentMessage = contentText{
            let currentMessageLength = currentMessage.count
            // charactersRemaining : 문자열 길이 제한 값(상수)
            charactersRemaining = (100 - currentMessageLength) as NSNumber
            
            print("currentMessage : \(currentMessage) // 길이 : \(currentMessageLength) // 제한 : \(charactersRemaining)")
            if Int(charactersRemaining) < 0 {
                print("100자가 넘었을때는 공유할 수 없다!")
                return false
            }
        }
        return true
    }

    override func didSelectPost() {
        // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
    
        // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
        self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
    }

    // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
    override func configurationItems() -> [Any]! {
        let item = SLComposeSheetConfigurationItem()
        
        item?.title = "여기는 제목입니다"
        // item?.tapHandler : 유저가 터치했을 때 호출되는 핸들러
        return [item]
    }
}



소스만 봐도 이해가 갈 것이다. 데이터를 넘겨받는 isContentValid()와 기본 템플릿 UI 변경의 configurationItems(), 그리고 위의 화면에서 Post를 눌럿을 때 동작하는 didSelectPost()함수가 있다. 여기서 입맛에 맞춰 사용하면 되고, 중요한 것은 didSelectPost()일 것이다. 여기에서 데이터를 가져와서 UserDefaults에 저장하거나, 넘어온 데이터의 타입들을 검사하는 등의 작업을 수행한다. 그리고 액션 익스텐션과 마찬가지로 넘어온 데이터들을 제한하거나 할 때는 해당 익스텐션의 plist안에서 rule을 넣으면 된다. (이전 액션 익스텐션 포스팅 참고)


이 부분은 커스텀 쉐어 익스텐션 예제를 만들면서 해볼 것이므로, 여기까지 하고 기본 템플릿은 넘어간다. 



[예제2]


이제 커스텀 쉐어 익스텐션을 만들것이다.

커스텀을 하는 방법은 간단하다. 생성까지는 동일하지만 ShareViewController에서 상속받는 부분을 UIViewController로 변경하면 끝이다.



이 상속을 아래로!



그럼 자연스레 필수구현해야하는 함수 3개(위의 3함수)를 제거하고 일반 뷰 컨트롤러 처럼사용하면 간단하다.


그리고 두 가지가 남았다.


1. 공유하기를 통해 넘어온 데이터는 어떻게 받을 수 있는가(contentText는 SLComposeServiceViewController 내에 있는 변수이기 때문에 UIViewController는 접근 불가)


2. 앱 <-> 익스텐션간의 뷰 이동은 불가능하다. 그렇지만 데이터를 완전히 공유할 수 없는 것은 아니다.



[첫 번째 답 : 코드]

import UIKit
import Social
import MobileCoreServices

class ShareViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let extensionItems = extensionContext?.inputItems as! [NSExtensionItem]

        for extensionItem in extensionItems {
            if let itemProviders = extensionItem.attachments as? [NSItemProvider] {
                for itemProvider in itemProviders {
                    if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
                        itemProvider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil, completionHandler: { text, error in
                            let alert = UIAlertController(title: "알림", message: "여기는 ShareViewController : \(text)", preferredStyle: .alert)
                            alert.addAction(UIAlertAction(title: "확인", style: .default, handler: {
                                (a) -> Void in
                                let board = UIStoryboard(name: "MainInterface", bundle: nil)
                                self.present(board.instantiateViewController(withIdentifier: "abc"), animated: true, completion: nil)
                            }))
                            self.present(alert, animated: true, completion: nil)

                        })
                    }
                }
            }
        }
    }
}

extensionContextUIViewController의 변수이며,  SLComposeServiceViewController또한 UIViewController를 상속받아 접근이 가능하다.

그러므로 SLComposeServiceViewController를 상속받는 기본템플릿에서도 이 변수를 통해 접근할 수 있다.


이 예제는 넘어오는 텍스트값이 있다면 해당 텍스트 값을 alert로 띄우며, 익스텐션 내에 있는 또 다른 뷰에 이동하는 로직까지 넣어져 있다. 만약 이미지가 필요하다면 kUTTypeText를 Image타입으로 변경하면 된다.



[두 번째 답 : 설정]


기본적으로 익스텐션과 앱은 별개다. 서로 영역을 접근할 수 없기 때문에 앱에서 익스텐션을 띄울 수 없으며 반대도 마찬가지이다. 그런데 익스텐션에서는 어찌보면 앱의 확장이기때문에 앱의 데이터를 사용해야할 경우가 생긴다.


이때 사용할 수 있는게 UserDefault라는 기본적으로 제공하는 key:value 형태의 폰 DB다. 


일단 타겟을 눌러 Capabilities를 눌러 App Groups를 활성화하고, identifier를 생성한다.

원래는 App Groups는 개발자사이트에서 프로비저닝 파일만드는 곳에서 만들어야한다. 근데 XCode가 진화하고 버전업이되면서 개발자의 편의성이 향상되었다. 그래서 실제로 프로비저닝 파일을 먼저 만들지 않고, Xcode에서 활성화하면 자동 생성해준다(개발자 사이트에서 생성된 것 확인 가능) 그래도 정식 루트로는 개발자사이트에서 App Groups를 먼저 생성하는 것이 맞다.


'




두 스샷의 차이점은 무엇인가? 타겟을 잡은게 하나는 메인 앱단위이고, 나머지는 익스텐션 단위로 잡았다는 것이다. 이 두 개의 타겟이 같은 앱 그룹을 택해야 공유가 가능하다.


** 참고로 동일한 App Groups에 묶여 있는 애들은 또 다른 외부의 앱, 익스텐션에서도 호출이 가능하다. 즉, 지금 현재의 프로젝트가 아니라 다른 프로젝트의 앱에서도 다른 앱의 데이터 공유가 가능하다는 사실! 앱 그룹은 익스텐션만을 위한 기능이 아니라는 걸 알아야한다.


[코드]


1. ViewController 


메인 뷰 컨트롤러의 소스이다.

import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. let defaults = UserDefaults(suiteName: "group.com.shuvic.share") defaults?.set("sss", forKey: "share") defaults?.synchronize() } }


앱을 실행시키면, 먼저 UserDefaults로 sss라는 값을 share라는 키 값에 저장 후 동기화한다.


import UIKit
import Social
import MobileCoreServices

class ShareViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let defaults = UserDefaults(suiteName: "group.com.shuvic.share")
        let data = defaults?.string(forKey: "share")
        let alert = UIAlertController(title: "알림", message: "여기는 ShareViewController : \(data)", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "확인", style: .default, handler: {
            (a) -> Void in
            let board = UIStoryboard(name: "MainInterface", bundle: nil)
            self.present(board.instantiateViewController(withIdentifier: "abc"), animated: true, completion: nil)
        }))
        self.present(alert, animated: true, completion: nil)
    }
}


익스텐션이 열리게 되면 UserDefaults를 검사하여 해당 값을 alert로 띄운다.

테스트하려면 해당 익스텐션이 있는 앱을 먼저 실행시켜야 한다. 그래야 sss가 저장된다


이제 일반 앱이나 어디에서든 공유하기를 통해 해당 익스텐션을 실행하면?



이렇게 sss가 온다. 이를 응용해서 반대로하는 것도 가능하며(익스텐션에서는 폰DB에 저장만하고 앱을 닫고, 앱이 켜지면 폰DB 유무를 검사하여 원하는 작업 수행 등) 넘겨받은 값을 UserDefaults에 저장하는 것도 많이 사용된다.


카카오톡과 같은 경우에는, 주소록 대화목록을 Http 통신을 통해 Restful로 가져왔을 수도 있고, 주소록, 대화목록은 DB에 넣어두고 조회한뒤에, 발송만 네트워크를 통하는 방법을 사용했을 수도 있다. 커스텀은 말 그대로 사용하는 사람의 입맛에 맞게 사용하면 된다는 것이다.



[주의사항]


커스텀 익스텐션을 사용할 때 주의점!

만약 메인 앱에서 사용하는 이미지나 리소스를 사용할 때, 익스텐션에 가져다 쓰면 이미지가 안보인다. 이유는 Assets.xcassets는 기본적으로 메인앱을 타겟으로 하고 있기 때문이다. 이럴 때는 아래처럼 타겟 멤버십을 해당 익스텐션에도 체크할 것(리소스에는 클래스 같은 애들도 포함이다!)




익스텐션에서 사용할 메인 앱의 클래스는 모듈도 바꿔줘야 적용되는 듯 하다. (익스텐션 안에서)



[추가사항]


기본적인 내장 폰 DB, UserDefaults도 있지만, 파일을 공유하는 것도 가능하다. 무슨말인고 하니, SQLite처럼 .sqlite(혹은 .db)도 파일이고, 이 파일을 생성하는 위치를 앱 그룹이란 identifier아래에 생성하도록 설정한다면, SQLite도 앱과 익스텐션간 공유가 가능하단 의미이다.


sqlite도 있지만, 나같은 경우 FMDB라는 sqlite의 라이브러리를 사용하기 때문에 아래와 같은 과정으로 사용한다.


[podfile]

target ‘main’ do
    pod 'Socket.IO-Client-Swift', '~> 12.1.3' # Or latest version
    pod 'Floaty', '~> 4.0.0'
    pod 'DKImagePickerController', '~> 3.8.1'
    pod 'FMDB' , '~> 2.7.2'
    pod 'Alamofire', '~> 4.5.1'
    pod 'Toast-Swift', '~> 3.0.1'
    pod 'SwiftyGif'
    pod 'SwiftyRSA'
    pod 'UICircularProgressRing'


end

target 'share' do
    pod 'Socket.IO-Client-Swift', '~> 12.1.3' # Or latest version
    pod 'FMDB' , '~> 2.7.2'
    pod 'Alamofire', '~> 4.5.1'
end
    

그리고 기존 podfile안에 extension을 넣었다면 pod update를 수행해야한다는 것, 그리고 해당 타겟을 눌러 들어간 다음에 General 맨 하단 링크 프레임 워크, 라이브러리를 확인하자. 익스텐션에 메인 앱 프레임워크가 들어가있는 것은 아닌지 등..


그리고 이러한 설정 뒤에 사용방법은 FMDB기준

   let fileManager = FileManager.default
        let directory = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.xxx.xxx.xxx")
        let path = directory!.appendingPathComponent("share.sqlite")
        
        let database = FMDatabase(url: path)
        
        guard database.open() else {
            print("Unable to open database")
            return
        }
        
        print("database open :\(path.path)")

이처럼 경로를 지정하는 부분에 FileManager.default.containerURL을 사용한다는 것이다. 기존 방법의 경우에는 

         let fileURL = try! FileManager.default
         .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
         .appendingPathComponent("share.sqlite")

아마 이렇게 되어있었을 것이기 때문이다.

전자는 앱 그룹내에 공유할 수 있는 디렉토리를 설정한 것이고, 후자의 경우는 일반적인 앱 내 디렉토리에 파일을 생성한 경우이다.


궁금하다면 print로 해당 url.path를 찍어보자. 그럼 알 수 있다.



참고 사이트 : https://stackoverflow.com/questions/45251714/share-sqlite-db-into-share-extension-into-iphone-app



[추가사항2]


Share Extension은 메인 앱과 다른 또 다른 타겟이기 때문에.. 해당 익스텐션에서 Alamofire같은 인터넷 통신 라이브러리(혹은 URLSession)을 사용하려면?



까먹지 말자. 익스텐션 내 plist



원하는 데이터 타입, 설정 등을 할 때 사용하는 Rule까지!


https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/AppExtensionKeys.html