34

I'm trying to implement “Save image to Library” function and then return back to the current view controller, but on a new iOS 13 it dismisses back to the view controller that presented the current one:

PHPhotoLibrary.requestAuthorization({(_ status: PHAuthorizationStatus) -> Void in })

let shareItems: Array = [newImg,"Hello"] as [Any]

let activityController = UIActivityViewController(activityItems: shareItems, applicationActivities: nil)

if UIDevice.current.userInterfaceIdiom == .pad {
    activityController.popoverPresentationController?.sourceView = saveButton
}

present(activityController, animated: true)
rmaddy
  • 314,917
  • 42
  • 532
  • 579
Diana
  • 683
  • 1
  • 8
  • 17

9 Answers9

12

Swift version of @KDP's solution:

let fakeViewController = TransparentViewController()
fakeViewController.modalPresentationStyle = .overFullScreen

activityViewController.completionWithItemsHandler = { [weak fakeViewController] _, _, _, _ in
    if let presentingViewController = fakeViewController?.presentingViewController {
        presentingViewController.dismiss(animated: false, completion: nil)
    } else {
        fakeViewController?.dismiss(animated: false, completion: nil)
    }
}
present(fakeViewController, animated: true) { [weak fakeViewController] in
    fakeViewController?.present(activityViewController, animated: true, completion: nil)
}

fakeViewController gets either dismissed by activity completion, or we need to dismiss it in completion.

janh
  • 232
  • 2
  • 9
7

I can confirm that this bug is still present in iOS 13.3.1. The following workaround is a Swift version of franze's solution. I prefer this approach, as it doesn't make any further assumptions on the view controller hierarchy and doesn't use method swizzling.

Using this additional UIWindow breaks the Cancel button of the UIActivityViewController on iOS 12 and earlier, so I added a check for the OS version.

private let activityWindow: UIWindow = {
  let window = UIWindow(frame: UIScreen.main.bounds)
  window.rootViewController = UIViewController()
  return window
}()

func showActivityController() {
  let activityViewController = UIActivityViewController(/* ... */)
  activityViewController.completionWithItemsHandler = {
     // ...
     UIApplication.shared.delegate?.window??.makeKeyAndVisible()       
  }

  // Use this workaround only on iOS 13
  if ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 13 {
    activityWindow.makeKeyAndVisible()
    activityWindow.rootViewController?.present(activityViewController, animated: true)
  } else {
    present(activityViewController, animated: true)
  }
}

Update: Apparently, this solution doesn't work reliably on iPads. It looks like on iPad the UIActivityViewController is presented differently and as soon as it's visible on screen, no touch events are registered, effectively freezing the app.

Theo
  • 3,826
  • 30
  • 59
  • Thank you! Finally solved for my case. I've changed only this line `activityViewController.completionWithItemsHandler = { (activity: UIActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in )}` – Diana Apr 04 '20 at 22:07
  • 1
    Upon further testing, I noticed that my original solution doesn't work correctly on iOS 12 and earlier - please see my updated answer. – Theo Apr 05 '20 at 08:54
  • @Theo Im assuming that this is implemented into the .m file of the plugin but i am struggling to find where, i have tried bypassing the getTopMostViewController function but it just breaks it could you show where you implemented this to overwrite the standard view. thank you in advance – troggy69 May 07 '20 at 13:40
  • @troggy69 Implement the method in the view controller that presents the `UIActivityViewController`. Then, instead of calling `present(activityViewController, animated: true)`, call `showActivityViewController()` instead. – Theo May 07 '20 at 15:19
  • Please note my warning that I just added - this solution doesn't work on iPad. If someone knows how to fix it, I'd appreciate edits/comments. – Theo May 07 '20 at 15:32
  • @Theo not sure if this will help for you but the issue i have found with ipad is that it shows behind the view so on open you need to add (depending on your variables) `ref.show();` on mine i had to add `ref.hide()` first. i doubt this will be what you need but might shed some insight – troggy69 May 07 '20 at 15:55
  • Also in regards to my implementation question i apologise as i worded it very badly, my app is a cordova app and i am trying to implement your fix into `cordova-plugin-file-opener2` into the .m file in the ios folder i believe the code style is objective c from what i can tell as i havent been coding apps for very long – troggy69 May 07 '20 at 15:59
  • it doesn't work - the app is frozen after dismissing activity controller – Vyachaslav Gerchicov Jun 23 '20 at 13:03
  • Update: For iOS 14 - `majorVersion == 14` – Diana Jun 30 '20 at 20:49
  • This solution cause some issues in my case. It acts weird after dismissing and presenting for further times. – Mehdi Gilanpour Jul 07 '20 at 07:16
  • I can confirm that this bug is still present in iOS 14 beta 3. I have filed a bug report FB8111145. – Theo Jul 23 '20 at 08:28
  • @troggy69 can you please help out with your code, I just wanted to know which view you hide in iPad? – Berlin Feb 10 '23 at 11:34
