How to cancel UIActivityItemProvider and don't show activity? - nsoperation

I'm using UIActivityItemProvider subclass to provide custom data. But sometimes getting data fails and I don't want to present activity (e.g. message composer). Tried [self cancel] and return nil; in item method, but message composer still shows (with empty message).

If you dismiss the UIActivityViewController before returning from -(id)item it will not present the users chosen activity.
To do this you first need to grab the activityViewController in activityViewControllerPlaceholderItem. In -(id)item run code in a dispatch_async to update progress and dismiss on complete / error which I'm doing using a promise lib.
In your subclass of UIActivityItemProvider do something similar to the example below.
-(id) activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController
{ self.avc = activityViewController;
return NSURL;
}
-(id)item
{ __block BOOL fileProcDone = NO;
dispatch_async(dispatch_get_main_queue(), ^
{ self.pvc = [[ProgressAlertVC alloc] init];
[self.vc presentViewController:self.pvc animated:YES completion:nil];
[[[[self promiseMakeFile]
progressed:^(float progress)
{ self.pvc.progress = progress;
}]
fulfilled:^(id result)
{ [self.pvc dismissViewControllerAnimated:YES completion:^
{ fileProcDone = YES;
}];
}]
failed:^(NSError *error)
{ [self.pvc dismissViewControllerAnimated:YES completion:^
{ [self.vc dismissViewControllerAnimated:YES completion:^
{ fileProcDone = YES;
}];
}];
}];
});
while (!fileProcDone)
{ [NSThread sleepForTimeInterval:0.1];
};
return NSURL;
}
This will result in a console log message from activity extensions but as long as they deal correctly with errors things should be fine. If you return nil from -(id)activityViewController: itemForActivityType: you don't get console errors but will get the users chosen activity even if you dismiss the UIActivityViewController at this point.

You simply need to call the cancel method of UIActivityItemProvider. Since UIActivityItemProvider is an NSOperation, calling cancel will mark the operation cancelled.
At that point, you have a few options to actually stop the long running task, depending on the structure of your task. You could override the cancel method and do your cancellation there, just be sure to call [super cancel] as well. The second option is the check the value of isCancelled within the item method.
An example item provider
import UIKit
import Dispatch
class ItemProvider: UIActivityItemProvider {
override public var item: Any {
let semaphore = DispatchSemaphore(value: 0)
let message = "This will stop the entire share flow until you press OK. It represents a long running task."
let alert = UIAlertController.init(title: "Hello", message: message, preferredStyle: .alert)
let action = UIAlertAction.init(title: "OK", style: .default, handler:
{ action in
semaphore.signal()
})
let cancel = UIAlertAction.init(title: "CANCEL", style: .destructive, handler:
{ [weak self] action in
self?.cancel()
semaphore.signal()
})
alert.addAction(action)
alert.addAction(cancel)
//Truly, some hacking to for the purpose of demonstrating the solution
DispatchQueue.main.async {
UIApplication.shared.delegate?.window??.rootViewController?.presentedViewController!.present(alert, animated: true, completion: nil)
}
// We can block here, because our long running task is in another queue
semaphore.wait()
// Once the item is properly cancelled, it doesn't really matter what you return
return NSURL.init(string: "blah") as Any
}
}
In the view controller, start a share activity like this.
let provider = ItemProvider.init(placeholderItem: "SomeString")
let vc = UIActivityViewController.init(activityItems: [provider], applicationActivities: nil)
self.present(vc, animated: true, completion: nil)

Related

iOS Share Extension flow

