Controls not dimming when view controller is presented locally and alert displayed - ios

When I present a UIAlertController over a view controller, the controls are dimmed as expected.
However, if the same view controller is itself modally presented, the display of the alert controller does not dim the controls (the two buttons remain blue).
How do I make a presented view controller itself handle presentations correctly, and dim its controls?
Here is a small example project. The relevant code is in MainViewController.swift.

The best workaround I have so far is to use a customized UIAlertController subclass to set the tintAdjustmentMode alongside its appear/disappear animations, using the transitionCoordinator:
/// A `UIAlertController` that can udpates its presenting view controller's `tintAdjustmentMode` code as it appears and disappears
class TintAdjustingAlertController: UIAlertController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animatePresentingViewTintAdjustmentMode(tintAdjustmentMode: .dimmed, forViewControllerAtKey: .from)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
animatePresentingViewTintAdjustmentMode(tintAdjustmentMode: .automatic, forViewControllerAtKey: .to)
}
private func animatePresentingViewTintAdjustmentMode(tintAdjustmentMode mode: UIView.TintAdjustmentMode, forViewControllerAtKey key: UITransitionContextViewControllerKey) {
transitionCoordinator?.animate(alongsideTransition: { context in
if let presentingNavigationController = context.viewController(forKey: key) as? UINavigationController {
presentingNavigationController.navigationBar.tintAdjustmentMode = mode
presentingNavigationController.viewControllers.forEach { $0.view.tintAdjustmentMode = mode }
} else if let presentingViewController = context.viewController(forKey: key) {
presentingViewController.view.tintAdjustmentMode = mode
}
}, completion: nil)
}
}
This works, but I hope not to have to pepper it throughout my code. Would still love to know a) if there is a simple way to make this work as expected, or b) if this is indeed an iOS bug, is there a more elegant workaround?
I have also submitted a radar for this: http://www.openradar.me/radar?id=6113750608248832

Related

Correct way of presenting a ViewController B from ViewController A before A is visible

My iOS App starts with UIViewController A which is embedded as first element in a UINavigationController. When the app is started or when returning to it after some time in background I would like to show a password prompt. In this case UIViewController A should present UIViewController B which shows the password prompt.
The user should immediately see UIViewController B, not A and then B sliding in, etc. Thus, I have presented UIViewController B in viewWillAppear in UIViewController A:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: nil)
}
}
This works fine, but an error message is logged:
Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x7fe9af01c200>.
It is obvious that presenting UIViewController B from UIViewController A before it became visible causes this problem. Moving from viewWillAppear to viewDidAppear would solve the error message. However, than the user would first see A then B...
Is it even possible to overlay a ViewControler A with ViewController B without A becoming visible first?
I know that there might be other solutions like adding the view of the password ViewController manually to the view hierachy, etc. However, I would prefer a clean way where A is in complete control. Is this possible?
Or is it save to simple ignore the warning?
It sounds a bit tricky, might do the job though.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
self.view.alpha = 0
// Maybe (or not?)
self.navigationController?.view.backgroundColor = .white
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: { [weak self] in
self?.view.alpha = 1
})
}
}

Why does UIActivityViewController call viewWillDisappear() on the presenter?

