Share Extension to open containing app

zanzoken picture zanzoken · Dec 16, 2014 · Viewed 27.6k times · Source

I want to create an Android Style share feature for my app. I created a share extension which gets called when you select pictures inside the stock photo app and press share. Now I want those pictures to be sent to the main app and get handled over there. My question is now:

  1. Can iOS open my app after a button is pressed on the share extension window?
  2. How do I get the picture files inside my main app?

Answer

coyer picture coyer · Jun 12, 2017

Swift 4+ (tested on iOS 13)

@objc should be added to the declaration of openURL, that is,

@objc func openURL(_ url: URL) -> Bool {
    // Code below.
}

Without it one would see this compiler error:

Argument of '#selector' refers to instance method 'openURL' that is not exposed to Objective-C

Working solution in Swift 3.1 (tested in iOS10):

You need to create your own URL Scheme, then add this function to your ViewController and call it with openURL("myScheme://myIdentifier")

//  Function must be named exactly like this so a selector can be found by the compiler!
//  Anyway - it's another selector in another instance that would be "performed" instead.
func openURL(_ url: URL) -> Bool {
    var responder: UIResponder? = self
    while responder != nil {
        if let application = responder as? UIApplication {
            return application.perform(#selector(openURL(_:)), with: url) != nil
        }
        responder = responder?.next
    }
    return false
}

Edit: Notes for clarification: openURL is a method of UIApplication - since your ShareExtension is not derived from UIApplication I added my own openURL with the same definition as the one from UIApplication to keep the compiler happy (so that #selector(openURL(_:) can be found).

Then I go through the responders until I find one that is really derived from UIApplication and call openURL on that.

More stripped-down-example-code which copies files in a ShareExtension to a local directory, serializing filenames and calling openURL on another app:

//
//  ShareViewController.swift
//

import UIKit
import Social
import MobileCoreServices

class ShareViewController: UIViewController {

var docPath = ""

override func viewDidLoad() {
    super.viewDidLoad()

    let containerURL = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.com.my-domain")!
    docPath = "\(containerURL.path)/share"
    
    //  Create directory if not exists
    do {
        try FileManager.default.createDirectory(atPath: docPath, withIntermediateDirectories: true, attributes: nil)
    } catch let error as NSError {
        print("Could not create the directory \(error)")
    } catch {
        fatalError()
    }

    //  removing previous stored files
    let files = try! FileManager.default.contentsOfDirectory(atPath: docPath)
    for file in files {
        try? FileManager.default.removeItem(at: URL(fileURLWithPath: "\(docPath)/\(file)"))
    }
}

override func viewDidAppear(_ animated: Bool) {

    let alertView = UIAlertController(title: "Export", message: " ", preferredStyle: .alert)
    
    self.present(alertView, animated: true, completion: {

        let group = DispatchGroup()
        
        NSLog("inputItems: \(self.extensionContext!.inputItems.count)")
        
            for item: Any in self.extensionContext!.inputItems {
                
            let inputItem = item as! NSExtensionItem
            
            for provider: Any in inputItem.attachments! {
                
                let itemProvider = provider as! NSItemProvider
                group.enter()
                itemProvider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { data, error in
                    if error == nil {
                        //  Note: "data" may be another type (e.g. Data or UIImage). Casting to URL may fail. Better use switch-statement for other types.
                        //  "screenshot-tool" from iOS11 will give you an UIImage here
                        let url = data as! URL
                        let path = "\(self.docPath)/\(url.pathComponents.last ?? "")"
                        print(">>> sharepath: \(String(describing: url.path))")

                        try? FileManager.default.copyItem(at: url, to: URL(fileURLWithPath: path))
                        
                    } else {
                        NSLog("\(error)")
                    }
                    group.leave()
                }
            }
        }
        
        group.notify(queue: DispatchQueue.main) {
            NSLog("done")
            
            let files = try! FileManager.default.contentsOfDirectory(atPath: self.docPath)
            
            NSLog("directory: \(files)")
            
            //  Serialize filenames, call openURL:
            do {
                let jsonData : Data = try JSONSerialization.data(
                    withJSONObject: [
                        "action" : "incoming-files"
                        ],
                    options: JSONSerialization.WritingOptions.init(rawValue: 0))
                let jsonString = (NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
                let result = self.openURL(URL(string: "myapp://com.myapp.share?\(jsonString!)")!)
            } catch {
                alertView.message = "Error: \(error.localizedDescription)"
            }
            self.dismiss(animated: false) {
                self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
            }
        }
    })
}

//  Function must be named exactly like this so a selector can be found by the compiler!
//  Anyway - it's another selector in another instance that would be "performed" instead.
@objc func openURL(_ url: URL) -> Bool {
    var responder: UIResponder? = self
    while responder != nil {
        if let application = responder as? UIApplication {
            return application.perform(#selector(openURL(_:)), with: url) != nil
        }
        responder = responder?.next
    }
    return false
}
}