receive_sharing_intent icon indicating copy to clipboard operation
receive_sharing_intent copied to clipboard

App blocked after share iOS

Open steno983 opened this issue 3 years ago • 11 comments

I just created a demo app like the example. After some fix, it works, but when I share a link from an app (chrome, amazon) the starting app stops to works. If I share a link from the same app to another, it continue to work

steno983 avatar Sep 29 '20 10:09 steno983

Use the code for ShareViewController.swift from example directory. That solved the problem for me (2 days ago)

klausszilvas avatar Oct 01 '20 17:10 klausszilvas

I just created a demo app like the example. After some fix, it works, but when I share a link from an app (chrome, amazon) the starting app stops to works. If I share a link from the same app to another, it continue to work

Same here, i've copied example directory. This affects safari browser (trying share a link). Using Xcode 12.0.1 and iOS 14.0. Seems like this relates to iOS simulator, everything works fine on real iPhone SE 13.7.

igokom avatar Oct 06 '20 20:10 igokom

I tried even with amazon app on ios14 real device. Still blocking, even with the example .swift file. I'll try to create a new project, starting from example folder, and then build my app over it.

steno983 avatar Oct 12 '20 07:10 steno983

Does anyone know how to pass this issue?

luunc avatar Nov 24 '20 02:11 luunc

I got the same problem here.

Looks like the share extension will always has a "post window" opened. But the following click on the 'Cancel button', Safari ( sharing the URL) froze.

The official doc says that the following should be executed to make sure the clean-up. But looks that something wrong with it here?

extensionContext!.completeRequest(returningItems: [], completionHandler: nil)

Any idea?

Thanks.

jianmei avatar Nov 26 '20 20:11 jianmei

I am about to submit my app and I have a feeling its going to be rejected due to this issue. Did anyone manage to resolve this issue?

varundroid avatar Nov 29 '20 07:11 varundroid

the same problem

honjow avatar Nov 29 '20 17:11 honjow

Same problem here

melbek avatar Dec 25 '20 20:12 melbek

I am not sure what is the issue yet, but i could make it "work" by overriding the viewDidAppear and

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

By doing that removed from the completeRequest from the redirectToHostApp

    private func redirectToHostApp(type: RedirectType) {
        ...
        // extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
        ...
    }

anderson-oki avatar Jan 03 '21 05:01 anderson-oki

I just move the code handle from didSelectPost to viewDidAppear and it works. I still keep completeRequest in function redirectToHostApp

override func viewDidAppear(_ animated: Bool) {
    if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
        if let contents = content.attachments {
            for (index, attachment) in (contents).enumerated() {
                if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
                    handleUrl(content: content, attachment: attachment, index: index)
                } else if attachment.hasItemConformingToTypeIdentifier(textContentType) {
                    handleText(content: content, attachment: attachment, index: index)
                } else {
                    extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
                }
            }
        }
    }
}

override func didSelectPost() {
    print("didSelectPost");
}

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

qhu91it avatar Feb 09 '21 15:02 qhu91it

Hi there. Nothing works for me from suggestions above and linked issue. But I have own solution after 5 hours of debugging.

Problem: Sharing text frizzes Safari.

Main points in my solution:

  1. Use didSelectPost() for handling, as described in ios docs. NOT viewDidLoad() or viewDidAppear() because these functions for animations, not for actions.
  2. Correctly raise errors. dismissWithError does not work as expected, so I moved completion after clicking "OK" on error message. And also pass error message to alert. Sorry, but original Error loading data means nothing.
  3. Handle kUTTypePlainText UTI together with kUTTypeText.

My full solution:

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 plainTextContentType = kUTTypePlainText as String
    let urlContentType = kUTTypeURL as String
    let fileURLType = kUTTypeFileURL as String;

    override func isContentValid() -> Bool {
        return true
    }

    override func didSelectPost() {
        // 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(plainTextContentType) {
                        handlePlainText(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)
                    } else {
                        let msg = "Not found intent handler for " + attachment.registeredTypeIdentifiers.description
                        self.dismissWithError(message: msg)
                    }
                }
            }
        }
    }

    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 handlePlainText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
            attachment.loadItem(forTypeIdentifier: plainTextContentType, 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(message: error.debugDescription)
                }
            }
        }

    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(message: String = "Error loading data") {
        print("[ERROR] \(message)")
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)

        let action = UIAlertAction(title: "Error", style: .cancel) { _ in
            self.dismiss(animated: true, completion: nil)
            self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
        }

        alert.addAction(action)
        present(alert, animated: true, completion: 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
    }
}

vetcher avatar Jul 14 '21 07:07 vetcher