I have problems with creating share extension like share extension of the Pinterest app. When user is not logged to the containing app the share extension only presents alert with an option to log in and cancel.
Where in code decide which view controller to show in my shared extension. I see this like I need to check authorization status from shared container and if this status is not logged I need to present alert controller. If status is logged I need to show my main view controller ShareViewController which is a subclass of SLComposeServiceViewController
My question is not UI related but where to put this check code. I didn't find any method where app extension starts so I can select some initial view controller for extension based on some state.
In Pinterest extension I don't see their main view controller when user is logged out from their containing app. I see only alert with options.
Second question: How to programatically switch from share extension to containing app. How this Pinterest share extension is doing this when user need to authenticate?
I'm working on latest iOS SDK 10.2
Since I didn't receive any feedback for this and I figure it out how to do this. Here is the answer.
To control which view load I used loadView method from UIViewController lifecycle. This method is fired when application first need to get view property from UIViewController. So this is lazy loading. This method is also fired when user invoke loadViewIfNeeded() from UIViewController api. In the body of this method You need to be very careful to not read view property because this will invoke loadView again and You will have recursive loop.
My implementation for this method is following. I need to tell if user is logged in or not in containing app and based on this choose which view to load.
override func loadView() {
// check in shared Keychain if user is authenticated
self.userAuthenticated = userService.isAuthenticated()
if self.userAuthenticated {
// load default view of the ShareViewController
super.loadView()
} else {
// if user is not logged in show only alert view controller with transparent dummy view
let view = UIView()
self.view = view
}
}
And if user is not logged in I show alert in
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let context = self.extensionContext!
if !self.userAuthenticated {
let alert = UIAlertController(title: "Error", message: "User not logged in", preferredStyle: .alert)
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in
context.completeRequest(returningItems: nil, completionHandler: nil)
}
let login = UIAlertAction(title: "Log In", style: .default, handler: { _ in
let url = URL(string: "fashionapp://login")!
// This is utility method written in Objective-C.
// I don't know yet if it passes Apple Review process or not.
// We will see ;)
self.open(url, options: [:], completionHandler: nil)
context.completeRequest(returningItems: nil, completionHandler: nil)
})
alert.addAction(cancel)
alert.addAction(login)
present(alert, animated: true, completion: nil)
}
}
And here is the method for opening containing app from share extension. I hope it will be useful and Apple will review this without any problems. It is written in Objective-C because in Swift there is no NSInvocation class so you can only perform selectors with max two arguments.
#import <UIKit/UIKit.h>
#interface UIViewController (OpenURL)
- (void)openURL:(nonnull NSURL *)url
options:(nonnull NSDictionary<NSString *, id> *)options
completionHandler:(void (^ __nullable)(BOOL success))completion;
#end
And implementation.
#import "UIViewController+OpenURL.h"
#implementation UIViewController (OpenURL)
- (void)openURL:(nonnull NSURL *)url
options:(nonnull NSDictionary<NSString *, id> *)options
completionHandler:(void (^ __nullable)(BOOL success))completion {
SEL selector = NSSelectorFromString(#"openURL:options:completionHandler:");
UIResponder* responder = self;
while ((responder = [responder nextResponder]) != nil) {
if([responder respondsToSelector:selector] == true) {
NSMethodSignature *methodSignature = [responder methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget: responder];
[invocation setSelector: selector];
[invocation setArgument: &url atIndex: 2];
[invocation setArgument: &options atIndex:3];
[invocation setArgument: &completion atIndex: 4];
[invocation invoke];
break;
}
}
}
#end

What can cause UIAlertController actions to never be called?

In crash reports I'm seeing NSInternalInconsistencyException get thrown from the WKWebView delegate methods for prompting JavaScript alerts, ie. "Completion handler passed to -[MyClass webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called".
Every UIAlertAction calls the WKWebView completion handler from its handler. The only explanation is that the alert is being canceled without invoking any action. UIAlertView had delegate methods for cases like this, but UIAlertController does not offer that level of control.
Has anyone devised a solution for this?
I've considered using the same technique Apple uses in CompletionHandlerCallChecker (in WebKit) to capture my own callback's failure to be invoked, and invoke the WKWebView's handler to prevent the spurious exception. Seems awfully kludgy, and I'm not yet sure it would work. I'd rather prevent this from happening in the first place.
Edit: I know that programmatically dismissing the alert controller produces this behavior, which is unfortunate, but I do not know the condition where iOS decides to dismiss the controller without user interaction.
Edit 2: For those requesting the code, it is really the minimum implementation:
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler();
}]];
[self presentViewController:alertController animated:YES completion:nil];
}
Here's my kludgy but working solution:
class CompletionHandlerCallCheckerDefeater: NSObject {
private var calledCompletionHandler: Bool = false
private var fallbackHandler: () -> Void
init(fallbackHandler: () -> Void) {
self.fallbackHandler = fallbackHandler
}
deinit {
if (!calledCompletionHandler) {
fallbackHandler()
}
}
func didCallCompletionHandler() {
calledCompletionHandler = true
}
}
In your delegate callback, use it like this:
let defeater = CompletionHandlerCallCheckerDefeater(fallbackHandler: completionHandler)
let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
alertController.addAction(UIAlertAction(title:"OK", style: .Default) { (action) in
completionHandler()
defeater.didCallCompletionHandler()
})
presentViewController(alertController, animated: true, completion: nil)
Assign whatever is necessary to the fallbackHandler, it doesn't have to be the block passed in, it just needs to invoke the completionHandler with appropriate parameters. The "defeater" is destroyed when all references are released, that is, when the alert is destroyed, and it will call the fallbackHandler (which calls completionHandler) if didCallCompletionHandler was never called, thus avoiding the exception.

