ShoutSocial / share_handler

A plugin to facilitate receiving and handling share intents
43 stars 46 forks source link

iOS not working, No data is parse into app #81

Closed AimanKyo97 closed 3 weeks ago

AimanKyo97 commented 10 months ago

I did follow the documentation but its not working for me in iOS, can help me check what i doing wrong? The app can launch when being shared but no data is parse in the app.

iOS Device iPhone: Version: 17.2.1

Xcode Version: 15.1

In info.plis

`CFBundleURLTypes

CFBundleURLName XXX CFBundleTypeRole Editor CFBundleURLSchemes ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) ` In Podfile target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) # share_handler addition start target 'Share Extension' do inherit! :search_paths pod "share_handler_ios_models", :path => ".symlinks/plugins/share_handler_ios/ios/Models" end end In Folder Share Extension->ShareViewController.swift import share_handler_ios_models @available(iOSApplicationExtension 14.0, *) class ShareViewController: ShareHandlerIosViewController {} Inside ShareHandlerIosViewController {} // // ShareHandlerIosViewController.swift // Pods // // Created by Josh Juncker on 7/7/22. // import UIKit import Social import MobileCoreServices import Photos import Intents import Contacts @available(iOS 14.0, *) @available(iOSApplicationExtension 14.0, *) open class ShareHandlerIosViewController: UIViewController { static var hostAppBundleIdentifier = "com.xxx.xxx" static var appGroupId = "group.com.xxx.xxx" let sharedKey = "ShareKey" var sharedText: [String] = [] let imageContentType = UTType.image.identifier let movieContentType = UTType.movie.identifier let textContentType = UTType.text.identifier let urlContentType = UTType.url.identifier let fileURLType = UTType.fileURL.identifier let dataContentType = UTType.data.identifier var sharedAttachments: [SharedAttachment] = [] lazy var userDefaults: UserDefaults = { return UserDefaults(suiteName: ShareHandlerIosViewController.appGroupId)! }() public func loadIds() { // loading Share extension App Id let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!; // convert ShareExtension id to host app id // By default it is remove last part of id after last point // For example: com.test.ShareExtension -> com.test let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: "."); ShareHandlerIosViewController.hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[.. ShareHandlerIosViewController.appGroupId = (Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as? String) ?? "group.\(ShareHandlerIosViewController.hostAppBundleIdentifier)"; } public override func viewDidLoad() { super.viewDidLoad(); // load group and app id from build info loadIds(); Task { await handleInputItems() } } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } func handleInputItems() async { if let content = extensionContext!.inputItems[0] as? NSExtensionItem { if let contents = content.attachments { for (index, attachment) in (contents).enumerated() { do { if attachment.hasItemConformingToTypeIdentifier(imageContentType) { try await handleImages(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(movieContentType) { try await handleVideos(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(fileURLType){ try await handleFiles(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { try await handleUrl(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { try await handleText(content: content, attachment: attachment, index: index) } else if attachment.hasItemConformingToTypeIdentifier(dataContentType) { try await handleData(content: content, attachment: attachment, index: index) } else { print("Attachment not handled with registered type identifiers: \(attachment.registeredTypeIdentifiers)") } } catch { self.dismissWithError() } } } redirectToHostApp() } } public func getNewFileUrl(fileName: String) -> URL { let newFileUrl = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: ShareHandlerIosViewController.appGroupId)! .appendingPathComponent(fileName) return newFileUrl } public func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws { let data = try await attachment.loadItem(forTypeIdentifier: textContentType, options: nil) if let item = data as? String { sharedText.append(item) } else { if let d = data as? Data { do{ let contacts = try CNContactVCardSerialization.contacts(with: d) for contact in contacts { let data = try CNContactVCardSerialization.data(with: [contact]) let str = String(data: data, encoding: .utf8)! sharedText.append(str) } } catch { dismissWithError() } } else { dismissWithError() } } } public func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws { let data = try await attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) if let item = data as? URL { sharedText.append(item.absoluteString) } else { dismissWithError() } } public func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws { let data = try await attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) var fileName: String? var imageData: Data? var sourceUrl: URL? if let url = data as? URL { fileName = getFileName(from: url, type: .image) sourceUrl = url } else if let iData = data as? Data { fileName = UUID().uuidString + ".png" imageData = iData } else if let image = data as? UIImage { fileName = UUID().uuidString + ".png" imageData = image.pngData() } if let _fileName = fileName { let newFileUrl = getNewFileUrl(fileName: _fileName) do { if FileManager.default.fileExists(atPath: newFileUrl.path) { try FileManager.default.removeItem(at: newFileUrl) } } catch { print("Error removing item") } var copied: Bool = false if let _data = imageData { copied = FileManager.default.createFile(atPath: newFileUrl.path, contents: _data) } else if let _sourceUrl = sourceUrl { copied = copyFile(at: _sourceUrl, to: newFileUrl) } if (copied) { sharedAttachments.append(SharedAttachment.init(path: newFileUrl.absoluteString, type: .image)) } else { dismissWithError() return } } else { dismissWithError() return } } public func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws { let data = try await attachment.loadItem(forTypeIdentifier: movieContentType, options: nil) if let url = data as? URL { // Always copy let fileName = getFileName(from: url, type: .video) let newFileUrl = getNewFileUrl(fileName: fileName) let copied = copyFile(at: url, to: newFileUrl) if(copied) { sharedAttachments.append(SharedAttachment.init(path: newFileUrl.absoluteString, type: .video)) } } else { dismissWithError() } } public func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws { let data = try await attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) if let url = data as? URL { // Always copy let fileName = getFileName(from :url, type: .file) let newFileUrl = getNewFileUrl(fileName: fileName) let copied = copyFile(at: url, to: newFileUrl) if (copied) { sharedAttachments.append(SharedAttachment.init(path: newFileUrl.absoluteString, type: .file)) } } else { dismissWithError() } } public func handleData (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws { let data = try await attachment.loadItem(forTypeIdentifier: dataContentType, options: nil) if let url = data as? URL { // Always copy let fileName = getFileName(from :url, type: .file) let newFileUrl = getNewFileUrl(fileName: fileName) let copied = copyFile(at: url, to: newFileUrl) if (copied) { sharedAttachments.append(SharedAttachment.init(path: newFileUrl.absoluteString, type: .file)) } } else { dismissWithError() } } public 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) } public func redirectToHostApp() { // ids may not loaded yet so we need loadIds here too loadIds(); let url = URL(string: "ShareMedia-\(ShareHandlerIosViewController.hostAppBundleIdentifier)://\(ShareHandlerIosViewController.hostAppBundleIdentifier)?key=\(sharedKey)") var responder = self as UIResponder? let selectorOpenURL = sel_registerName("openURL:") let intent = self.extensionContext?.intent as? INSendMessageIntent let conversationIdentifier = intent?.conversationIdentifier let sender = intent?.sender let serviceName = intent?.serviceName let speakableGroupName = intent?.speakableGroupName let sharedMedia = SharedMedia.init(attachments: sharedAttachments, conversationIdentifier: conversationIdentifier, content: sharedText.joined(separator: "\n"), speakableGroupName: speakableGroupName?.spokenPhrase, serviceName: serviceName, senderIdentifier: sender?.contactIdentifier ?? sender?.customIdentifier, imageFilePath: nil) let json = sharedMedia.toJson() userDefaults.set(json, forKey: sharedKey) userDefaults.synchronize() while (responder != nil) { if (responder?.responds(to: selectorOpenURL))! { let _ = responder?.perform(selectorOpenURL, with: url) } responder = responder!.next } } enum RedirectType { case media case text case file } func getExtension(from url: URL, type: SharedAttachmentType) -> 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" default: ex = "" } } return ex ?? "Unknown" } func getFileName(from url: URL, type: SharedAttachmentType) -> 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 } }` Screenshot at Jan 17 09-42-54 Screenshot at Jan 17 10-14-16 Screenshot at Jan 17 09-46-34 Screenshot at Jan 17 09-49-56 Screenshot at Jan 17 09-55-10 Part 1 cycle: Screenshot at Jan 17 10-00-00 Part 2 cycle: Screenshot at Jan 17 10-00-42 Screenshot at Jan 17 10-08-37 @JoshJuncker @czredhat @sidlatau @daviddomkar @martingeorgiu Thank you in advanced.
simranKa commented 10 months ago

