iOS 8 Share extension loadItemForTypeIdentifier:options:completionHandler: completion closure not executing - share

I'm using the
loadItemForTypeIdentifier:options:completionHandler: method on an NSItemProvider object to extract a url from Safari via a Share extension in iOS 8.
In Objective-C, this code and works and the block runs.
[itemProvider loadItemForTypeIdentifier:(#"public.url" options:nil completionHandler:^(NSURL *url, NSError *error) {
//My code
}];
In Swift, it looks very similar, however the closure doesn't run. Also, itemProvider.hasItemConformingToTypeIdentifier("public.url") returns YES so there must be a valid object to parse the url from inside the itemProvider.
itemProvider.loadItemForTypeIdentifier("public.url", options: nil, completionHandler: { (urlItem, error) in
//My code
})
The Info.plist NSExtension portion is exactly the same for both Objective-C and Swift version and looks like this:
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionPointName</key>
<string>com.apple.share-services</string>
<key>NSExtensionPointVersion</key>
<string>1.0</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
</dict>
What am I doing wrong?

call
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
at the end of completionHandler instead of calling it at the end of didSelectPost()

Since completeRequestReturningItems must be called after all completionHandlers are called back, below is what I do.
let group = dispatch_group_create()
for item: AnyObject in self.extensionContext!.inputItems {
let inputItem = item as! NSExtensionItem
for provider: AnyObject in inputItem.attachments! {
let itemProvider = provider as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
dispatch_group_enter(group)
itemProvider.loadItemForTypeIdentifier("public.url", options: nil, completionHandler: {
(result: NSSecureCoding!, error: NSError!) -> Void in
//...
dispatch_group_leave(group)
});
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
dispatch_group_enter(group)
itemProvider.loadItemForTypeIdentifier(kUTTypeImage as String, options: nil, completionHandler: { (result, error) -> Void in
if let resultURL = result as? NSURL {
if let image = UIImage(data: NSData(contentsOfURL: resultURL)!) {
// ...
}
}
dispatch_group_leave(group)
});
}
}
}
dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
})

I take no credit for this, but have a look at how this guy did it: https://github.com/oguzbilgener/SendToInstapaper/blob/master/ShareExtension/SendingViewController.swift