How to present AlertController 2 seconds after click a button

I want to present an alert view after the user click on a button at 2s. The function is to start calibration, and I want to show the user that calibration is done after 2s.
I'm counting in an array that when it reaches 20 (presents 2s), and I used
if showCalibDone {
let calibDoneAlert = UIAlertController(title: "", message: "Calibration is finished", preferredStyle: .Alert)
calibDoneAlert.addAction(UIAlertAction(title: "", style: .Default, handler: {(action: UIAlertAction!) in
self.x0 = self.average40(self.xCalibrate)
self.presentViewController(calibDoneAlert, animated: true, completion: nil)
}))
This is the counting. xCalibrate is an array which will add an object each time the accelerometer updates.
if xCalibrate.count == 40 {
println("40 achieved")
self.showCalibDone = true
}
But the alert view never appears with this if condition. I tried to put it in viewWillAppear or viewDidLoad, but it seems both are not correct.
How can I get this 'loop' be executed? Where should I put it? Or maybe I should use a timer to count?
If you just want some code to be executed after some a delay, use dispatch_after:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue()) {
// ...
}
You can also use performSelector to achieve the same results
- (void) showAlert
{
[[[UIAlertView alloc] initWithTitle:#"Title" message:#"Some message" delegate:nil cancelButtonTitle:nil otherButtonTitles:#"OK",nil] show];
}
//
[self performSelector:#selector(showAlert) withObject:nil afterDelay:2.0f];

swift spritekit Facebook share button

Hey there my app is almost ready for release but i want to add a Facebook share button. The thing is i have no idea how the communication between the scene and the viewcontroler works. i did my research but only found code in obj-c like this one
- (void)lkFaceBookShare {
NSString *serviceType = SLServiceTypeFacebook;
if (![SLComposeViewController isAvailableForServiceType:serviceType])
{
[self showUnavailableAlertForServiceType:serviceType];
}
else
{
SLComposeViewController *composeViewController = [SLComposeViewController composeViewControllerForServiceType:serviceType];
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
CGRect rect = [keyWindow bounds];
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0.5f);
[self.view drawViewHierarchyInRect:rect afterScreenUpdates:YES];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[composeViewController addImage:viewImage];
NSString *initalTextString = [NSString stringWithFormat:#"Let's join together in the form of underground catch word go along with me!! Link: https://itunes.apple.com/us/app/uoi-hinh-bat-chu-gioi-duoi/id907330926?ls=1&mt=8"];
[composeViewController setInitialText:initalTextString];
UIViewController *vc = self.view.window.rootViewController;
[vc presentViewController:composeViewController animated:YES completion:nil];
}
}
- (void)showUnavailableAlertForServiceType:(NSString *)serviceType
{
NSString *serviceName = #"";
if (serviceType == SLServiceTypeFacebook)
{
serviceName = #"Facebook";
}
else if (serviceType == SLServiceTypeSinaWeibo)
{
serviceName = #"Sina Weibo";
}
else if (serviceType == SLServiceTypeTwitter)
{
serviceName = #"Twitter";
}
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:#"Account"
message:[NSString stringWithFormat:#"Please go to the device settings and add a %# account in order to share through that service", serviceName]
delegate:nil
cancelButtonTitle:#"Dismiss"
otherButtonTitles:nil];
[alertView show];
}
my experience and knowledge is too low to port this too swift so i need some help with this D:
Thanks
This is some code I did for twitter a while ago which still works in swift. I show you how to convert it to Facebook below. Put this your viewController:
func showTweetSheet() {
let tweetSheet = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
tweetSheet.completionHandler = {
result in
switch result {
case SLComposeViewControllerResult.Cancelled:
//Add code to deal with it being cancelled
break
case SLComposeViewControllerResult.Done:
//Add code here to deal with it being completed
//Remember that dimissing the view is done for you, and sending the tweet to social media is automatic too. You could use this to give in game rewards?
break
}
}
tweetSheet.setInitialText("Test Twitter") //The default text in the tweet
tweetSheet.addImage(UIImage(named: "TestImage.png")) //Add an image if you like?
tweetSheet.addURL(NSURL(string: "http://twitter.com")) //A url which takes you into safari if tapped on
self.presentViewController(tweetSheet, animated: false, completion: {
//Optional completion statement
})
}
To convert it to Facebook, simply swap SLServiceTypeTwitter to SLServiceTypeFacebook and rename the variables for readability. If you want to call this method from the SKScene, you have to somehow alert the viewController that you want it to call a method.
My preferred way is to use NSNotificationCenter so that I post an alert from the scene and it is received by the viewController so that it fires a method. This is also incredibly easy to setup. In the scene, you need to put this line of code wherever you want to call the Facebook popup:
NSNotificationCenter.defaultCenter().postNotificationName("WhateverYouWantToCallTheNotification", object: nil)
This sends out a notification with a name. Now, in the viewController you need to subscribe to this alert by putting the following code in either viewDidLoad, viewDidAppear or something similar.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "ThisIsTheMethodName", name: "WhateverYouCalledTheAlertInTheOtherLineOfCode", object: nil)
Now the scene will communicate with the viewController and you will be able to show the Facebook sheet. Remember to replace my strings with ones relative to your project. Hope this helps - sorry it was such a long answer!
func lkFaceBookShare() {
var serviceType: String = SLServiceTypeFacebook
if !SLComposeViewController.isAvailableForServiceType(serviceType) {
self.showUnavailableAlertForServiceType(serviceType)
}
else {
var composeViewController: SLComposeViewController = SLComposeViewController.composeViewControllerForServiceType(serviceType)
var keyWindow: UIWindow = UIApplication.sharedApplication().keyWindow
var rect: CGRect = keyWindow.bounds
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, false, 0.5)
self.view!.drawViewHierarchyInRect(rect, afterScreenUpdates: true)
var viewImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
composeViewController.addImage(viewImage)
var initalTextString: String = String(format: "Let's join together in the form of underground catch word go along with me!! Link: https://itunes.apple.com/us/app/uoi-hinh-bat-chu-gioi-duoi/id907330926?ls=1&mt=8")
composeViewController.initialText = initalTextString
var vc: UIViewController = self.view.window.rootViewController
vc.presentViewController(composeViewController, animated: true, completion: { _ in })
}
}
func showUnavailableAlertForServiceType(serviceType: String) {
var serviceName: String = ""
if serviceType == SLServiceTypeFacebook {
serviceName = "Facebook"
}
else if serviceType == SLServiceTypeSinaWeibo {
serviceName = "Sina Weibo"
}
else if serviceType == SLServiceTypeTwitter {
serviceName = "Twitter"
}
var alertView: UIAlertView = UIAlertView(title: "Account", message: "Please go to the device settings and add a \(serviceName) account in order to share through that service", delegate: nil, cancelButtonTitle: "Dismiss", otherButtonTitles: "")
alertView.show()
}
Swift Conversion of Obj-C answer posted by a very helpful user...... original post

UIActivityViewController freezes when email or text but not fb or Twitter

I have a very standard implementation of UIActivityViewController. When I use Twitter or Facebook, the view controller is dismissed, and the app continues working. However, when I email or text the same content, the view controller is dismissed but the app freezes (not crashes). Everything is still on screen but frozen - no input etc.
Perhaps the Mail or Message apps have not released control back to my app? Is there a way using Instruments to analyze what's going on?
Thanks!
I am getting a leak from this part from NSArray as the offenders
- (void)postToFacebook:(UITapGestureRecognizer *)sender
{
NSString *postText = #"Testing";
UIImage *imageToPost = [self captureTheScreenImage];
NSArray *postItems = #[postText, imageToPost];
UIActivityViewController *activityPostVC = [[UIActivityViewController alloc]initWithActivityItems:postItems applicationActivities:nil];
NSArray *excludedItems = #[UIActivityTypePostToWeibo,UIActivityTypePrint,UIActivityTypeCopyToPasteboard,UIActivityTypeAssignToContact,UIActivityTypeSaveToCameraRoll, UIActivityTypeMail, UIActivityTypeMessage];
[activityPostVC setExcludedActivityTypes:excludedItems];
[self presentViewController:activityPostVC animated:YES completion:nil];
}
This issue happened to me too when using multiple UIWindow objects at the same time.
Upon dismissal of the UIActivityViewController, the presenting windows contents are not restored correctly. Specifically, the window's first subview (UILayoutContainerView) is missing its constraints to the superview. This causes the window the width of the UILayoutContainerView to be zero causing the window appear transparent and reveal the window underneath it but not allowing user interaction.
The fix can be to place an empty transparent window on top of the current window and present the UIActivityViewController from an empty view controller associated with that new window. When the UIActivityViewController is dismissed, we can dispose of the empty window that it was presented from.
import Foundation
private var previousWindow: UIWindow?
private var activityViewControllerWindow: UIWindow?
extension UIViewController {
fileprivate var isActivityViewControllerWindowPresented: Bool {
return activityViewControllerWindow?.isKeyWindow ?? false
}
func presentActivityViewController(_ activityViewController: UIActivityViewController, animated: Bool = true, completion: (() -> Void)? = nil) {
if isActivityViewControllerWindowPresented {
return
}
let window = UIWindow(frame: view.window!.frame)
previousWindow = UIApplication.shared.keyWindow
activityViewControllerWindow = window
window.rootViewController = UIViewController()
window.makeKeyAndVisible()
let activityCompletionClosure = activityViewController.completionWithItemsHandler
activityViewController.completionWithItemsHandler = { [weak self] (activityType, completed, returnedItems, activityError) in
self?.cleanUpActivityViewControllerWindow()
activityCompletionClosure?(activityType, completed, returnedItems, activityError)
}
window.rootViewController?.present(activityViewController, animated: animated, completion: completion)
}
fileprivate func cleanUpActivityViewControllerWindow() {
previousWindow?.makeKeyAndVisible()
activityViewControllerWindow?.rootViewController = nil
activityViewControllerWindow = nil
}
}
Yes, there is a way, like you mentioned, using Instruments. But If I were to foreshadow your results, I'd say you might want to do network calls on a non-UI thread, somewhere in the background so that your UI thread can do its thang while your app talks to Twitter or Facebook.

Resources