Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support "No Share Extension UI" flow on iOS #285

Open
7 tasks
lindboe opened this issue Dec 6, 2023 · 6 comments
Open
7 tasks

Support "No Share Extension UI" flow on iOS #285

lindboe opened this issue Dec 6, 2023 · 6 comments

Comments

@lindboe
Copy link

lindboe commented Dec 6, 2023

In this ticket, we'll add support for the option to make iOS behave like Android: navigate immediately into the main app to share, without any visible share extension UI. (The share extension will still exist and run; it will just immediately link to the main app without showing any UI, using the linking logic already present in the library).

Scope:

  • Add another view controller, like ShareViewController or ReactShareViewController, that library users can link to.
  • Refactor business logic where necessary so it can be shared across the view controllers.
  • Add documentation explaining how to set this new view controller up. Include instructions on how to configure the podfile so that react-native-share-menu is the only pod linked to the project in this case (i.e., don't auto-link all your native RN dependencies in the share extension)
  • Add an entry to the Changelog for this
  • Add an example to the repo that uses this "No Share Extension UI" flow on iOS (blocked by Update example project #284).
@skizzo
Copy link

skizzo commented Jan 4, 2024

Hi,
are there any updates on this? I would really like to implement this "No Share Extension UI flow", mentioned in issue #300.

@lindboe
Copy link
Author

lindboe commented Jan 5, 2024

Hi @skizzo, at the moment we're working on wrapping up another project but should get started on this soon.

@gbyesiltas
Copy link

Hey, is there an update on this? :)

@sanguineman91
Copy link

Hello, is there an update on this? :)

@jaaywags
Copy link

jaaywags commented Jul 7, 2024

I don't have a good solution but I have a solution. I followed the ios instructions and at the bottom of the override func viewDidLoad() { function, I added this didSelectPost(). Also, when I added the ShareViewController.swift file, I set Copy FIle so that I can have a hard copy in my repo and it won't be replaced whenever we run a fresh node module install.

Though, one downside I noticed is that if you wait for some time, then navigate back to the previous app, the social media post view does show. Still working on a better solution.

@jaaywags
Copy link

jaaywags commented Jul 7, 2024

Okay, I have a nice clean solution now. I have not tested it with much though. I really just needed a way for my app to show when I share a website and then have my app accept the URL and do stuff with it. I haven't tested any other scenarios like sharing text, images, files, or anything else.

  1. Go to the share extension project in xcode
  2. Remove the ShareViewController.swift file. If you added it without copying, then just remove the reference so it stays in the node_modules folder. If you copied it, then move it to trash.
  3. Right click the project
  4. Select New File
  5. Select a Swift file and name it ShareViewController. It should be created as ShareViewController.swift.
  6. Add this to it
// This file comes from the react-native-share-menu package.
// ./node_modules/react-native-share-menu/ios/ShareViewController.swift

import Foundation
import MobileCoreServices
import UIKit
import Social
import RNShareMenu

class ShareViewController: UIViewController {
  var hostAppId: String?
  var hostAppUrlScheme: String?
  var sharedItems: [Any] = []
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    if let hostAppId = Bundle.main.object(forInfoDictionaryKey: HOST_APP_IDENTIFIER_INFO_PLIST_KEY) as? String {
      self.hostAppId = hostAppId
    } else {
      print("Error: \(NO_INFO_PLIST_INDENTIFIER_ERROR)")
    }
    
    if let hostAppUrlScheme = Bundle.main.object(forInfoDictionaryKey: HOST_URL_SCHEME_INFO_PLIST_KEY) as? String {
      self.hostAppUrlScheme = hostAppUrlScheme
    } else {
      print("Error: \(NO_INFO_PLIST_URL_SCHEME_ERROR)")
    }
    
    // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
    guard let items = extensionContext?.inputItems as? [NSExtensionItem] else {
      cancelRequest()
      return
    }