I was never managed completionHandler to work properly for Share extension with no user interface (in such case extension's class is a subclass on NSObject).
Despite the [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL] returns YES the completionHandler is never called both on the device or simulator.
After trying different approaches I ended up with workaround based on javascript passing URL back to extension (sorry as I use ObjC not Swift for my example).
Info.plist NSExtension portion:
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>Action</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.services</string>
<key>NSExtensionPrincipalClass</key>
<string>ActionRequestHandler</string>
</dict>
Javascript Action.js file:
var Action = function() {};
Action.prototype = {
run: function(arguments) {
arguments.completionFunction({ "currentURL" : window.location.href })
},
finalize: function(arguments) {
}
};
var ExtensionPreprocessingJS = new Action
ActionRequestHandler.h header file:
#interface ActionRequestHandler : NSObject <NSExtensionRequestHandling>
#end
ActionRequestHandler.m based on default Action extension template:
#import "ActionRequestHandler.h"
#import <MobileCoreServices/MobileCoreServices.h>
#interface ActionRequestHandler ()
#property (nonatomic, strong) NSExtensionContext *extensionContext;
#end
#implementation ActionRequestHandler
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context {
// Do not call super in an Action extension with no user interface
self.extensionContext = context;
BOOL found = NO;
// Find the item containing the results from the JavaScript preprocessing.
for (NSExtensionItem *item in self.extensionContext.inputItems) {
for (NSItemProvider *itemProvider in item.attachments) {
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePropertyList]) {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *dictionary, NSError *error) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self itemLoadCompletedWithPreprocessingResults:dictionary[NSExtensionJavaScriptPreprocessingResultsKey]];
}];
}];
found = YES;
}
break;
}
if (found) {
break;
}
}
if (!found) {
// We did not find anything - signal that we're done
[self.extensionContext completeRequestReturningItems:#[] completionHandler:nil];
// Don't hold on to this after we finished with it
self.extensionContext = nil;
}
}
- (void)itemLoadCompletedWithPreprocessingResults:(NSDictionary *)javaScriptPreprocessingResults
{
// Get the URL
if ([javaScriptPreprocessingResults[#"currentURL"] length] != 0) {
NSLog(#"*** URL: %#", javaScriptPreprocessingResults[#"currentURL"]);
}
// Signal that we're done
[self.extensionContext completeRequestReturningItems:#[] completionHandler:nil];
// Don't hold on to this after we finished with it
self.extensionContext = nil;
}
#end
Hope it will help somebody to save couple of hours struggling with the completionHandler issue.

I had the same issue in my iOS 12.1. I'm calling
loadItemForTypeIdentifier:kUTTypeData
instead of kUTTypeImage etc. It worked for me.

I have been battled with this issue on and off for the last few weeks, and have finally found the issue. I has nothing to do with Objective C or Swift, it just appears to be a bug in Apple's code.
It seems that (as at iOS 8.0), the completion block is only called if you are using your own UIViewController subclass. If you are using a subclass of SLComposeServiceViewController, then the completion block is not called.
This is really annoying, as by default XCode creates you a ShareViewController with a subclass of SLComposeServiceViewController. To work around this issue, you just have to modify ShareViewController to inherit from UIViewController. This will still give access to the extensionContext property, but you'll obviously lose all of nice standard functionality and will have to implement your UI from scratch.
I've filed a radar with Apple, but have not had a reply yet. Hopefully this will be fixed in a future update.

Related

In iOS, how can I get the contact shared from "Recent" calls list to my app in Flutter?

Please excuse me if I sound stupid, I'm new to flutter.
I have started learning flutter recently and wanted to create an app where anyone can share a contact from the "Recent" calls list to my app. I'm following this blog post which allows text share from any other app to my app.
What I have done so far:
This is my plist file, added the public.vcard to allow my app to appear on the tap of "Share Contact".
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>
SUBQUERY (
extensionItems, $extensionItem,
SUBQUERY (
$extensionItem.attachments, $attachment,
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.vcard"
).#count >= 1
).#count > 0
</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
Here's my ShareViewController.swift
import Social
import MobileCoreServices
class ShareViewController: SLComposeServiceViewController {
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
print("Something is not right")
return true
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// 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.
let sharedSuiteName: String = "group.com.thelogicalbeing.whatsappshare"
let sharedDataKey: String = "SharedData"
let extensionItem = extensionContext?.inputItems[0] as! NSExtensionItem
let contentTypeText = kUTTypeText as String // Note, you need to import 'MobileCoreServices' for this
for attachment in extensionItem.attachments! {
print(attachment)
if attachment.hasItemConformingToTypeIdentifier(contentTypeText) {
attachment.loadItem(forTypeIdentifier: contentTypeText, options: nil, completionHandler: {(results, error) in
if let sharedText = results as! String? {
if let userDefaults = UserDefaults(suiteName: sharedSuiteName) {
userDefaults.set(sharedText, forKey: sharedDataKey)
}
}
})
}
}
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 []
}
}
Here's my AppDelegate.swift
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let sharedSuiteName: String = "group.com.thelogicalbeing.whatsappshare"
let sharedDataKey: String = "SharedData"
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "com.thelogicalbeing.whatsappshare", binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: #escaping FlutterResult) -> Void in
if call.method == "getSharedData" {
if let prefs = UserDefaults(suiteName: sharedSuiteName) {
if let sharedText = prefs.string(forKey: sharedDataKey) {
result(sharedText);
}
// clear out the cached data
prefs.set("", forKey: sharedDataKey);
}
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
What I'm trying to achieve is that I need to receive the phone number and display it in my app.
Don't know how to proceed. Any help will be appreciated.
1- Apple does not allow fetching call logs on iOS!
You can fetch all contacts with all their information. But not the calls log.
2- On Android you can use the pub.dev dependency plugin call_log to do that.
Have a look at this package called receive_sharing_intent, it allows you to to receive sharing photos, videos, text, urls or any other file types from another app. And it also supports iOS Share extension and launching the host app automatically.

How to read the string in an attached file, not sharing the string directly, using Share Extension from other APP to my APP in Swift?

In the following code I can read text in this way and print the text in the code.
But I do not know how to share txt file as an attachment and read it out in the following code.
import UIKit
import Social
import CoreServices
class ShareViewController: SLComposeServiceViewController {
private var textString: String?
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
if textString != nil {
if !contentText.isEmpty {
return true
}
}
return true
}
override func viewDidLoad() {
super.viewDidLoad()
let extensionItem = extensionContext?.inputItems[0] as! NSExtensionItem
let contentTypeText = kUTTypeText as String
for attachment in extensionItem.attachments! {
if attachment.isText {
attachment.loadItem(forTypeIdentifier: contentTypeText, options: nil, completionHandler: { (results, error) in
let text = results as! String
self.textString = text
_ = self.isContentValid()
})
}
}
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
print(String(textString!)) // <-- I cannot read txt file and print it
// 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 []
}
}
//MARK: NSItemProvider check
extension NSItemProvider {
var isText: Bool {
return hasItemConformingToTypeIdentifier(kUTTypeText as String)
}
}
Is there any method to share the txt file as an attachment in Share Extension?
Thanks for any help.
First of all, in info.plist of your share extension you have to set what type of attachments and how many your extension can get
Example:
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
All keys you can find here
When your extension recives attachment first you need to check it for type(is it what you are expecting to recive).
And then, in didSelectPost() you can get that attachment and do something with it (maybe store it in your app if that's what you want).
Here is nice article about shere extension for photos

Firebase Twitter oAuth callback not working for Swift ios13

I have followed the instructions on https://firebase.google.com/docs/auth/ios/twitter-login to a T for Swift and I get the web popup to authorise the App I created on Twitter Dev, the callback is called and then the webview sits on an empty page of about:blank. Nothing can be done but click the Done button which then results in a Error Domain=FIRAuthErrorDomain Code=17058 "The interaction was cancelled by the user."
Callback address is correct. I've used the Twitter Consumer API Keys as the keys to enter in the Firebase console.
What am I missing?
For everyone who still has an issue with getting callback working, I managed to fix it. Sadly, you have to edit the method in the library (not the best way, but still. Bug was reported to firebase team). The method should look like the one below (you can find it in the file named FIROAuthProvider.m, line 125. I intentionally left commented lines, so you see the problem there... Hope it helps somebody :)
- (void)getCredentialWithUIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
completion:(nullable FIRAuthCredentialCallback)completion {
if (![FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:self->_callbackScheme]) {
[NSException raise:NSInternalInconsistencyException
format:#"Please register custom URL scheme '%#' in the app's Info.plist file.",
self->_callbackScheme];
}
// __weak __typeof__(self) weakSelf = self;
// __weak FIRAuth *weakAuth = _auth;
// __weak NSString *weakProviderID = _providerID;
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
FIRAuthCredentialCallback callbackOnMainThread =
^(FIRAuthCredential *_Nullable credential, NSError *_Nullable error) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(credential, error);
});
}
};
NSString *eventID = [FIRAuthWebUtils randomStringWithLength:10];
NSString *sessionID = [FIRAuthWebUtils randomStringWithLength:10];
// __strong __typeof__(self) strongSelf = weakSelf;
[self
getHeadFulLiteURLWithEventID:eventID
sessionID:sessionID
completion:^(NSURL *_Nullable headfulLiteURL, NSError *_Nullable error) {
if (error) {
callbackOnMainThread(nil, error);
return;
}
FIRAuthURLCallbackMatcher callbackMatcher =
^BOOL(NSURL *_Nullable callbackURL) {
return [FIRAuthWebUtils
isExpectedCallbackURL:callbackURL
eventID:eventID
authType:kAuthTypeSignInWithRedirect
callbackScheme:self->_callbackScheme];
};
// __strong FIRAuth *strongAuth = weakAuth;
[_auth.authURLPresenter
presentURL:headfulLiteURL
UIDelegate:UIDelegate
callbackMatcher:callbackMatcher
completion:^(NSURL *_Nullable callbackURL,
NSError *_Nullable error) {
if (error) {
callbackOnMainThread(nil, error);
return;
}
NSString *OAuthResponseURLString =
[self OAuthResponseForURL:callbackURL
error:&error];
if (error) {
callbackOnMainThread(nil, error);
return;
}
__strong NSString *strongProviderID = _providerID;
FIROAuthCredential *credential = [[FIROAuthCredential alloc]
initWithProviderID:strongProviderID
sessionID:sessionID
OAuthResponseURLString:OAuthResponseURLString];
callbackOnMainThread(credential, nil);
}];
}];
});
}
For me there was no need in framework modifications.
As stated in a issue on GitHub: provider property should be
declared at a class or global level
So I just moved its initialisation out of a function.
Before:
class AuthViewController: UIViewController {
private func signIn() {
let provider = OAuthProvider(providerID: "twitter.com")
provider.getCredentialWith(nil) {
...
}
}
}
After:
class AuthViewController: UIViewController {
private let provider = OAuthProvider(providerID: "twitter.com")
private func signIn() {
provider.getCredentialWith(nil) {
...
}
}
}
Ok so I resolved this question but not entirely sure how now. I believe it was because the callback wasn't being issued and thus not picked up by the app. The callback wasn't issue due to the authentication and I believe because I hadn't created the terms of service and privacy policy. So make sure you have done that in the Twitter dev page.
In the twitter dev page, the callback link is: https://yourApp.firebaseapp.com/__/auth/handler
You'll find this in your firebase authentication settings when you enable twitter.
Ensure you have in info.plist LSApplicationQueriesSchemes an array of:
twitter
twitterauth
In your swift file:
var provider = OAuthProvider(providerID: "twitter.com")
Your button action:
#IBAction func onCustonTwitterButtonPressed(_ sender: Any) {
MyAppsCoreServicesScripts.logoutSocial() // My logout routine.
provider.getCredentialWith(nil) { credential, error in
if let error = error {
MyAppsCoreServicesScripts.showError(prefix: "Twitter Login",error: error, showMsg: true)
} else if credential != nil {
self.firebaseLogin(credential!)
}
}
}
Good luck! Hope this helps someone.
Got stucked in about:blank page for days, tried all the above but not working, solved by add following code in AppDelegate
func application(_ app: UIApplication, open url: URL, options: func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let twitterhandeled = Auth.auth().canHandle(url)
return twitterhandeled
}