I am presenting a UIActivityViewController to share an .mp4 video from a URL:
let viewController: UIViewController = ... // the presenting view controller
let url: URL = ... // local file
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
viewController.present(activityController, animated: false, completion: nil)
When the "save video" option is selected, the video is saved, but the presenting UIViewController disappears (and I can see that .viewWillDisappear() is called on it.)
How do I make the presenting UIViewController not disappear?
Note that all of the other share options that I've tried do not have this problem (messages, airdrop, instagram).
I have tried to set the sourceView and sourceRect, but it does not seem to help.
activityController.popoverPresentationController?.sourceView = viewController.view!
activityController.popoverPresentationController?.sourceRect = viewController.view!.frame
I've looked for errors, but didn't find any:
activityController.completionWithItemsHandler = { (a: UIActivity.ActivityType?, b: Bool, c: [Any]?, d: Error?) in
if let error = d {
print(error)
}
}
Also, all of my UIViewController lifecycle overrides call their super, i.e.:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
This is what it looks like:
It's bringing down my entire view with it!
For what it's worth, viewController is setup by calling a segue that is setup in a storyboard:
class LaunchController : UIViewController {
var performedSegue = false
override func viewDidLayoutSubviews() {
if !performedSegue {
self.performSegue(withIdentifier: "main", sender: self)
performedSegue = true
}
}
}
It looks like this "feature" was introduced by Apple in iOS 13. I tested
this on iOS 12 and the presenting ViewController does not disappear.
I traced the call stack and it appear that UIActivityViewController is calling dismiss on presenting view controller (or it's UINavigationController) when saving to camera roll succeeds.
I don't know the way to prevent this since it's Apple private API and there's nothing in documentation about this. The only way I found, is to to set some kind of flag savingToCameraRoll and set it to true when presenting UIActivityViewController, override dismiss method on presenting ViewController and then check this flag inside dismiss.
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if !savingToCameraRoll {
// Dismiss view controller if UIActivityViewController not in use
super.dismiss(animated: animated, completion: completion)
}
// Handle UIActivityViewController dismiss attempt.
// If you do nothing, the presenting ViewController should not be dismissed.
}
You should also remember to set savingToCameraRoll to false in completionWithItemsHandler.
In the storyboard, I marked the presenting ViewController as "Is Initial View Controller" instead of presenting it via a segue. That solved the issue for me (though it's a workaround that would not work in general).

MessageKit InputBar is hidden/removed on ViewController dismissal

I am using MessageKit 3.0.0-swift5 branch for chats.
Clicking on the message, I am presenting the ViewController.
When Viewcontroller is dismissed, I am not able to access InputBar.
Has anybody come across this issue?
Check the video here.
Code:
// MessageCellDelegate
func didTapMessage(in cell: MessageCollectionViewCell) {
self.showFileInBrowser(withTitle: "", url: fileURL)
}
func showFileInBrowser(withTitle title: String? = nil, url: URL) {
self.fileBrowser = FileBrowserViewController(title: title, url: url)
let navigation = BaseNavigationController(rootViewController: fileBrowser!)
self.present(navigation, animated: true, completion: nil)
}
// FileBrowserViewController
#objc func closeButtonTapped() {
self.dismiss(animated: true, completion: nil)
}
I am also using IQKeyboardManager, but the below solution is not working.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
IQKeyboardManager.shared().isEnabled = false
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
IQKeyboardManager.shared().isEnabled = true
}
I had the same issue in my application, which is using just InputBarViewController (in some reasons I had to implement my own chat, not using MessageKit).
So, this issue is very easy to reproduce if you are trying to show some modal view controller not from current controller, or even moving first responder focus to another UITextField within current VC (e.g. search field).
For me working solution is just call becomeFirstResponder on current view controller instance just after resigning control from search field or inside viewDidAppear (or in some other place where you can be sure, chat UI is visible again).
P.S. I also faced this issue when UIContextMenu (long touch in cell) was dismissed. Solution was pretty the same.
I think that you have to before present the next ViewController disable Keyboard by TextView.resignFirstResponder() Because the problem begins while ViewController is Presenting
I too facing that issue before, but i tried to to present nextViewController as by adding the following code. hope it will work.
nextViewController.modalPresentationStyle = .overCurrentContext
nextViewController.modalTransitionStyle = .coverVertical

Weird bug when presenting a view controller

I have a basic app with a UITabBarController as the root view controller. When a user of the app is not signed in I'm showing a modal controller via window!.rootViewController!.present(viewController, animated: true) in my AppDelegate. This works fine on all iPhone models, however the following happens on any iPad:
The background color of the SignInController is visible during the transition. Now comes the weird thing: When I change the view in Interface Builder to an iPad the bug is gone like so:
Changing the background color back to the transparent default removes at least the white background, however the view is still animating from the left bottom which is something I don't want. And by the way, changing the view in Interface Builder breaks the animation on all iPhones. Changing it back fixes it but breaks again all iPads.
This is the code (using ReSwift for state management):
func newState(state: State) {
switch (previousState.session, state.session) {
case (.loading, .notSignedIn), (.signedIn, .loading):
(window!.rootViewController! as! UITabBarController).selectedIndex = 0
let viewController = storyboard.instantiateViewController(withIdentifier: "SignInViewController")
window!.rootViewController!.present(viewController, animated: true, completion: nil)
default:
// more stuff
break
}
}
EDIT: Added the actual code.
I fixed it! 😊
The problem was a combination of having an observer on keyboardWillShowNotification and a becomeFirstResponder in the viewWillAppear method of the presented controller.
Moving the becomeFirstResponder into viewDidAppear fixed all the problems!
Thanks man! Saved my day.. I'm presenting the keyboard from within a tableview cell - I fixed it like this:
private var canPresentKeyboard: Bool = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
canPresentKeyboard = true
if _currentlySelectedIdType != .image {
reload(section: .idType)
}
}
func configure(cell: NumberIdTableViewCellInput) {
cell.set(delegate: self)
if canPresentKeyboard {
cell.clearAndSetFirstResponder()
}
}
I know the code is a bit out of context, but I believe the intention is clear.

