I'm trying to add a navigation controller to my UIViewController subclass programmatically (I'm not using storyboards) and I wanted to find the best place to init it and configure such.
I have tried viewDidLoad (the views weren't initialized by init) and a convenience init (just to make sure) method but no luck.
Here's how I'm creating it:
override init!(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let nav = UINavigationController(rootViewController: self)
}
What would be the most appropriate method to place this logic in? I would like to keep this logic contained within this VC.
I am able to accomplish this by creating the nav. with a root vc from the presenting VC, but this leaks this logic and I'd rather not do that.
I'm not sure this is what you're looking for, but you could put some logic within a method in the controller you're presenting to determine whether it should be presented with or without a navigation controller; doing it this way means the presenting view controller doesn't need to know whether the controller it's presenting is embedded in a navigation controller or not. The presenting controller, would call this method after instantiating your controller. The presented controller could look something like this,
class NextViewController: UIViewController {
var wantsNavigationcontroller = true
func viewControllerWithOrWithoutNavigationController() -> UIViewController {
if wantsNavigationcontroller {
let nav = UINavigationController(rootViewController: self)
return nav
}else{
return self
}
}
}
The presenting controller would do this,
#IBAction func PresentNextcontroller(sender: UIButton) {
var nextVC = NextViewController()
self.presentViewController(nextVC.viewControllerWithOrWithoutNavigationController(), animated: true, completion: nil)
}
Related
i'm working on multiple frameworks and my question is just a "philosophic" one.
I created an utility function to show view controllers
static func presentViewController(identifier: String, storyboardName: String = "Main", presentationStyle: UIModalPresentationStyle = .fullScreen){
let storyboard = UIStoryboard(name: storyboardName, bundle: InternalConstants.bundle)
var viewResult: UIViewController
if #available(iOS 13.0, *) {
viewResult = storyboard.instantiateViewController(identifier: identifier)
} else {
viewResult = storyboard.instantiateViewController(withIdentifier: identifier)
}
viewResult.modalPresentationStyle = presentationStyle
var top = UIApplication.shared.keyWindow?.rootViewController
while top?.presentedViewController != nil {
top = top!.presentedViewController
}
top!.present(viewResult, animated: true, completion: nil)
}
First of all, is this a correct way to present a view controller or is there a better way?
Then, is it better to present a view controller in a navigation controller or not?
First of all, is a correct way to present a view controller or there is a better way?
instead of a utility make it inside
extension UIViewController {
func ......
}
Then, is better to present a view controller in a navigation controller or not?
a nav is oriented for push/pop but it's also not wrong to use it to present another vc
First of all, is this a correct way to present a view controller or is there a better way?
as long as it's working then it's correct it's just your way of doing this specific thing, but is it the right thing to do as for an iOS and UIKit standpoint the answer is no it's usually is a bad thing to present a viewController by looking at the rootViewController's presentedViewController because it's not guaranteed that the last presentedViewController you find is a good thing to present on it and you won't know until it breaks, that presentedViewController could be a UISearchController and if you use UIContentContainer or ContainerView from storyboards, you might have a small viewController that is just a UISlider at the end, this could be bad for viewController appearance and disappearance
another problem that you will face is when you need to pass data to and from the viewController that you presenting by using this approach you don't even have a reference to the viewController you are presenting, because you are only passing an identifier
from an MVC standpoint you should never try to present viewController from a UIView by calling your function from your view directly Thats Bad Practice
if you take a look at the UIKit SDK if you ever try to present any system UIViewController you will find that you have the responsibility of instantiating and presenting the vc for example UIImagePickerController, UIActivityViewController, UIDocumentPickerViewController, UIDocumentMenuViewController, UIPrinterPickerController, UIVideoEditorController
Apple themselves didn't go for providing a function to present theirs system vcs
instead if you are developing a framework and don't want to give users access to your viewControllers you should make you own window and give it a rootViewController
Apple also has many examples for this too, in the AuthenticationServices framework for security reasons you should not have a reference to the safari web browser they have something called ASWebAuthenticationSession that controls the flow of presenting and dismissing the Safari Web ViewController by calling start() and cancel() functions
also the users of your framework will not always want to present your viewController with the default presentation animation they might want to use custom viewContollers animations which they will need access to the transitioningDelegate property on the viewController
imagine every public useful property on UIViewController will not be accessible if you go with this approach
Then, is it better to present a view controller in a navigation controller or not?
as for this part it's totally fine to present anything on a navigationController
Storyboards headaches
as for the storyboards initialization headaches there are plenty of articles out there talking about optimizing the storyboards initialization call site for that I would recommend doing something like this
extension UIStoryboard {
enum AppStoryBoards: String {
case
login,
main,
chat,
cart
}
convenience init(_ storyboard: AppStoryBoards, bundle: Bundle? = nil) {
self.init(name: storyboard.rawValue.prefix(1).capitalized + storyboard.rawValue.dropFirst(), bundle: bundle)
}
}
This way you can initialize a storyboards using enum which improves the call site to be like this
let login = UIStoryboard.init(.login)
then you can have another extension for view controller initialization like this
extension UIStoryboard {
func instantiateInitialVC<T: UIViewController>() -> T {
return self.instantiateInitialViewController() as! T
}
func instantiateVC<T: UIViewController>(_: T.Type) -> T {
return self.instantiateViewController(withIdentifier: String(describing: T.self)) as! T
}
}
and you can then call it like this
let loginVC = UIStoryboard.init(.login).instantiateInitialVC()
or this
let loginVC = UIStoryboard.init(.login).instantiateVC(LoginViewController.self)
by doing that you improve your overall code for presenting any viewController
let dvc = UIStoryboard.init(.login).instantiateVC(LoginViewController.self)
dvc.plaplapla = "whatever"
present(dvc, animated: true)
I have a loading view controller when my app starts(is is my initial view controller).When an animation in this view controller finished I want it to show another view controller and dismiss the view controller with the animation.
The loading view controller is the initial view controller,
I have this code when UIStoryboard.mflMainTabBarViewController(). returns the view controller that I want to present
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
let animationID = anim.value(forKey: "animationID")
if animationID as! NSString == "transform" {
self.present(UIStoryboard.mflMainTabBarViewController(), animated: true, completion: {
_ = self.popoverPresentationController
})
}
}
But when deinit is never called
deinit {
print("deinit")
}
What is the best way to deinit the first view controller, and making the presenting view controller the root view controller?
Unless you're doing something very specialized, you don't need to de-init an object in Swift. It will be called automatically when the reference count goes to 0. If you really need to, you should be able to set you window's rootViewController through your AppDelegate.
However, be aware that maintenance like this is rarely necessary.
A deinitializer is called immediately before a class instance is deallocated
after you can use
if let delegate = UIApplication.shared.delegate as? AppDelegate {
let storyboard : UIStoryboard? = UIStoryboard(name: "storyboardName", bundle: nil)
let rootController = storyboard!.instantiateViewController(withIdentifier: "controllerIdentifier")
delegate.window?.rootViewController = rootController
}
I would like to process code when a ViewController is no longer visible due to presenting a new ViewController.
I cannot use ViewWillDisappear etc since the controller is not technically ever dismissed from the stack - you just can't see it.
What process can I use so that code runs when the controller is no longer visible (i.e. topmost) and when it becomes visible again?
EDIT:
Seems some confusion here - not sure why.
I have a viewcontroller.
I use the following code to present another controller
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navController = storyboard.instantiateViewControllerWithIdentifier("NavController") as! UINavigationController
let thisController = navController.viewControllers[0] as! MyController
self.presentViewController(navController, animated: true, completion: nil)
This controller does not trigger a viewWillDisappear on the previous controller since the previous view is not removed - just hidden.
I need to process code when this view is hidden (i.e. not visible) and, more importantly, process code when it becomes visible again.
When presenting a UIViewController if the presentation style has been set to UIModalPresentationOverCurrentContext it doesn't call the viewWillDisappear and related methods as the view never disappears or gets hidden.
A simple test to check if thats the case would be to set the NavController that you are using to have a clear background color. If you do this and present the NavController and you can still view the first UIViewController below your NavController content. Then you are using UIModalPresentationOverCurrentContext and that is why the viewDidDisappear isn't called.
Have a look at the answer referenced by Serghei Catraniuc (https://stackoverflow.com/a/30787112/4539192).
EDIT: This is in Swift 3, you can adjust your method accordingly if you're using an older version of Swift
If you won't be able to figure out why viewDidAppear and viewDidDisappear are not called, here's a workaround
protocol MyControllerDelegate {
func myControllerWillDismiss()
}
class MyController: UIViewController {
var delegate: MyControllerDelegate?
// your controller logic here
func dismiss() { // call this method when you want to dismiss your view controller
// inform delegate on dismiss that you're about to dismiss
delegate?.myControllerWillDismiss()
dismiss(animated: true, completion: nil)
}
}
class PresentingController: UIViewController, MyControllerDelegate {
func functionInWhichYouPresentMyController() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navController = storyboard.instantiateViewController(withIdentifier: "NavController") as! UINavigationController
let thisController = navController.viewControllers[0] as! MyController
thisController.delegate = self // assign self as delegate
present(navController, animated: true, completion: {
// place your code that you want executed when it disappears here
})
}
func myControllerWillDismiss() {
// this method will be called now when MyController will dismiss
// place your code that you want executed when it re-appears here
}
}
Firstly, thanks to Serghei for his time in helping work through this.
To clarify, both my potential presented controllers were set to Full Screen presentation style in the storyboard, however one was being set to Custom via a piece of pasted code dealing with the presentation. I can't find the error with the other.
However, if I force a presentation style of Full Screen as part of the presenting process then all is ok.
Hopefully my frustrating afternoon can help to save someone else's - always try to understand the implications and processes involved in pasted snippets.
I meet a strange problem: I made 2 view controllers for wich I can switch the view with code:
var currentViewController:UIViewController=UIApplication.shared.keyWindow!.rootViewController!
func showController()
{
let ViewControllernew1 = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "viewController2")
currentViewController.present(ViewControllernew1, animated: true, completion: nil)
}
My app open correctly to the first view controller, then, when I click on the button created on a sprite kit scene, I can switch the view to my new view controller successfully (I get my second scene successfully showed) but then, I can not change anymore my view controller after this switch. If I click again on the button, I get this message:
Attempt to present on Test_Vuforia.GameViewController: 0x12f549610 whose view is not in the window hierarchy!
Do you know what is the problem ? I understand I'm in the root position so that I can not change anymore my view controller after having switched it, but how to change that ?
Thanks !
Edit:
My code is used inside a SKScene and not from a UIVewController and I get this error when I use the suffix self. : Value of type View (SKScene) has no member 'present'.
I'm creating an augmented reality game with Vuforia and I need to switch AR view with SKScene.
Issue
Current viewController is not the rootViewController from UIApplication. So you should find the current viewController which is visible and then present it from there.
Solution
Simply find the topViewController on your UIApplication Stack, and from there present your controller.
let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "viewController2")
UIApplication.topViewController()?.present(newViewController, animated: true, completion: nil)
This extension of UIApplication comes in handy for your case
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
References: Gist
Calling function in viewDidAppear helps in my case. Solution for Swift 3:
In your Main Controller:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showTutorialModally()
}
func showTutorialModally() {
let tutorialViewController = TutorialViewController()
tutorialViewController.modalPresentationStyle = .overCurrentContext
present(tutorialViewController, animated: true, completion: nil)
}
In your Tutorial Controller:
view.backgroundColor = UIColor.clear
view.isOpaque = false
Use the extension below to retrieve the next available controller in the stack.
Swift 3
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
Swift 2.3
extension UIResponder {
func nextResponder<T: UIResponder>(_ type: T.Type) -> T? {
return nextResponder() as? T ?? nextResponder()?.nextResponder(type)
}
}
Inside your SKScene, view?.next(UIViewController.self) gives you the next available UIViewController in the hierarchy.
Add this extension to a group in your project called Categories, if this group does not exist already create it, then create a new file called UIResponder+NextOfType.swift and paste the extension.
Xcode error significance for roughly: this view is not in the Window of the view hierarchy.
What I don't think the above answer questions, but maybe you might have wondered why this would happen.
But I find that you are the reasons for this problem is likely to be in the ViewController life cycle at ViewDidLoading switch view Code execution inside.
Reason is probably that, when the ViewController implementation allco init during initialization, it will be executed asynchronously viewWillLoad - > viewDidLoad... -- -- -- -- > viewDidApper. Then may be in code execution to the viewDidLoad. The ViewController may not assign values to the Window. The rootViewController. So we directly use [self presentViewController:] will appear this error.
It is recommended that you move the code of the switch to ViewDidApper.
I hope it will help you.
Probably your rootViewController is not the current ViewController. Either you presented or pushed a new UIViewController on top of it.
The viewController's view is not in the
window's view hierarchy at the point that it has been loaded (when
the viewDidLoad message is sent), but it is in the window
hierarchy after it has been presented (when the viewDidAppear:
message is sent). if you calling showController method from
viewDidLoad just call it from viewDidAppear method
Do something like:
let vc: UIViewController = (self.storyboard?.instantiateViewControllerWithIdentifier("viewController2"))!
self.presentViewController(vc, animated: true, completion: nil)
// OR
self.navigationController?.pushViewController(vc, animated: true)
Use like this
let vc = self.view?.window?.rootViewController
func showController()
{
let ViewControllernew1 = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "viewController2")
vc.present(ViewControllernew1, animated: true, completion: nil)
}
Maybe the issue is with the currentViewController.
I have two classes:
class ExplorerViewController: UIViewController {
lazy var studyButton: ExploreButton = {
let button = ExploreButton()
button.setTitle("Study", forState: .Normal)
return button
}()
}
and
class ViewController: UIViewController, UISearchBarDelegate, LocateOnTheMap, GMSMapViewDelegate {
}
I'm trying to make it so that when I click the studyButton, it sends the button title to ViewController and goes to that view.
I'm not using storyboards and am having trouble with segues since every tutorial seems to give different examples that are specific to the things they've been working with and 95% of them seem to be operating with storyboard. Can someone give me a general way of how to do this?
How do I give the starting view controller an identifier because it isn't instantiated like the other controllers that I 'move' to after. How can I move from ViewController to ExplorerViewController and then move back to that same ViewController (with all changes intact).
Create an initializer for your ViewController that receives the "title" variable:
class ViewController: UIViewController, UISearchBarDelegate, LocateOnTheMap, GMSMapViewDelegate {
var btnTitle: String?
init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?, btnTitle:String?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.btnTitle = btnTitle
}
}
When creating the ViewController object use this initializer.
var viewController = ViewController(nibName: "ViewController", bundle: nil, btnTitle: title
You can initialize UIViewController that you want navigate to, assign data to properties in that controller and call this method:
presentViewController(viewController, animated: true, completion: nil)
For example:
let destinationViewController = ViewController()
destinationViewController.frame = self.view.frame
destinationViewController.buttonTitle = "title"
self.presentViewController(destinationViewController, animated: true, completion: nil)
Although I would suggest you to get familiar with Storyboards and perform navigation with Segues.
Make sure of two things:-
1.) You have given your viewController an StoryBoard ID lets say "viewControllerVC_ID" in it's Identity inspector
2.) You have NavigationController Embed in to your Initial entry point View Controller
In ViewController declare a variable
var btnLabelTxt : String!
Create an #IBAction of that button in ExplorerViewController :-
#IBAction func exploreBtnAction(sender : UIButton!){
let vcScene = self.navigationController?.storyboard?.instantiateViewControllerWithIdentifier("viewControllerVC_ID") as! ViewController
vcScene.btnLabelTxt = "Study"
//or you can just access the button itself in the viewController and set the title
//By vcScene.yourBtn.setTitle("Study", forState: .Normal)
self.navigationController?.pushViewController(vcScene, animated: true)
}
please see this question How to push viewcontroller ( view controller )? for how to switch between views.
to pass data once you have reference to the new view, you can assign the data to a property of that view.