4

I generated following monkey patch (Checked by iOS 13.1.2)

- (void)export {

  //
  // ... Generate Your Activity Items ...
  //

  UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems
                                                                                           applicationActivities:nil
                                                                                                   completeBlock:^(NSError *activityError, BOOL completed) {
                                                                                                       // Swizzling Dismiss Method
                                                                                                       [[self class]   switchInstanceMethodFrom:@selector(dismissViewControllerAnimated:completion:) To:@selector(lockedDismissViewControllerAnimated:completion:)];
                                                                                                   }
                                                                                ];
  // Swizzling Dismiss Method
  [[self class] switchInstanceMethodFrom:@selector(dismissViewControllerAnimated:completion:) To:@selector(lockedDismissViewControllerAnimated:completion:)];
  [self presentViewController:activityViewController animated:YES completion:nil];
}

- (void)lockedDismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    if ([self presentedViewController]) {
        [self lockedDismissViewControllerAnimated:flag completion:completion];
    }
}

// from http://qiita.com/paming/items/25eaf89e4f448ab05752
+(void)switchInstanceMethodFrom:(SEL)from To:(SEL)to
{
    Method fromMethod = class_getInstanceMethod(self,from);
    Method toMethod   = class_getInstanceMethod(self,to  );
    method_exchangeImplementations(fromMethod, toMethod);
}
ANNotunzdY
  • 379
  • 1
  • 3
  • 14
3

Here is the way I worked around this bug for now. I create a fake view controller and push it onto the current stack. It seems that UIActivityTypeSaveToCameraRoll dismisses the top view controller on the stack, whereas the other options do not. I take care of dismissing the fake view controller in the completion block if the activity type is not complete and UIActivityTypeSaveToCameraRoll.

typeof(self) __weak weakSelf = self;

[self.activityViewController setCompletionHandler:^(NSString *activityType, BOOL completed) {
        if (activityType== UIActivityTypeSaveToCameraRoll && completed){
              weakSelf.activityViewController = nil;
        }
        else{
             [weakSelf dismissViewControllerAnimated:NO completion:nil];
             weakSelf.activityViewController = nil;       
        }
}];


UIViewController *fakeVC=[[UIViewController alloc] init];

[self presentViewController:fakeVC animated:NO completion:^{
       [fakeVC presentViewController:self.activityViewController animated:YES completion:nil];
}];
KDP
  • 61
  • 3
3
- (UIWindow *)displayWindow
{
    if (!_displayWindow)
    {
        _displayWindow = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
        _displayWindow.rootViewController = [[UIViewController alloc] init];
    }
    return _displayWindow;
}

- (void)showActivityController
{
    UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[] applicationActivities:nil];
    activityViewController.completionWithItemsHandler = ^(UIActivityType __nullable activityType, BOOL completed, NSArray * __nullable returnedItems, NSError * __nullable activityError)
    {
        [UIApplication.sharedApplication.delegate.window makeKeyAndVisible];
    };

    [self.displayWindow makeKeyAndVisible];
    [self.displayWindow.rootViewController presentViewController:activityViewController animated:true completion:nil];
}

Make sure the _displayWindow is strong reference.

franze
  • 31
  • 2
2

I resolved this issue by setting the root view controller to the current window, I have no idea why it dismisses the current view controller. I noticed that while presenting the new view controller in iOS 13 it will present to the card stack up style and if I chose "Save Image" in UIActivityController then the current view controller (card) will be dismissed and show up the previous view controller.

guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
      let window = appDelegate.window else { return }
window.rootViewController = viewcontroller

I use this one instead of presenting view controller when I need to start a new story.

BTW, it depends on your requirements. In this case, I can dismiss the old unused one because no need to back, maybe you can use this instead of presenting a new scene.

Silverize
  • 21
  • 3
0

Seems it's fixed in iOS 14. For older versions I found easier workaround via overriding dismiss(animated:) method with empty implementation. See https://stackoverflow.com/a/66595125/2095408

Palli
  • 510
  • 6
  • 14
0

I experienced the same problem, therefore I altered the way the viewcontroller was displayed.

Calling the share button's viewcontroller presents a problem for me.

I was calling share button's viewcontoller with enter image description here self.present(.......) then I change it

self.view.window.rootviewcontroller = share button's viewcontoller

and fixed.

  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Vikram Parimi Sep 16 '22 at 11:59
-1

For those who gets the screen freezes on iPads, here a simple solution. It works on iPhone too.

func shareItems(_ sharedItems: [Any]) {

        let activityViewController = UIActivityViewController(activityItems: sharedItems, applicationActivities: nil)
        if let popoverController = activityViewController.popoverPresentationController {
            popoverController.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 2, width: 0, height: 0)
            popoverController.sourceView = self.view
            popoverController.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
        }

        self.present(activityViewController, animated: true, completion: nil)
}
Ofcourse
  • 617
  • 1
  • 7
  • 19