Swift - UIPopoverController in iOS 8

I'm trying to add a simple popoverController to my iphone app, and I'm currently struggling with the classic "blank screen" which covers everything when I tap the button.
My code looks like this:
#IBAction func sendTapped(sender: UIBarButtonItem) {
var popView = PopViewController(nibName: "PopView", bundle: nil)
var popController = UIPopoverController(contentViewController: popView)
popController.popoverContentSize = CGSize(width: 3, height: 3)
popController.presentPopoverFromBarButtonItem(sendTappedOutl, permittedArrowDirections: UIPopoverArrowDirection.Up, animated: true)
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController!) -> UIModalPresentationStyle {
// Return no adaptive presentation style, use default presentation behaviour
return .None
}
}
The adaptivePresentationStyleForPresentationController-function was just something I added because I read somewhere that this is what you need to implement to get this function on the iphone. But still: there is still a blank image covering the whole screen, and I do not know how to fix it.
Any suggestions would be appreciated.
The solution I implemented for this is based on an example presented in the 2014 WWDC session View Controller Advancements in iOS 8 (see the slide notes). Note that you do have to implement the adaptivePresentationStyleForPresentationController function as a part of the UIPopoverPresentationControllerDelegate, but that function should be outside of your sendTapped function in your main view controller, and you must specify UIPopoverPresentationControllerDelegate in your class declaration line in that file to make sure that your code modifies that behaviour. I also took the liberty to separate out the logic to present a view controller in a popover into its own function and added a check to make sure the function does not present the request view controller if it is already presented in the current context.
So, your solution could look something like this:
// ViewController must implement UIPopoverPresentationControllerDelegate
class TheViewController: UIViewController, UIPopoverPresentationControllerDelegate {
// ...
// The contents of TheViewController class
// ...
#IBAction func sendTapped(sender: UIBarButtonItem) {
let popView = PopViewController(nibName: "PopView", bundle: nil)
self.presentViewControllerAsPopover(popView, barButtonItem: sender)
}
func presentViewControllerAsPopover(viewController: UIViewController, barButtonItem: UIBarButtonItem) {
if let presentedVC = self.presentedViewController {
if presentedVC.nibName == viewController.nibName {
// The view is already being presented
return
}
}
// Specify presentation style first (makes the popoverPresentationController property available)
viewController.modalPresentationStyle = .Popover
let viewPresentationController = viewController.popoverPresentationController?
if let presentationController = viewPresentationController {
presentationController.delegate = self
presentationController.barButtonItem = barButtonItem
presentationController.permittedArrowDirections = .Up
}
viewController.preferredContentSize = CGSize(width: 30, height: 30)
self.presentViewController(viewController, animated: true, completion: nil)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
}
Real world implementation
I implemented this approach for input validation on a sign up form in an in-progress app that I host on Github. I implemented it as extensions to UIVIewController in UIViewController+Extensions.swift. You can see it in use in the validation functions in AuthViewController.swift. The presentAlertPopover method takes a string and uses it to set the value of a UILabel in a GenericAlertViewController that I have set up (makes it easy to have dynamic text popovers). But the actual popover magic all happens in the presentViewControllerAsPopover method, which takes two parameters: the UIViewController instance to be presented, and a UIView object to use as the anchor from which to present the popover. The arrow direction is hardcoded as UIPopoverArrowDirection.Up, but that wouldn’t be hard to change.

Resources