How to share a Note including Markup with Share Extensions Swift

I've already figured out how to share the general content of a Note (text and pictures). But the way I currently approach the problem the markup of the note is not kept (Titles, Lists, etc.). I just receive the pure text of the note. When you share a note with Mail for instance, you can see that the markup is transferred. Is there a way to do that for your own apps?
My current solution where I only receive the pure text:
class ShareViewController: UIViewController{
override func viewDidLoad() {
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
// Verify the provider is valid
if let contents = content.attachments as? [NSItemProvider] {
// look for images
for attachment in contents {
print(attachment.registeredTypeIdentifiers)
if attachment.hasItemConformingToTypeIdentifier("public.plain-text"){
attachment.loadItem(forTypeIdentifier: "public.plain-text", options: nil) { data, error in
let string = data as! String
print(string)
}
}
}
}
}
}
}
EDIT:
My current NSExtensionActivationRules:
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationDictionaryVersion</key>
<integer>2</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>100</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
</dict>
</dict>
Use the attributedContentText property of your NSExtensionItem (content):
override func viewDidLoad() {
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
// move your content validation to `func isContentValid() -> Bool`
let attributedString = content.attributedContentText // yay NSAttributedString!
}
}

How can I open the parent app on iPhone from my WatchKit app?

I'm trying to open the parent application of my Apple Watch app.
In Xcode Beta 2 we could use this code:
WKInterFaceController.openParentApplication
However, in Xcode beta 3 I couldn't found that code any longer. Now I don't know how to open the parent application from the watch app. Please help.
The Objective-C method is:
+ (BOOL)openParentApplication:(NSDictionary *)userInfo
reply:(void (^)(NSDictionary *replyInfo,
NSError *error))reply
The Swift method is:
class func openParentApplication(_ userInfo: [NSObject : AnyObject]!,
reply reply: (([NSObject : AnyObject]!,
NSError!) -> Void)!) -> Bool
So you need to pass the iPhone application a reply() block in order to have activate it from your WatchKit extension. Here's one way it could be implemented, for instance:
NSString *requestString = [NSString stringWithFormat:#"executeMethodA"]; // This string is arbitrary, just must match here and at the iPhone side of the implementation.
NSDictionary *applicationData = [[NSDictionary alloc] initWithObjects:#[requestString] forKeys:#[#"theRequestString"]];
[WKInterfaceController openParentApplication:applicationData reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"\nReply info: %#\nError: %#",replyInfo, error);
}];
Your iPhone application's AppDelegate needs to implement the following method:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
NSString * request = [userInfo objectForKey:#"requestString"];
if ([request isEqualToString:#"executeMethodA"]) {
// Do whatever you want to do when sent the message. For instance...
[self executeMethodABC];
}
// This is just an example of what you could return. The one requirement is
// you do have to execute the reply block, even if it is just to 'reply(nil)'.
// All of the objects in the dictionary [must be serializable to a property list file][3].
// If necessary, you can covert other objects to NSData blobs first.
NSArray * objects = [[NSArray alloc] initWithObjects:myObjectA, myObjectB, myObjectC, nil];
NSArray * keys = [[NSArray alloc] initWithObjects:#"objectAName", #"objectBName", #"objectCName", nil];
NSDictionary * replyContent = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
reply(replyContent);
}
The WKInterfaceController method openParentApplication:reply: launches the containing app in the background when the iPhone (or iOS Simulator) is unlocked or locked. Note that statements from Apple indicate that the WatchKit extension was always intended to launch your iPhone application in the background, and it was only an implementation detail of the simulator that it appeared to launch your iPhone application in the foreground in previous betas.
If you want to test your WatchKit app and your iPhone app running at the same time, simply launch the WatchKit app from Xcode under the Schemes menu, and then manually launch your iPhone app in the simulator by clicking on its springboard icon.
If you need to open your parent app in the foreground, use Handoff!
Example:
Somewhere shared for both:
static let sharedUserActivityType = "com.yourcompany.yourapp.youraction"
static let sharedIdentifierKey = "identifier"
on your Watch:
updateUserActivity(sharedUserActivityType, userInfo: [sharedIdentifierKey : 123456], webpageURL: nil)
on your iPhone in App Delegate:
func application(application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
if (userActivityType == sharedUserActivityType) {
return true
}
return false
}
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]!) -> Void) -> Bool {
if (userActivity.activityType == sharedUserActivityType) {
if let userInfo = userActivity.userInfo as? [String : AnyObject] {
if let identifier = userInfo[sharedIdentifierKey] as? Int {
//Do something
let alert = UIAlertView(title: "Handoff", message: "Handoff has been triggered for identifier \(identifier)" , delegate: nil, cancelButtonTitle: "Thanks for the info!")
alert.show()
return true
}
}
}
return false
}
And finally (this step is important!!!): In your Info.plist(s)