on iPad, I have a a ViewController that presented popover on another ViewController.
private lazy var menuPadViewController = MenuViewController()
private func presentMenuVC(from sourceView: UIButton) {
let nc = UINavigationController(rootViewController: menuPadViewController)
nc.modalPresentationStyle = .popover
nc.popoverPresentationController?.sourceView = sourceView
present(nc, animated: true)
}
Due to UI of the presented MenuViewController, I need to dismiss it when device is rotate, otherwise it would be look so mess.
So, in viewWillTransition I set that the MenuViewController should be dismissed after rotation. It works pretty fine
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
menuPadViewController.dismiss(animated: true)
}
The issue is, it works fine when the MenuViewController is presented (on the display), but when it's already dismissed, menuPadViewController.dismiss(animated: true) will dismiss the parent ViewController. I need to implement some conditions to only dismiss it if it's presented and on the display.
Would be awesome if you can show me the best reliable way to do it, many thanks!
From the presenting view controller, you can check the type of view controller that is currently being presented before dismissing it.
if let presented = presentedViewController,
presented is YourPresentedViewController {
// dismiss
}
Related
as I wrote in the title, I'm not sure why ViewWillAppear is not called when another VC is dismissed. I think my project is a little bit tricky, so I'm gonna explain what is going on in my project.
Mostly, I configure UIs in code. I have two VCs, ListVC and CameraVC, and I have a tab bar and a navigation var in ListVC, which I configured all in code.
So in the SceneDelegate
I wrote something like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: scene.coordinateSpace.bounds)
window?.windowScene = scene
window?.rootViewController = createTabBar()
window?.makeKeyAndVisible()
}
func createTabBar() -> CustomTabBarController {
let tabbar = CustomTabBarController()
tabbar.viewControllers = tabbar.setUpTabbarItems()
return tabbar
}
and in the CustomTabBarController class, since I just wanted to present the event list, I added
func createListNC() -> UINavigationController {
let ListVC = ListViewController()
ListVC.tabBarItem = UITabBarItem(title: "", image: UIImage(named: "add-icon"), tag: 0)
return UINavigationController(rootViewController: ListVC)
}
func setUpTabbarItems() -> [UIViewController]{
return [createListNC()]
}
So now I can display List VC on the home of the app. However, I made the CameraVC all in the storyboard and I added the following code when a user taps one of the Event cells in List VC and it presents CamearaVC.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let storyboard = UIStoryboard(name: "Camera", bundle: nil)
let CameraVC = storyboard.instantiateViewController(identifier: "Camera")
self.present(CameraVC, animated: true, completion: nil)
}
So I can also present the CameraVC from ListVC.
But the problem happens when I want to back from CameraVC to ListVC.
I added IBAction to the button saying "< back" in CameraVC, and I can dismiss the CameraVC, however, since the cameraVC's screen is horizontal, the ListVC also stack with horizontal, which I want to make the ListVC vertical.
#IBAction func unwindToLiveList (_ segue: UIStoryboardSegue) {
self.presentingViewController?.dismiss(animated: true, completion: nil)
}
So, I was planning to write a code for fixing the orientation of the app when a user back from CameraVC to ListVC.
I just wanted to know where I should put that fixing orientation code, so I added viewwillappear and some print statements to ListVC, but none of them are called when back from CameraVC to ListVC. I also wrote the same code in CustomTabBarController class, but it is never called...
So, I was wondering where and which file I can write the code for fixing the orientation to trigger when the user came back from CameraVC to ListVC.
Also, if anyone can explain why this is happening, please let me know.
The problem seems to be the way View controllers are presented now. Since the bottom view controller never actually leaves view will appear isn't called when dismissing anymore. I'm pretty sure it did in the past, but now that controllers present as overlays I guess it doesn't anymore.
However, you are in a navigation stack so if you just use that you'll get a free back button and viewWillAppear will get called.
let cameraVC = storyboard.instantiateViewController(identifier: "Camera")
self.navigationController?.pushViewController(cameraVC, animated: true)
As I said you'll get a free back button on cameraVC and that will pop cameraVC off the stack for you.
If you want to keep from using the navigationController for some reason take a look at
https://developer.apple.com/documentation/uikit/uicontentcontainer/1621466-viewwilltransition
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
print(size)
}
The reason is about the change they had made in iOS13 (or 14). Before the default modal presentation mode (it seems that you are presenting modally) was full screen, now they are presented over the current context without making the presenting view controller to disappear, that is why willAppear is not called, it' already there.
If you want to change that behavior you can setup a different presentation style as .fullScreen.
I implemented the share extension and I want animate my View Controller with a crossDissolve, so i set the modalPresentationStyle = .overFullScreen and modalTransitionStyle = crossDissolve but it seems not working. The VC still appear from the bottom to the top and with the new iOS 13 modal style (not completly full screen).
Anyone know how to solve it? It tried both with and without storyboard.
NB: I'm not talking about a normal VC presentation, but the presentation of the share extension, it means that it's another app that present my VC.
One way to do it would be to have the system presented viewcontroller as a container.
And then present your content viewcontroller inside modally.
// this is the entry point
// either the initial viewcontroller inside the extensions storyboard
// or
// the one you specify in the .plist file
class ContainerVC: UIViewController {
// afaik presenting in viewDidLoad/viewWillAppear is not a good idea, but this produces the exact result you are looking for.
// meaning the content slides up when extension is triggered.
override func viewWillAppear() {
super.viewWillAppear()
view.backgroundColor = .clear
let vc = YourRootVC()
vc.view.backgroundColor = .clear
vc.modalPresentationStyle = .overFullScreen
vc.loadViewIfNeeded()
present(vc, animated: false, completion: nil)
}
}
and then use the content viewcontroller to show your root viewcontroller and its view hierarchy.
class YourRootVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let vc = UIViewController() // your actual content
vc.view.backgroundColor = .blue
vc.view.frame = CGRect(origin: vc.view.center, size: CGSize(width: 200, height: 200))
view.addSubview(vc.view)
addChild(vc)
}
}
Basically a container and a wrapper in order to get the control over the views being displayed.
Source: I had the same problem. This solution works for me.
I have UINavigationController with several pushed view controllers.
UPD: Last pushed controller modally presents another controller.
Also, I have UINavigationControllerDelegate with some logic at navigationController:willShowViewController:animated:.
UPD: Navigation controller is its own delegate. Delegate is set in viewDidLoad method.
Question rises when I try to close all controllers programically from presented view controller:
// Close all controllers in navigation stack
presentingViewController?.navigationController?.popToRootViewController(animated: true)
// Close presented view controller
dismiss(animated: true, completion: nil)
Method navigationController:willShowViewController:animated: is not called. But it is called when I do the same without presented controller (thanks to #donmag for example project where it works).
Searched SO for answers or similar questions, but found nothing, any thoughts?
In your "presenting" VC, you want to implement a delegate/protocol pattern so your "presented" VC can call back and perform the dismiss and popToRoot...
// protocol for the presented VC to "call back" to the presenting VC
protocol dismissAndPopToRootProtocol {
func dismissAndPopToRoot(_ animated: Bool)
}
// in the presenting VC
#IBAction func presentTapped(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "presentMeVC") as? PresentMeViewController {
// Assign the delegate when instantiating and presenting the VC
vc.dapDelegate = self
present(vc, animated: true, completion: nil)
}
}
func dismissAndPopToRoot(_ animated: Bool) -> Void {
// this will dismiss the presented VC and then pop to root on the NavVC stack
dismiss(animated: animated, completion: {
self.navigationController?.popToRootViewController(animated: animated)
})
}
// in the presented VC
var dapDelegate: dismissAndPopToRootProtocol?
#IBAction func dismissTapped(_ sender: Any) {
// delegate/protocol pattern - pass true or false for dismiss/pop animation
dapDelegate?.dismissAndPopToRoot(false)
}
Here's a full demo project: https://github.com/DonMag/CustomNavController
From documentation:
popToRootViewControllerAnimated:
Pops all the view controllers on the stack except the root view controller and updates the display.
popViewControllerAnimated:
Pops the top view controller from the navigation stack and updates the display.
So seems like in order to get navigationController:willShowViewController:animated: called every time you have to do subsequent popViewControllerAnimated:, because the display got updated each time after you pop a new controller. When you pop to root view controller, update display is called only once.
I have a container view controller. One of the child view controllers needs to present a popover. The problem is that the popover's presentingViewController ends up being the container view controller and not the child view controller that presented it.
*I've set the definesPresentationContext property to true on the child view controller (and its navigation controller) but that doesn't change anything (that's only useful if the presented controller's modalPresentationStyle is .currentContext or overCurrentContext
The documentation for UIViewController present(_:animated:completion:) talks about how the presentation may be from another controller depending on the presentation style. So I'm hoping there is a way to override or intercept that determination so I can ensure, in this case, that the popover's presentingViewController is the controller that originally presented it.
I have other functionality in the container view controller related to adapting to changes in size class that are being hindered by this problem.
Is there any way to ensure a popover's presentingViewController is the original presenter? It's probably related to how the presentationController is created but I don't see how to tap into that process.
Below is code that demonstrates the problem. If you wish to replicate the problem, create a new iOS Single View App project in Xcode. Then replace the contents of the provided ViewController.swift with the code below. Run on an iPad simulator. A container view controller with one child view controller will be added and a popup will be presented from the child. The debugger console will show some output. Notice how the popup's presentingViewController is the container and not the child.
Note: While the following is in Swift, Objective-C responses are fine too.
import UIKit
class ViewController: UIViewController {
var childViewController: UIViewController!
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
self.childViewController.navigationController?.view.frame = CGRect(x: size.width / 2, y: 0, width: size.width / 2, height: size.height)
}, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
childViewController = UIViewController()
childViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, target: nil, action: nil)
let nav = UINavigationController(rootViewController: childViewController)
addChildViewController(nav)
view.addSubview(nav.view)
nav.didMove(toParentViewController: self)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let popup = UIViewController()
popup.definesPresentationContext = true
let nav = UINavigationController(rootViewController: popup)
nav.modalPresentationStyle = .popover
nav.definesPresentationContext = true
nav.popoverPresentationController?.barButtonItem = childViewController.navigationItem.leftBarButtonItem
childViewController.present(nav, animated: true) {
print("Popup displayed")
print("container: \(self)")
print("child: \(self.childViewController), child.nav: \(self.childViewController.navigationController)")
print("presentingViewController: \(nav.presentingViewController)")
}
}
}
I have a UILabel in my ViewController that has a NavigationController (let's say view controller A) with a tap gesture recognizer attached to the label. When the label is tapped another view appears (let's call it B). The user picks some text in B and the view dismisses back to A with the label text updated with the selection. So I created a delegation between A and B to get the selection. The problem is that I do not see the NavigationBar when B appears. Is there a way to fix this?
ViewController A
#IBOutlet weak var sectionName: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let sectionLabelTap = UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:)))
sectionName.isUserInteractionEnabled = true
sectionName.addGestureRecognizer(sectionLabelTap)
}
#objc func labelTapped(_ sender: UITapGestureRecognizer) {
let sectionNameVC = storyboard?.instantiateViewController(withIdentifier: "SectionName") as! SectionNameTableViewController
sectionNameVC.selectionNameDelegate = self
sectionNameVC.userData = userData
present(sectionNameVC, animated: true, completion: nil)
}
In order to display the Navigation bar the UIViewController needs to have a UINavigationController.
You can add that sectionNameVC ViewController into a UINavigationController to persevere the present animation.
In that case your code might look something like this:
#objc func labelTapped(_ sender: UITapGestureRecognizer) {
let sectionNameVC = storyboard?.instantiateViewController(withIdentifier: "SectionName") as! SectionNameTableViewController
sectionNameVC.selectionNameDelegate = self
sectionNameVC.userData = userData
let naviagtionController = UINavigationController(rootViewController: sectionNameVC)
present(naviagtionController, animated: true, completion: nil)
}
Or you can simply call pushViewController on the View Controller A's navigation Controller, like this:
self.navigationController?.pushViewController(sectionNameVC, animated: true)
This will add sectionNameVC into the View Controller A's navigation Controller stack. In this case the transition animation will be different, the sectionNameVC will come from your right.
You are missing the concept between "Presenting" View Controller & "Navigating" the View Controller. You will get the answer, once you understood the concept. Here, it is..
When you are presenting the ViewController, you are completely replacing the stack container to the new view controller.
STACK holds the addresses of the ViewControllers you push or pop via navigating.
e.g:
present(sectionNameVC, animated: true, completion: nil)
On the other hand, if you are navigating to other view controller by pushing it. In this case, you can go back to previous controller by simple popping the ViewController address from stack.
e.g:
self.navigationController?.pushViewController(sectionNameVC, animated: true)
self.navigationController?.popViewController(animated: true)
So, If you navigate then, only you will get navigation Bar.
Now, in your case, you are presenting the ViewController and hence, navigation bar is not showing.