By making these changes I am able to get data in IOS:

  1. In ShareHandlerIosViewController -> loadIds() -> Replace ShareHandlerIosViewController.hostAppBundleIdentifier with your app bundle identifier and ShareHandlerIosViewController.appGroupId with your appGroupId
  2. Screenshot 2024-01-17 at 10 40 52 AM In Runner -> Build Phase reorder the list also work to get IOS app in share tray.

AimanKyo97 commented 10 months ago

By making these changes I am able to get data in IOS:

  1. In ShareHandlerIosViewController -> loadIds() -> Replace ShareHandlerIosViewController.hostAppBundleIdentifier with your app bundle identifier and ShareHandlerIosViewController.appGroupId with your appGroupId

Screenshot 2024-01-17 at 10 40 52 AM In Runner -> Build Phase reorder the list also work to get IOS app in share tray.

@simranKa I have try it but the data i receive still null, below is example code i used in ShareHandlerIosViewController, and Build Phases in my project

// // ShareHandlerIosViewController.swift // Pods // // Created by Josh Juncker on 7/7/22. //

import UIKit import Social import MobileCoreServices import Photos import Intents import Contacts

@available(iOS 14.0, ) @available(iOSApplicationExtension 14.0, ) open class ShareHandlerIosViewController: UIViewController { static var hostAppBundleIdentifier = "com.xxx.xxx” static var appGroupId = "group.com.xxx.xxx” let sharedKey = "ShareKey" var sharedText: [String] = [] let imageContentType = UTType.image.identifier let movieContentType = UTType.movie.identifier let textContentType = UTType.text.identifier let urlContentType = UTType.url.identifier let fileURLType = UTType.fileURL.identifier let dataContentType = UTType.data.identifier var sharedAttachments: [SharedAttachment] = [] lazy var userDefaults: UserDefaults = { return UserDefaults(suiteName: ShareHandlerIosViewController.appGroupId)! }()

public func loadIds() {
        // loading Share extension App Id
        let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!;

        // convert ShareExtension id to host app id
        // By default it is remove last part of id after last point
        // For example: com.test.ShareExtension -> com.test
        let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: ".");
    ShareHandlerIosViewController.hostAppBundleIdentifier = "com.xxx.xxx";

