본문 바로가기

iOS+

Share Extension

개요

이 글은 Share Extension을 통해 외부에서 앱으로 데이터를 공유하는 방법에 대해 알아본다

몇 가지 과정들을 나눠서 진행되는데, 위의 블로그를 참고했다

과정

  1. Share Extension 추가
  2. Activation Rule 설정
  3. ShareViewController에서 extensionContext를 통해 데이터 처리
    • UIHostingController을 통해 SwiftUI View 사용
  4. 공유 데이터 설정 후, 앱과 데이터 공유

 

Share Extension 추가

Share Extension을 프로젝트에서 추가한다

 

위의 블로그에서 언급한대로, 애플은 Share Extension 기능과 관련해서 업데이트를 안한지 오래된 것 같다

공식문서는 옛날 형태로 존재하고, 기본적으로 SwiftUI를 사용하는 것이 아닌 UIKit을 사용한다

그렇지만 해당 글에서는 SwiftUI View를 Hosting하여 Share 시, 데이터 입력을 받도록 처리할 것이다

 

생성을 수행하면 기본적으로 다음과 같이 파일들이 생성된다

 

 

Activation Rule 설정

Extension을 추가하고, 기본적으로 Info.plist를 보면 다음과 같다

 

ActivationRule은 쉽게말해 우리들의 앱이 어떤 경우에 Share로 이용될 수 있는지를 설정하는 것이다

 

기본적으로 TRUEPREDICATE가 설정되어있는데, 이는 항상 공유 시 앱이 사용될 수 있음을 의미한다

실제 배포 시엔, 공유가 사용되는 경우를 아래와 같이 지정해줘야 한다

 

만약 간단하게 String에 대해서만 Share Extension을 사용하려면 다음과 같이 수정한다

 

  • NSExtensionActivationRule의 String을 dictionary로 변경
  • NSExtensionActivationRule dictionary의 key 설정

https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/AppExtensionKeys.html#//apple_ref/doc/uid/TP40014212-SW10

 

기본적으로 지원하는 키들은 위와 같이 존재한다

 

ShareViewController

이번엔 Share Extension에서 사용되는 뷰를 설정하기 위한 과정에 대해 알아본다

 

우선 이전 Info.plist에서 기본으로 설정된 스토리보드를 지우고, ViewController를 설정한다

 

ViewController 설정은 다음과 같이 NSExtensionPrincipalClass를 키 값으로 설정하고, <NameOfTheShareTarget>.ShareViewController를 값으로 설정한다

 

만약 현재 Share Extension 이름이 ShareContent라면 다음과 같이 Info.plist가 수정될 것이다

 

 

또한 다음과 같이 우선 ViewController를 가장 기본적인 형태로 바꾼다

 

import UIKit

class ShareViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Ensure access to extensionItem and itemProvider
        guard
            let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
            let itemProvider = extensionItem.attachments?.first else {
            close()
            return
        }
    }
    
    /// Close the Share Extension
    func close() {
        self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
    }
    
}

 

NSExtensionContext는 Share Extension을 통해 공유하려는 데이터를 얻어오는 데 사용된다

 

extension UIViewController : NSExtensionRequestHandling {

    @available(iOS 8.0, *)
    open var extensionContext: NSExtensionContext? { get }
}

 

이에 대해 좀 더 자세히 알아보기 위해 UIViewController 코드를 살펴보면 NSExtensionContext인 extensionContext가 존재한다

 

 

공유 데이터는 inputItems로 부터 얻을 수 있는데, 이 데이터를 얻어서 SwiftUI 뷰를 통해 앱으로 전달하는 과정을 정리하면 다음과 같다

 

import UIKit
import UniformTypeIdentifiers

class ShareViewController: UIViewController {
	
	override func viewDidLoad() {
		/// Ensure access to extensionItem and itemProvider
		guard
			let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
			let itemProvider = extensionItem.attachments?.first else {
			close()
			return
		}
		
		/// Check type identifier
		let textDataType = UTType.plainText.identifier
		guard itemProvider.hasItemConformingToTypeIdentifier(textDataType) else {
			close()
			return
		}
		
		/// Load the item from itemProvider
		itemProvider.loadItem(forTypeIdentifier: textDataType , options: nil) { (providedText, error) in
			if let _ = error {
				self.close()
				return
			}
			
			guard let text = providedText as? String else {
				self.close()
				return
			}
				
			print(text)
		}
		
		/// Close the Share Extension
		func close() {
			self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
	}
}

 

SwiftUI를 통한 데이터 입력 뷰 구현

SwiftUI의 UIHostingController(rootView:)를 사용하면, View를 UIKit에서 사용할 수 있다

입력받은 데이터에 처리과정을 거친 text를 다음과 같이 SwiftUI의 뷰에 전달한다

 

...
			print(text)				
				
			DispatchQueue.main.async {
				/// host the SwiftU view
				let contentView = UIHostingController(rootView: ShareExtensionView(text: text))
				self.addChild(contentView)
				self.view.addSubview(contentView.view)
				
				/// set up constraints
				contentView.view.translatesAutoresizingMaskIntoConstraints = false
				contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
				contentView.view.bottomAnchor.constraint (equalTo: self.view.bottomAnchor).isActive = true
				contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
				contentView.view.rightAnchor.constraint (equalTo: self.view.rightAnchor).isActive = true
			}
			
			// add observer for close notification
			NotificationCenter.default.addObserver(forName: NSNotification.Name("close"), object: nil, queue: nil) { _ in
		    DispatchQueue.main.async {
	        self.close()
		    }
			}
	  }
	
...
}

struct ShareExtensionView: View {
	@State var text: String = ""
	
	var body: some View {
		VStack {
			Text(text)
			Button("close") {
				close()
			}
		}
	}
	
	func close() {
    NotificationCenter.default.post(name: NSNotification.Name("close"), object: nil)
  }
}

 

이 둘(View와 ViewController)의 상호작용은 NotificationCenter를 통해 수행한다

 

 

앱과 데이터 공유

그렇다면 Share Extension에서 앱과 데이터는 어떻게 공유할 수 있는가?

 

Widget처럼 AppGroup을 통해 데이터 공유 메커니즘을 통해 앱에 데이터를 전달하면 된다

 

이때 가장 간단한 방법은 App Groups과 SwiftData를 이용하는 것일 것이다

 

우선 앱과 Share Extnesion에 App Groups를 추가하여 데이터 공유의 장을 마련한다

 

 

이후 앱에서 모델 컨테이너를 정의하고, SwiftData 모델을 정의하여, 확장과 공유한다

 


import SwiftUI
import SwiftData

@main
struct GCG_DictApp: App {
	var sharedModelContainer: ModelContainer = {
		let schema = Schema([ShareContentDTO.self])
		do {
			return try ModelContainer(for: schema)
		} catch {
			fatalError("Failed to create ModelContainer: \\(error)")
		}
	}()
	
  var body: some Scene {
    WindowGroup {
      HomeView()
				.modelContainer(sharedModelContainer)
    }
  }
}

 

 

 

마찬가지로 Share Extension에서도 컨테이너를 만들고, Hosting하는 SwiftUI 뷰에 전달하면 앱으로 데이터를 저장시킬 수 있다

 

class ShareViewController: UIViewController {
	private var modelContainer: ModelContainer!
	
	private func setupModelContainer() {
		let schema = Schema([ShareContentDTO.self])
		do {
			self.modelContainer = try ModelContainer(for: schema)
		} catch {
			fatalError("Failed to create ModelContainer: \\(error)")
		}
	}
	
	override func viewDidLoad() {
		setupModelContainer()
		
		...
		
				DispatchQueue.main.async {
				let shareView = ShareExtensionView(text: text)
					.modelContainer(self.modelContainer)
				
				/// host the SwiftU view
				let contentView = UIHostingController(rootView: shareView)
				self.addChild(contentView)
				self.view.addSubview(contentView.view)
				
...
struct ShareExtensionView: View {
	@Environment(\\.modelContext) var modelContext
	
	let text: String
	
	var body: some View {
		VStack {
			Text(text)
			
			Button("추가") {
				modelContext.insert(ShareContentDTO(content: text, createdAt: .now))
				do {
					try modelContext.save()
				} catch {
					print("Failed to save: \\(error)")
					close()
				}
			}
			
			Button("close") {
				close()
			}
		}
	}
	
	...

 

이후 문자열을 선택 후, Share를 누르면 ShareExtensionView가 나타난다

 

여기서 추가 버튼을 클릭 시, 앱과 공유되는 SwiftData 컨테이너에 데이터를 저장하고

우측 사진과 같이 앱에서도 해당 데이터를 확인할 수 있다 

 

'iOS+' 카테고리의 다른 글