在網路上已經有很多關於 iOS Share Extension 的教學,例如這裡就有一篇。有需要的人可以自行在網路上搜尋,這邊就不多作著墨。我這次想要分享的主要是在開發 share extension 的過程遇到一些要注意的事項,以及踩到的一些坑。
我的開發環境是 macOS Sonoma + Xcode 15,使用 Swift 開發。
要建立一個 share extension 很簡單,就是在你的專案新增一個 target
然後選擇 Share Extension
即可。Xcode 會自動幫忙產生必要的檔案,以及做好基本設定。
設定入口畫面
Xcode 會自動產生 ShareViewController
這個檔案,在蘋果的規劃裡頭,它預期大部分 Share Extension 的情境就是要把內容轉發到其他網路平台,所以這個 ShareViewController 會繼承自 SLComposeServiceViewController
並且也有提供發文的程式碼骨架。如果這符合你的情境,那很棒;如果不是的話,你可以手動把它改成繼承自 UIViewController
然後刪除那些骨架程式碼。
Xcode 也會自動幫忙產生一個 MainInterface.storyboard
作為入口點。目前 storyboard 已經不被鼓勵使用,而且很多開發者也逐步轉向 SwiftUI 開發畫面,所以我們可以刪除這個檔案,然後在 ShareViewController
裡頭刻畫面。
要修改畫面入口點,除了刪除 storyboard 之外,還要修改 Info.plist
告訴 Xcode 我們的入口點是什麼。在 Share Extension 的 Info.plist
裡面,找到 NSExtensionMainStoryboard
這組 key,把它的名稱改成 NSExtensionPrincipalClass
並且把對應的 value 改成 $(PRODUCT_NAME).ShareViewController
。
設定要支援的分享內容格式
使用者有可能分享多種內容格式,最常見的就是「文字」、「圖片」、「網址」這幾種,當然還有各種不同的檔案類型等等。我們需要在 Info.plist
裡頭設定,告訴系統我們的 share extension 支援哪些內容格式,只有當使用者分享的內容是我們支援的,系統的分享選單才會出現我們的 share extension。
要支援的格式放在 NSExtensionActivationRule
key 底下,支援的格式寫在這篇文件裡,如果預設的支援格式不符使用要求,也可以使用更複雜也更有彈性的 Predicate。
舉例來說,如果要支援網址的分享,Info.plist
長得像這樣:
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
有些 app 的分享功能同時分享了多種內容格式,例如瀏覽器的網頁分享可能同時包含「文字(這個網頁的標題)」跟「網址(這個網頁的網址)」。通常我們會希望分享內容有我們支援的格式,系統選單就要出現我們的 share extension,但實際情況是並不是如此。
以我們的例子來說,在 Info.plist
裡頭宣告支援網址格式,系統就會在分享內容「只有」網址格式的時候,才會出現我們的 share extension;不會在分享內容「同時有」網址跟其他格式的時候出現。
這顯然不是我們要的結果,解法就是在 Info.plist
加入 NSExtensionActivationDictionaryVersion
key 並將 value 設為 2
。
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationDictionaryVersion</key>
<integer>2</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
處理分享內容的時間點
網路上很多教學都是在 viewDidLoad
處理分享內容,但我測試發現會遇到一些奇怪的 UI 問題,所以我選擇在 viewDidAppear
處理。
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Make sure to load shared content and update UI after view did appear, otherwise UI may not work as expected.
loadSharedContent()
}
private func loadSharedContent() {
// ....
}
要在 Main Thread 處理 UI
這看起來很像是基本常識,但是在開發 Share Extension 的時候很容易忘記它。當你發現畫面不如預期變化的時候,很有可能就是沒有在 main thread 處理 UI 的關係。
private func loadSharedContent() {
guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
let attachment = item.attachments?.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.url.identifier) })
else {
finish()
return
}
attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] data, error in
if let url = data as? URL {
self?.handleURL(url)
}
}
}
private func finish() {
DispatchQueue.main.async {
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
private func handleURL(_ url: URL) {
// Make sure to run in main thread, otherwise UI may freeze.
DispatchQueue.main.async {
self.label.text = url.absoluteString
}
}
以上就是本次分享的內容,希望可以幫助各位少踩一些坑。