I've built a sharing extension for my iOS app. The goal is to store all URLs user shared in UserDefaults variable. Here is my code:
import UIKit
import Social
import MobileCoreServices
class ShareViewController: SLComposeServiceViewController {
override func viewDidLoad() {
let extensionItem = extensionContext?.inputItems.first as! NSExtensionItem
let itemProvider = extensionItem.attachments?.first as! NSItemProvider
let propertyList = String(kUTTypePropertyList)
if itemProvider.hasItemConformingToTypeIdentifier(propertyList) {
itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in
guard let dictionary = item as? NSDictionary else { return }
OperationQueue.main.addOperation {
if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary,
let urlString = results["URL"] as? String,
let url = NSURL(string: urlString) {
print("URL retrieved: \(urlString)")
let userDefaults = UserDefaults(suiteName: "group.my.app")
if userDefaults?.object(forKey: "extensionArticles") == nil {
userDefaults?.setValue([urlString], forKey: "extensionArticles")
} else {
var urls = userDefaults?.object(forKey: "extensionArticles") as? [String]
urls?.append(urlString)
print("im here in the extension")
dump(urls)
userDefaults?.setValue(urls, forKey: "extensionArticles")
}
}
}
})
} else {
print("error")
}
Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(self.didSelectPost), userInfo: nil, repeats: false)
}
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
return true
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
print("posted")
// 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.
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
}
When I go to the webpage in Safari and share it being on that page, everything works fine. However, when I long press (aka 3d touch) on a link in Safari, and use my extension from there, it won't add a link to UserDefaults. I couldn't find how to fix this, plz help ðŸ˜
Related
I am new to ios development. I am doing my first project and I want to implement a small application that will allow saving favorite links from the browser. I am using SwiftUI for main app + share extension component to pass link to main app from browser.
The flow looks like this: I find an interesting site, click the Share button and select the icon for my application. After that, my application opens and there I see my link. This works successfully, but after that the browser freezes completely. If I post a link from other apps, the same thing happens.
Below is the code of the share extension and the component of the main application that reads the link:
#objc(ShareExtensionViewController)
class ShareViewController: UIViewController {
private static let URL_STORAGE_KEY = "url"
private static let APP_URL = "MyApp://"
private static let APP_GROUP_NAME = "group.ru.myapp"
override func viewDidLoad() {
super.viewDidLoad()
// set link to UserDefaults
handleShared()
// open main app from extension
openMainApp()
}
#objc
#discardableResult
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
}
private func openMainApp() {
DispatchQueue.global(qos: .background).async {
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: { _ in
let url = URL(string: ShareViewController.APP_URL)!
self.openURL(url)
})
}
}
private func handleShared() {
let attachments = (extensionContext?.inputItems.first as? NSExtensionItem)?.attachments ?? []
let contentType = kUTTypeURL as String
for provider in attachments {
if provider.hasItemConformingToTypeIdentifier(contentType) {
provider.loadItem(forTypeIdentifier: contentType, options: nil) {
[unowned self] (data, error) in
guard error == nil else {
return
}
let userDefaults = UserDefaults(suiteName: ShareViewController.APP_GROUP_NAME)!
userDefaults.set(data, forKey: ShareViewController.URL_STORAGE_KEY)
}
}
}
}
}
Snippet of the main application code:
static func tryGetLink() -> String? {
let userDefaults = UserDefaults(suiteName: DataStorage.APP_GROUP)
return userDefaults?.object(forKey: URL_KEY) as? String
}
What could be the reason for these freezes?
How to navigate Share Extension to host app in Swift after getting URL from ShareExtension?
import UIKit
import Social
import MobileCoreServices
class ShareViewController: UIViewController {
let sharedKey = "shareappKey"
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Picked URL"
getURL()
}
#IBAction func nextAction(_ sender: Any) {
self.redirectToHostApp()
}
#IBAction func cancelAction(_ sender: Any) {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func redirectToHostApp() {
let url = URL(string: "SelectedURL:\(sharedKey)")
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
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func getURL() {
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
if let itemProvider = item.attachments?.first {
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { (url, error) -> Void in
if let error = error {
print("error :-", error)
}
if (url as? NSURL) != nil {
// send url to server to share the link
do {
if (url as? URL) != nil {
// do what you want to do with shareURL
print("Selected URL :- ", url as Any)
print(url as Any)
let dict: [String : Any] = ["imgData" : url as Any, "name" : "Added" as Any]
print(dict)
let userDefault = UserDefaults.init(suiteName: "group.com.abcd.shareapp")
userDefault?.set(dict, forKey: self.sharedKey)
userDefault?.synchronize()
// Here I got Struct
}
}catch let err{
print(err)
}
}
self.extensionContext?.completeRequest(returningItems: [], completionHandler:nil)
})
}
}
}
}
}
// Host App
import UIKit
class ViewController: UIViewController {
var urlString = String()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let userDefault = UserDefaults.standard
userDefault.addSuite(named: "group.com.abcd.shareapp")
if let dict = userDefault.value(forKey: "img") as? NSDictionary{
let data = dict.value(forKey: "imgData") as! Data
let str = dict.value(forKey: "name") as! String
print("Data is :- ", data)
print("Str is :- ", str)
userDefault.removeObject(forKey: "img")
userDefault.synchronize()
// Here i need to get that URL from Share Extention
}
}
}
Error
2019-07-15 22:01:15.045361+0530 LearingAppShare[3183:73775] [User
Defaults] Attempt to set a non-property-list object {
imgData = "https://m.jagran.com/lite/cricket/headlines-sachin-tendulkar-son-arjun-tendulkar-picked-for-rs-5-lakh-for-t20-mumbai-league-19192257.html";
name = Added; } as an NSUserDefaults/CFPreferences value for key URLKey 2019-07-15 22:01:15.047424+0530 LearingAppShare[3183:73775]
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'Attempt to insert non-property
list object {
imgData = "https://m.jagran.com/lite/cricket/headlines-sachin-tendulkar-son-arjun-tendulkar-picked-for-rs-5-lakh-for-t20-mumbai-league-19192257.html";
name = Added; } for key URLKey'
I have done answer on own Question.
You can store in dictionary.
But UserDefaults can't save dictionary with custom data types like Image or URL .
So you need to convert dictionary to Data first before saving in defaults
import UIKit
import Social
import MobileCoreServices
class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Picked URL"
getURL()
}
#IBAction func nextAction(_ sender: Any) {
self.redirectToHostApp()
}
#IBAction func cancelAction(_ sender: Any) {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func redirectToHostApp() {
let url = URL(string: "YourOwnURLscheme:\(sharedKey)")
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
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func getURL() {
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
if let itemProvider = item.attachments?.first {
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { (url, error) -> Void in
if let error = error {
print("error :-", error)
}
if (url as? NSURL) != nil {
// send url to server to share the link
do {
var urlData: Data!
if let url = url as? URL{
urlData = try Data(contentsOf: url)
}
let dict: [String : Any] = ["urlData" : urlData as Any, "name" : self.contentText as Any]
print(dict)
let userDefault = UserDefaults(suiteName: "group.com.abcd.sharecontent1")
userDefault?.set(dict, forKey: "storedURLData")
userDefault?.synchronize()
}catch let err{
print(err)
}
}
})
}
}
}
}
}
// Host App
import UIKit
class ViewController: UIViewController {
#IBOutlet var imgView: UIImageView!
#IBOutlet var lblText: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let userDefault = UserDefaults(suiteName: "group.com.abcd.sharecontent1")
if let dict = userDefault?.value(forKey: "storedURLData") as? NSDictionary{
let data = dict.value(forKey: "urlData") as! Data
let str = dict.value(forKey: "name") as! String
print("Data is :- ", data)
print("str is :- ", str)
self.lblText.text = str
userDefault?.removeObject(forKey: "storedURLData")
userDefault?.synchronize()
}
}
}
How the title says, my Share extension crashes when I open a url.
Xcode says: Thread 3: EXC_BAD_ACCESS (code=1, address=0x10)
I already tried with other url schemes and the result is same.
This is my code:
import UIKit
import MobileCoreServices
class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let content = extensionContext!.inputItems[0] as! NSExtensionItem
let text = kUTTypeText as String
for attachment in content.attachments as! [NSItemProvider] {
if attachment.hasItemConformingToTypeIdentifier(text) {
attachment.loadItem(forTypeIdentifier: text, options: nil, completionHandler: { (data, error) in
if error == nil {
let url = data as! URL
print(url)
print("mSwift://?\(url.absoluteString)")
let mSwift = URL(string: "mSwift://?\(url.absoluteString)")
self.extensionContext!.open(mSwift!, completionHandler: nil) // Crash here
}else {
}
})
}
}
}
}
I am trying to link an button on the UI to a link that I receive after parsing a JSON response. I've added an outlet to the button with the a variable containing the link.
#IBAction func goOnline(sender: AnyObject) {
UIApplication.sharedApplication().openURL(prodURL)
}
I parse the JSON data in a method called in viewDidLoad.
if let convertedJSONIntoDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [String: AnyObject] {
if let JSONurl = convertedJSONIntoDict["url"] as? String {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.prodURL = NSURL(string: JSONurl)!
}
}
}
When I click on the button I don't get any response. As always, any help is much appreciated.
i dont have enough reputation to comment so i am adding here
what value do you get in json url???
even you dont need to save the value in the main queue you can also write like
self.prodURL = NSURL(string: JSONurl)!
Swift 3
Define your strings and variables and then it's the following code:
#IBAction func websiteButton(_ sender: Any) {
let websiteURL = ("http://") + JSONurl!
guard let url = URL(string: websiteURL) else {
return //be safe
}
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
}
That should work.
Once my share extension is loaded, not everytime and not everything is visible for user immediately. The most common is that there you can see image, number of images, and content text. However there are cases where there is a lot more informations.
How to get access to them?
I know that within SLComposeServiceViewController there is extensionContext and its inputItems property.
Ok, so I stopped the debugger at time, and print out on console some things with following command:
po (extensionContext!.inputItems[0] as! NSExtensionItem).userInfo![NSExtensionItemAttachmentsKey]
Is it correct way to do this?
Is there usually one input item?
there was two NSItemProvider objects as attachments to first NSExtensionItem
Ok, then I print out the first of attachments:
How to get that image from that NSItemProvider and url from the next one? Can you deliver some code?
I suppose we will use
loadItemForTypeIdentifier(_:options:completionHandler:)
but do not know how.
import MobileCoreServices
There is a simple function you can apply to your code:
private func fetchAndSetContentFromContext() {
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
return
}
for extensionItem in extensionItems {
if let itemProviders = extensionItem.attachments as? [NSItemProvider] {
for itemProvider in itemProviders {
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
itemProvider.loadItemForTypeIdentifier(kUTTypeText as String, options: nil, completionHandler: { text, error in
})
}
}
}
}
}
So now you know to use the loadItemForTypeIdentifier(_:options:completionHandler:) method to load your aspired data.
In your snapshots you wan to get image and url objects.
Let's begin.
guard
let items = extensionContext?.inputItems as? [NSExtensionItem],
let item = items.first,
let attachments = item.attachments
else { return }
var image: UIImage?
var url: URL?
let semaphore = DispatchSemaphore(value: 2)
for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
attachment.loadItem(forTypeIdentifier: kUTTypeImage as String) { item, _ in
image = item as? UIImage
semaphore.signal()
}
}
if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
attachment.loadItem(forTypeIdentifier: kUTTypeURL as String) { item, _ in
url = item as? URL
semaphore.signal()
}
}
}
_ = semaphore.wait(timeout: .now() + 1.0)
print(String(describing: image))
print(String(describing: url))