        // loading custom AppGroupId from Build Settings or use group.<hostAppBundleIdentifier>
    ShareHandlerIosViewController.appGroupId = "group.com.xxx.xxx”;
    }

public override func viewDidLoad() {
    super.viewDidLoad();

    // load group and app id from build info
            loadIds();
    Task {
        await handleInputItems()
    }
}

public override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}

func handleInputItems() async {
    if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
        if let contents = content.attachments {
            for (index, attachment) in (contents).enumerated() {
                do {
                    if attachment.hasItemConformingToTypeIdentifier(imageContentType) {
                        try await handleImages(content: content, attachment: attachment, index: index)
                    } else if attachment.hasItemConformingToTypeIdentifier(movieContentType) {
                        try await handleVideos(content: content, attachment: attachment, index: index)
                    } else if attachment.hasItemConformingToTypeIdentifier(fileURLType){
                        try await handleFiles(content: content, attachment: attachment, index: index)
                    } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
                        try await handleUrl(content: content, attachment: attachment, index: index)
                    } else if attachment.hasItemConformingToTypeIdentifier(textContentType) {
                        try await handleText(content: content, attachment: attachment, index: index)
                    } else if attachment.hasItemConformingToTypeIdentifier(dataContentType) {
                        try await handleData(content: content, attachment: attachment, index: index)
                    } else {
                        print("Attachment not handled with registered type identifiers: \(attachment.registeredTypeIdentifiers)")
                    }
                } catch {
                    self.dismissWithError()
                }

            }
        }
        redirectToHostApp()
    }
}

public func getNewFileUrl(fileName: String) -> URL {
    let newFileUrl = FileManager.default
        .containerURL(forSecurityApplicationGroupIdentifier: ShareHandlerIosViewController.appGroupId)!
        .appendingPathComponent(fileName)
    return newFileUrl
}

public func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws {
    let data = try await attachment.loadItem(forTypeIdentifier: textContentType, options: nil)

    if let item = data as? String {
        sharedText.append(item)
    } else {
        if let d = data as? Data {
            do{
                let contacts = try CNContactVCardSerialization.contacts(with: d)
                for contact in contacts {
                    let data = try CNContactVCardSerialization.data(with: [contact])
                    let str = String(data: data, encoding: .utf8)!
                    sharedText.append(str)
                }
            } catch {
                dismissWithError()
            }
        } else {
            dismissWithError()
        }
    }
} 

public func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws {
    let data = try await attachment.loadItem(forTypeIdentifier: urlContentType, options: nil)

        if let item = data as? URL {
            sharedText.append(item.absoluteString)
        } else {
            dismissWithError()
        }

}

public func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws {
    let data = try await attachment.loadItem(forTypeIdentifier: imageContentType, options: nil)

    var fileName: String?
    var imageData: Data?
    var sourceUrl: URL?
    if let url = data as? URL {
        fileName = getFileName(from: url, type: .image)
        sourceUrl = url
    } else if let iData = data as? Data {
        fileName = UUID().uuidString + ".png"
        imageData = iData
    } else if let image = data as? UIImage {
        fileName = UUID().uuidString + ".png"
        imageData = image.pngData()
    }

    if let _fileName = fileName {
        let newFileUrl = getNewFileUrl(fileName: _fileName)
        do {
            if FileManager.default.fileExists(atPath: newFileUrl.path) {
                try FileManager.default.removeItem(at: newFileUrl)
            }
        } catch {
            print("Error removing item")
        }

        var copied: Bool = false
        if let _data = imageData {
            copied = FileManager.default.createFile(atPath: newFileUrl.path, contents: _data)
        } else if let _sourceUrl = sourceUrl {
            copied = copyFile(at: _sourceUrl, to: newFileUrl)
        }

        if (copied) {
            sharedAttachments.append(SharedAttachment.init(path:  newFileUrl.absoluteString, type: .image))
        } else {
            dismissWithError()
            return
        }

    } else {
        dismissWithError()
        return
    }

}

public func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws {
    let data = try await attachment.loadItem(forTypeIdentifier: movieContentType, options: nil)

    if let url = data as? URL {

        // Always copy
        let fileName = getFileName(from: url, type: .video)
        let newFileUrl = getNewFileUrl(fileName: fileName)
        let copied = copyFile(at: url, to: newFileUrl)
        if(copied) {
            sharedAttachments.append(SharedAttachment.init(path:  newFileUrl.absoluteString, type: .video))
        }
    } else {
        dismissWithError()
    }

}

public func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws {
    let data = try await attachment.loadItem(forTypeIdentifier: fileURLType, options: nil)

    if let url = data as? URL {

        // Always copy
        let fileName = getFileName(from :url, type: .file)
        let newFileUrl = getNewFileUrl(fileName: fileName)
        let copied = copyFile(at: url, to: newFileUrl)
        if (copied) {
            sharedAttachments.append(SharedAttachment.init(path:  newFileUrl.absoluteString, type: .file))
        }
    } else {
        dismissWithError()
    }

}

public func handleData (content: NSExtensionItem, attachment: NSItemProvider, index: Int) async throws {
    let data = try await attachment.loadItem(forTypeIdentifier: dataContentType, options: nil)

    if let url = data as? URL {

        // Always copy
        let fileName = getFileName(from :url, type: .file)
        let newFileUrl = getNewFileUrl(fileName: fileName)
        let copied = copyFile(at: url, to: newFileUrl)
        if (copied) {
            sharedAttachments.append(SharedAttachment.init(path:  newFileUrl.absoluteString, type: .file))
        }
    } else {
        dismissWithError()
    }

}

public 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)
}

public func redirectToHostApp() {
    // ids may not loaded yet so we need loadIds here too
    loadIds();
    let url = URL(string: "ShareMedia-com.xxx.xxx://com.xxx.xxx?key=\(sharedKey)")
    var responder = self as UIResponder?
    let selectorOpenURL = sel_registerName("openURL:")

    let intent = self.extensionContext?.intent as? INSendMessageIntent

    let conversationIdentifier = intent?.conversationIdentifier
    let sender = intent?.sender
    let serviceName = intent?.serviceName
    let speakableGroupName = intent?.speakableGroupName

    let sharedMedia = SharedMedia.init(attachments: sharedAttachments, conversationIdentifier: conversationIdentifier, content: sharedText.joined(separator: "\n"), speakableGroupName: speakableGroupName?.spokenPhrase, serviceName: serviceName, senderIdentifier: sender?.contactIdentifier ?? sender?.customIdentifier, imageFilePath: nil)

    let json = sharedMedia.toJson()

    userDefaults.set(json, forKey: sharedKey)
    userDefaults.synchronize()

    while (responder != nil) {
        if (responder?.responds(to: selectorOpenURL))! {
            let _ = responder?.perform(selectorOpenURL, with: url)
        }
        responder = responder!.next
    }
}

enum RedirectType {
    case media
    case text
    case file
}

func getExtension(from url: URL, type: SharedAttachmentType) -> 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"
        default:
            ex = ""
        }
    }
    return ex ?? "Unknown"
}

func getFileName(from url: URL, type: SharedAttachmentType) -> 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
}

}

Build phases :

Screenshot at Jan 17 15-49-43

Below is my record video:

https://github.com/ShoutSocial/share_handler/assets/67236402/ad4ea1d3-bb16-46cc-af5e-9b7b05ce7e66

Is there something i missing? Thank you @simranKa

aniketcodebase commented 8 months ago

AimanKyo97

arrrrny commented 8 months ago

@aniketcodebase It has nothing to do with the loadIds, The build phase order is crucial, I was getting circular dependency error, so follow @simranKa on that, strangely when I compared the ShareExtension entitlements file with my project and the example project(which works fine). I realized Xcode added Wifi capability as well. I removed that. and also on the URL Schemes, make sure it is "ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)" Initially I made it something custom so that might be the issue as well. Happy hacking

Screenshot 2024-03-16 at 10 46 20 PM
aniketcodebase commented 8 months ago

in my case, actual problem was a mismatch (case sensitive) between the group-id (autogenerated after enabling the app-groups ) and bundle id

I had to correct my group-id to group.some.something.myTestApp

arrrrny commented 2 months ago

@AimanKyo97 I explained step by step how to setup fo Xcode 16 and IOS 18, the example app works right out the box by following instructions here

JoshJuncker commented 3 weeks ago

Closing this as it isn't a problem with the plugin, but a build phase ordering issue related specifically to Xcode build. If you think the readme needs additional info, please make a pull request with that info.