1. Add the following 1. 添加以下内容
ios/Runner/info.plist ios/Runner/info.plist
... <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>ShareMedia</string> </array> </dict> <dict/> </array> <key>NSPhotoLibraryUsageDescription</key> <string>To upload photos, please allow permission to access your photo library.</string> ...
2. Create Share Extension 2. 创建共享扩展
- Using xcode, go to File/New/Target and Choose "Share Extension"
使用 xcode,转到文件/新建/目标并选择“共享扩展” - Give it a name i.e. "Share Extension"
给它一个名字,即“共享扩展”
Make sure the deployment target for Runner.app and the share extension is the same. 确保 Runner.app 和共享扩展的部署目标相同。
Add the following code: 添加以下代码:
ios/Share Extension/info.plist ios/Share Extension/info.plist
.... <string>$(FLUTTER_BUILD_NAME)</string> <key>CFBundleVersion</key> <string>$(FLUTTER_BUILD_NUMBER)</string> <key>NSExtension</key> <dict> <key>NSExtensionAttributes</key> <dict> <key>PHSupportedMediaTypes</key> <array> <!--TODO: Add this flag, if you want to support sharing video into your app--> <string>Video</string> <!--TODO: Add this flag, if you want to support sharing images into your app--> <string>Image</string> </array> <key>NSExtensionActivationRule</key> <dict> <!--TODO: Add this flag, if you want to support sharing text into your app--> <key>NSExtensionActivationSupportsText</key> <true/> <!--TODO: Add this tag, if you want to support sharing urls into your app--> <key>NSExtensionActivationSupportsWebURLWithMaxCount</key> <integer>1</integer> <!--TODO: Add this flag, if you want to support sharing images into your app--> <key>NSExtensionActivationSupportsImageWithMaxCount</key> <integer>100</integer> <!--TODO: Add this flag, if you want to support sharing video into your app--> <key>NSExtensionActivationSupportsMovieWithMaxCount</key> <integer>100</integer> <!--TODO: Add this flag, if you want to support sharing other files into your app--> <!--Change the integer to however many files you want to be able to share at a time--> <key>NSExtensionActivationSupportsFileWithMaxCount</key> <integer>1</integer> </dict> </dict> <key>NSExtensionMainStoryboard</key> <string>MainInterface</string> <key>NSExtensionPointIdentifier</key> <string>com.apple.share-services</string> </dict> ....
ios/Runner/Runner.entitlements ios/Runner/Runner.rightss
.... <!--TODO: Add this tag, if you want support opening urls into your app--> <key>com.apple.developer.associated-domains</key> <array> <string>applinks:example.com</string> </array> ....
ios/Share Extension/ShareViewController.swift
ios/Share Extension/ShareViewController.swift
import UIKit import Social import MobileCoreServices import Photos class ShareViewController: SLComposeServiceViewController { // TODO: IMPORTANT: This should be your host app bundle identifier let hostAppBundleIdentifier = "com.kasem.sharing" let sharedKey = "ShareKey" var sharedMedia: [SharedMediaFile] = [] var sharedText: [String] = [] let imageContentType = kUTTypeImage as String let videoContentType = kUTTypeMovie as String let textContentType = kUTTypeText as String let urlContentType = kUTTypeURL as String let fileURLType = kUTTypeFileURL as String; override func isContentValid() -> Bool { return true } override func viewDidLoad() { super.viewDidLoad(); } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. if let content = extensionContext!.inputItems[0] as? NSExtensionItem { if let contents = content.attachments { for (index, attachment) in (contents).enumerated() { if attachment.hasItemConformingToTypeIdentifier(imageContentType) { handleImages(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { handleText(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { handleFiles(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { handleUrl(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { handleVideos(content: content, attachment: attachment, index: index) } } } } } override func didSelectPost() { print("didSelectPost"); } override func configurationItems() -> [Any]! { // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. return [] } private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in if error == nil, let item = data as? String, let this = self { this.sharedText.append(item) // If this is the last item, save imagesData in userDefaults and redirect to host app if index == (content.attachments?.count)! - 1 { let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") userDefaults?.set(this.sharedText, forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .text) } } else { self?.dismissWithError() } } } private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in if error == nil, let item = data as? URL, let this = self { this.sharedText.append(item.absoluteString) // If this is the last item, save imagesData in userDefaults and redirect to host app if index == (content.attachments?.count)! - 1 { let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") userDefaults?.set(this.sharedText, forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .text) } } else { self?.dismissWithError() } } } private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in if error == nil, let url = data as? URL, let this = self { // Always copy let fileName = this.getFileName(from: url, type: .image) let newPath = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! .appendingPathComponent(fileName) let copied = this.copyFile(at: url, to: newPath) if(copied) { this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) } // If this is the last item, save imagesData in userDefaults and redirect to host app if index == (content.attachments?.count)! - 1 { let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .media) } } else { self?.dismissWithError() } } } private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { attachment.loadItem(forTypeIdentifier: videoContentType, options: nil) { [weak self] data, error in if error == nil, let url = data as? URL, let this = self { // Always copy let fileName = this.getFileName(from: url, type: .video) let newPath = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! .appendingPathComponent(fileName) let copied = this.copyFile(at: url, to: newPath) if(copied) { guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else { return } this.sharedMedia.append(sharedFile) } // If this is the last item, save imagesData in userDefaults and redirect to host app if index == (content.attachments?.count)! - 1 { let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .media) } } else { self?.dismissWithError() } } } private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in if error == nil, let url = data as? URL, let this = self { // Always copy let fileName = this.getFileName(from :url, type: .file) let newPath = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! .appendingPathComponent(fileName) let copied = this.copyFile(at: url, to: newPath) if (copied) { this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file)) } if index == (content.attachments?.count)! - 1 { let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .file) } } else { self?.dismissWithError() } } } private func dismissWithError() { print("[ERROR] Error loading data!") let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) let action = UIAlertAction(title: "Error", style: .cancel) { _ in self.dismiss(animated: true, completion: nil) } alert.addAction(action) present(alert, animated: true, completion: nil) extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } private func redirectToHostApp(type: RedirectType) { let url = URL(string: "ShareMedia://dataUrl=\(sharedKey)#\(type)") var responder = self as UIResponder? let selectorOpenURL = sel_registerName("openURL:") while (responder != nil) { if (responder?.responds(to: selectorOpenURL))! { let _ = responder?.perform(selectorOpenURL, with: url) } responder = responder!.next } extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } enum RedirectType { case media case text case file } func getExtension(from url: URL, type: SharedMediaType) -> String { let parts = url.lastPathComponent.components(separatedBy: ".") var ex: String? = nil if (parts.count > 1) { ex = parts.last } if (ex == nil) { switch type { case .image: ex = "PNG" case .video: ex = "MP4" case .file: ex = "TXT" } } return ex ?? "Unknown" } func getFileName(from url: URL, type: SharedMediaType) -> String { var name = url.lastPathComponent if (name.isEmpty) { name = UUID().uuidString + "." + getExtension(from: url, type: type) } return name } func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { do { if FileManager.default.fileExists(atPath: dstURL.path) { try FileManager.default.removeItem(at: dstURL) } try FileManager.default.copyItem(at: srcURL, to: dstURL) } catch (let error) { print("Cannot copy item at \(srcURL) to \(dstURL): \(error)") return false } return true } private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? { let asset = AVAsset(url: forVideo) let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() let thumbnailPath = getThumbnailPath(for: forVideo) if FileManager.default.fileExists(atPath: thumbnailPath.path) { return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) } var saved = false let assetImgGenerate = AVAssetImageGenerator(asset: asset) assetImgGenerate.appliesPreferredTrackTransform = true // let scale = UIScreen.main.scale assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) do { let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) saved = true } catch { saved = false } return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil } private func getThumbnailPath(for url: URL) -> URL { let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") let path = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! .appendingPathComponent("\(fileName).jpg") return path } class SharedMediaFile: Codable { var path: String; // can be image, video or url path. It can also be text content var thumbnail: String?; // video thumbnail var duration: Double?; // video duration in milliseconds var type: SharedMediaType; init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) { self.path = path self.thumbnail = thumbnail self.duration = duration self.type = type } // Debug method to print out SharedMediaFile details in the console func toString() { print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") } } enum SharedMediaType: Int, Codable { case image case video case file } func toData(data: [SharedMediaFile]) -> Data { let encodedData = try? JSONEncoder().encode(data) return encodedData! } } extension Array { subscript (safe index: UInt) -> Element? { return Int(index) < count ? self[Int(index)] : nil } }
3. Add Runner and Share Extension in the same group 3. 在同一组中添加运行器和共享扩展
- Go to the Capabilities tab and switch on the App Groups switch for both targets. Add a new group and name it
group.YOUR_HOST_APP_BUNDLE_IDENTIFIER
in my casegroup.com.kasem.sharing
转到“功能”选项卡,然后打开两个目标的“应用组”开关。添加一个新组并将其命名为group.YOUR_HOST_APP_BUNDLE_IDENTIFIER
在我的情况下为group.com.kasem.sharing
4. Compiling issues and their fixes 4. 编译问题及其修复
- Error: App does not build after adding Share Extension?
错误:添加共享扩展后应用未构建? - Fix: Check Build Settings of your share extension and remove everything that tries to import Cocoapods from your main project. i.e. remove everything under
Linking/Other Linker Flags
修复:检查共享扩展的生成设置,并删除尝试从主项目导入 Cocoapods 的所有内容。即删除Linking/Other Linker Flags
下的所有内容 - You might need to disable bitcode for the extension target
您可能需要禁用扩展目标的位码 - Error: Invalid Bundle. The bundle at 'Runner.app/Plugins/Sharing Extension.appex' contains disallowed file 'Frameworks'
错误:捆绑包无效。“Runner.app/Plugins/Sharing Extension.appex”中的捆绑包包含不允许的文件“框架” - Fix: https://stackoverflow.com/a/25789145/2061365 修复: https://stackoverflow.com/a/25789145/2061365
Full Example 完整示例
import 'package:flutter/material.dart'; import 'dart:async'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { StreamSubscription _intentDataStreamSubscription; List<SharedMediaFile> _sharedFiles; String _sharedText; @override void initState() { super.initState(); // For sharing images coming from outside the app while the app is in the memory _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) { setState(() { print("Shared:" + (_sharedFiles?.map((f)=> f.path)?.join(",") ?? "")); _sharedFiles = value; }); }, onError: (err) { print("getIntentDataStream error: $err"); }); // For sharing images coming from outside the app while the app is closed ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) { setState(() { _sharedFiles = value; }); }); // For sharing or opening urls/text coming from outside the app while the app is in the memory _intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) { setState(() { _sharedText = value; }); }, onError: (err) { print("getLinkStream error: $err"); }); // For sharing or opening urls/text coming from outside the app while the app is closed ReceiveSharingIntent.getInitialText().then((String value) { setState(() { _sharedText = value; }); }); } @override void dispose() { _intentDataStreamSubscription.cancel(); super.dispose(); } @override Widget build(BuildContext context) { const textStyleBold = const TextStyle(fontWeight: FontWeight.bold); return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), body: Center( child: Column( children: <Widget>[ Text("Shared files:", style: textStyleBold), Text(_sharedFiles?.map((f)=> f.path)?.join(",") ?? ""), SizedBox(height: 100), Text("Shared urls/text:", style: textStyleBold), Text(_sharedText ?? "") ], ), ), ), ); } }