    handlePost(items)
  }

  func handlePost(_ items: [NSExtensionItem], extraData: [String:Any]? = nil) {
    DispatchQueue.global().async {
      guard let hostAppId = self.hostAppId else {
        self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR)
        return
      }
      guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else {
        self.exit(withError: NO_APP_GROUP_ERROR)
        return
      }

      if let data = extraData {
        self.storeExtraData(data)
      } else {
        self.removeExtraData()
      }

      let semaphore = DispatchSemaphore(value: 0)
      var results: [Any] = []

      for item in items {
        guard let attachments = item.attachments else {
          self.cancelRequest()
          return
        }

        for provider in attachments {
          if provider.isText {
            self.storeText(withProvider: provider, semaphore)
          } else if provider.isURL {
            self.storeUrl(withProvider: provider, semaphore)
          } else {
            self.storeFile(withProvider: provider, semaphore)
          }

          semaphore.wait()
        }
      }

      userDefaults.set(self.sharedItems,
                       forKey: USER_DEFAULTS_KEY)
      userDefaults.synchronize()

      self.openHostApp()
    }
  }

  func storeExtraData(_ data: [String:Any]) {
    guard let hostAppId = self.hostAppId else {
      print("Error: \(NO_INFO_PLIST_INDENTIFIER_ERROR)")
      return
    }
    guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else {
      print("Error: \(NO_APP_GROUP_ERROR)")
      return
    }
    userDefaults.set(data, forKey: USER_DEFAULTS_EXTRA_DATA_KEY)
    userDefaults.synchronize()
  }

  func removeExtraData() {
    guard let hostAppId = self.hostAppId else {
      print("Error: \(NO_INFO_PLIST_INDENTIFIER_ERROR)")
      return
    }
    guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else {
      print("Error: \(NO_APP_GROUP_ERROR)")
      return
    }
    userDefaults.removeObject(forKey: USER_DEFAULTS_EXTRA_DATA_KEY)
    userDefaults.synchronize()
  }
  
  func storeText(withProvider provider: NSItemProvider, _ semaphore: DispatchSemaphore) {
    provider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil) { (data, error) in
      guard (error == nil) else {
        self.exit(withError: error.debugDescription)
        return
      }
      guard let text = data as? String else {
        self.exit(withError: COULD_NOT_FIND_STRING_ERROR)
        return
      }
      
      self.sharedItems.append([DATA_KEY: text, MIME_TYPE_KEY: "text/plain"])
      semaphore.signal()
    }
  }
  
  func storeUrl(withProvider provider: NSItemProvider, _ semaphore: DispatchSemaphore) {
    provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (data, error) in
      guard (error == nil) else {
        self.exit(withError: error.debugDescription)
        return
      }
      guard let url = data as? URL else {
        self.exit(withError: COULD_NOT_FIND_URL_ERROR)
        return
      }
      
      self.sharedItems.append([DATA_KEY: url.absoluteString, MIME_TYPE_KEY: "text/plain"])
      semaphore.signal()
    }
  }
  
  func storeFile(withProvider provider: NSItemProvider, _ semaphore: DispatchSemaphore) {
    provider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { (data, error) in
      guard (error == nil) else {
        self.exit(withError: error.debugDescription)
        return
      }
      guard let url = data as? URL else {
        self.exit(withError: COULD_NOT_FIND_IMG_ERROR)
        return
      }
      guard let hostAppId = self.hostAppId else {
        self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR)
        return
      }
      guard let groupFileManagerContainer = FileManager.default
              .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppId)")
      else {
        self.exit(withError: NO_APP_GROUP_ERROR)
        return
      }
      
      let mimeType = url.extractMimeType()
      let fileExtension = url.pathExtension
      let fileName = UUID().uuidString
      let filePath = groupFileManagerContainer
        .appendingPathComponent("\(fileName).\(fileExtension)")
      
      guard self.moveFileToDisk(from: url, to: filePath) else {
        self.exit(withError: COULD_NOT_SAVE_FILE_ERROR)
        return
      }
      
      self.sharedItems.append([DATA_KEY: filePath.absoluteString, MIME_TYPE_KEY: mimeType])
      semaphore.signal()
    }
  }

  func moveFileToDisk(from srcUrl: URL, to destUrl: URL) -> Bool {
    do {
      if FileManager.default.fileExists(atPath: destUrl.path) {
        try FileManager.default.removeItem(at: destUrl)
      }
      try FileManager.default.copyItem(at: srcUrl, to: destUrl)
    } catch (let error) {
      print("Could not save file from \(srcUrl) to \(destUrl): \(error)")
      return false
    }
    
    return true
  }
  
  func exit(withError error: String) {
    print("Error: \(error)")
    cancelRequest()
  }
  
  internal func openHostApp() {
    guard let urlScheme = self.hostAppUrlScheme else {
      exit(withError: NO_INFO_PLIST_URL_SCHEME_ERROR)
      return
    }
    
    let url = URL(string: urlScheme)
    let selectorOpenURL = sel_registerName("openURL:")
    var responder: UIResponder? = self
    
    while responder != nil {
      if responder?.responds(to: selectorOpenURL) == true {
        responder?.perform(selectorOpenURL, with: url)
      }
      responder = responder!.next
    }
    
    completeRequest()
  }
  
  func completeRequest() {
    // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
    extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
  }
  
  func cancelRequest() {
    extensionContext!.cancelRequest(withError: NSError())
  }
}
  1. Rebuild your extension and that is it.

If you are curious what I changed, it was not much. The original was extending a subclass SLComposeServiceViewController but I changed it to UIViewController. That subclass is Apple's default View for social media posts.

I also removed some override functions (isContentValid, didSelectPost, and configurationItems) that subclass required.

Finally, I added a call to handlePost() at the end of the viewDidLoad() function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants