Trying to programmatically present contained View Controller - swift

Using XCode 6.1 I am trying to roll my own View Controller containment with a custom menu... I am using the following function within my Main View Controller to present child view controllers:
var currentController: UIViewController?
// Function within main view controller class
func presentController(controller: UIViewController) {
if currentController != controller {
if currentController != nil {
currentController!.willMoveToParentViewController(nil)
currentController!.view.removeFromSuperview()
currentController!.removeFromParentViewController()
}
controller.willMoveToParentViewController(self)
self.addChildViewController(controller)
self.view.addSubview(controller.view)
controller.didMoveToParentViewController(self)
currentController = controller
}
}
When the app is initially run I use self.presentController(firstViewController) inside viewDidAppear, which works.
However, in my custom menu (using REMenu) outside of the Main View Controller, I'm trying to display the selected view controller like so: MainViewController().presentController(secondViewController). When this runs the currentController gets removed (revealing the Main View Controller's view which is just a black background) but the new controller doesn't get loaded in.
Can anyone guide me in the right direction?

Assuming that MainViewController is the name of your main view controller's class, the following line probably does not do what you intend:
MainViewController().presentController(secondViewController)
The MainViewController() will instantiate a new copy of the main view controller. I assume you meant to get a reference to the existing controller, not instantiate a new one.
Unrelated to your problem at hand, but you should not call willMoveToParentViewController before calling addChildViewController (because it does that for you). You only need to call didMoveToParentViewController when you're done configuring the new controller's view. Thus:
func presentController(controller: UIViewController) {
if currentController != controller {
if currentController != nil {
currentController!.willMoveToParentViewController(nil)
currentController!.view.removeFromSuperview()
currentController!.removeFromParentViewController()
}
// controller.willMoveToParentViewController(self)
self.addChildViewController(controller)
self.view.addSubview(controller.view)
controller.didMoveToParentViewController(self)
currentController = controller
}
}

Related

Using NSPageController in History Mode (Content Mode) and no View Controllers?

I'm building an app with three views and I want the user to be able to swipe between them but also navigate between them by using custom buttons, like browsing in safari but only with three available views. The user can navigate between them in a somewhat random order and I provide back and forward buttons in a toolbar.
After reading Apple's documentation I believe history mode is the option I'm looking for as I can provide the views separately and navigation is not predefined like in book mode, however when I implement it it adds my view to the arrangedObjects property but the view itself doesn't change. I've correctly wired the page controller view programmatically to a subview of the app's contentView and I can see said view correctly displayed while debugging the view hierarchy.
The "Page Controller Managed View" is set as NSPageController.view programatically.
When I try using navigateForward(to:) the view gets added correctly to the arrangedObjects and the selectedIndex is correctly set but the NSPageController.view doesn't change at all (remains a blank NSView like the loaded view from storyboard). This is my code:
class MainViewController: NSViewController {
// MARK - Instance Properties
var notificationObservers: Array<NSObjectProtocol> = []
var pageController: NSPageController?
// MARK - Interface Builder Outlets
#IBOutlet var mainContentView: NSView? // Named "Page Controller Managed View" in storyboard
override func viewDidLoad() {
super.viewDidLoad()
pageController = (NSStoryboard.main!.instantiateController(
withIdentifier: "MainPageController") as! NSPageController)
pageController!.delegate = AppDelegate
pageController!.view = mainContentView!
notificationObservers.append(
NotificationCenter.default.addObserver(
forName: NSApplication.didFinishLaunchingNotification,
object: NSApp,
queue: .main,
using: { [weak self] (notification) in
// If the 'arrangedObjects' property is 0 it means no other object has set the first view
if self?.pageController!.arrangedObjects.count == 0 {
// Set the first view
self.pageController!.navigateForward(to: AppDelegate.firstView.nsView)
}
})
)
}
deinit {
// Perform cleanup of any Apple Notification Center notifications to which we may have registered
notificationObservers.forEach { (observer) in
NotificationCenter.default.removeObserver(observer)
}
}
}
This is the view hierarchy while running the app:
What am I doing wrong? Why is the page controller view not being updated with the navigateForward(to:) view?
This could shorten the amount of code I have to use by a lot compared to using view controllers (Book mode) so any help is appreciated.

How to maintain the navigation with a segue "present modally"?

I have : navigation controller -> tableViewController -> tab bar Controller -> ViewController1 / ViewController2 / ViewController3
I click on a cell on the TableViewController and I open the TabBar. All is OK
But, I wanted to have more details from the datas in the TableViewController so I decided to make a popup with the content of the cell. I found this tutorial https://www.youtube.com/watch?v=S5i8n_bqblE => GREAT ! It's about the use of segue "present modally" with a viewcontroller containing the popup. I made a link from the popup to the tabBarController and I lose the Navigation Bar
I tried to play with navigationBar but nothing is working. I changed the type of segue but I don't obtain what I want.
I think the problem come from the type of segue. It's OK if I use it like a go/back in viewController. Do you have any solution about using this sort of popup or do I have to use another way ?
Thanks
Ok, let's take a look.
Navigation Bar is a view which is provided by Navigation Controller. Sometimes we are confused with navigation bars and navigation items. Navigation bar is the only one and it belongs to navigation controller, navigation item belongs to individual view controller from navigation stack. So, first step is simple: if you want navigation bar, wrap your modally presented controller into navigation stack.
Yes, you will face other problem, the blurred view of previous controller will become a black area. Why? there is special object called Presentation Controller (UIPresentationController) which is responsible for how controller will be presented. And it hides view of previous controller by default (in sake of performance, I think).
Ok, let's move. We can create custom presentation controller and tell it not to hide view of previous controller. Like this:
class CustomPresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool {
return false
}
}
Next step. In controller we want present modally we have to specify to things: we want to use custom presentation controller and we also want to adjust delegate object for transitioning (where we can specify custom presentation controller). The trick is that you have to do it inside initialiser, because viewDidLoad is too late: controller had been already initialised:
class PopupViewController: UIViewController {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .custom
transitioningDelegate = self
}
}
Final step. When PopupViewController became delegate for its own transitioning, it means this controller is responsible for all of them. In our particular case popup controller provides custom version of presentation controller. Like this:
extension PopupViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
That's all. Now you should see view of previous controller.

How to decide what view to display based on global variable

I'm trying to implement an IOS swift app which starts off with a tab bar controller, and in one of the tab bar controllers is an item called "account".
When a user presses the account item, I want the app to decide (onclick event) whether the view that contains sign up/login is displayed or the profile view is displayed based on a global variable "loggedIn" (bool type).
(I've tried navigation controller but what I've understood from that is that it is a sequence of views which can't decide between views)
I want to know how can this be implemented, maybe some kind of "router" if you may that can switch between views...
If you didn't understand here's a picture of what I'm trying to implement
Basic Map of what I'm trying to explain
If you can suggest a more professional way of doing such design please don't hesitate to express your opinion.
I believe a good approach is updating the view controller when loggedIn status changes. If you don't already have, create a class inheriting from UITabBarController to manage your tabs. Here's the code:
class TabController: UITabBarController {}
In your storyboard, select your tab controller, go to the Identity Inspector and set TabController as the custom class. Now TabController will manage all the view controllers in your tab bar.
It's usually not a good approach to use global variables, so let's add loggedIn in the scope of TabController and listen for any of changes in it and update the corresponding view controller:
class TabController: UITabBarController {
var loggedIn = true {
didSet {
updateProfileTab()
}
}
}
Now, whenever you change loggedIn, that change will update the proper tab. Now let's write updateProfileTab():
class TabController: UITabBarController {
func updateProfileTab() {
let viewController: UIViewController
if loggedIn {
viewController = makeProfileViewController()
} else {
viewController = makeLoginViewController()
}
setViewController(viewController, at: 2)
}
func makeProfileViewController() -> ProfileViewController {
// create and return the profile view controller
}
func makeLoginViewController() -> LoginViewController {
// create and return the profile view controller
}
}
Naturally, you might want to write the body of both makeProfileViewController and makeLoginViewController methods. The last thing for TabController is to write setViewController(_:at:) method:
class TabController: UITabBarController {
...
func setViewController(_ viewController: UIViewController, at index: Int) {
let tabBarItem = viewControllers?[index].tabBarItem
viewController.tabBarItem = tabBarItem
viewControllers?[index] = viewController
}
...
}
Now, since TabController manages your tab bar, you can access it from within any of its children view controllers:
guard let tabController = tabBarController as? TabController else { return }
tabController.loggedIn = ...
Also, it's important to select the initial state. So, in the viewDidLoad from one of your tabbed view controllers, you should perform the above code. The first tab (the one that displays first) is probably the best place to do that. Hope this helps!
EDIT
To create your login and signup view controllers, the easiest way to go is by assigning ids in your storyboard. To do that, go to your storyboard, select the view controller, and in the Identity Inspector set a Storyboard ID, which you will use to instantiate the view controller:
func makeProfileViewController() -> ProfileViewController {
let controller = self.storyboard!.instantiateViewController(withIdentifier: "theStoryboardID")
return controller as! ProfileViewController
}
Note that I'm using force unwrap here (!). That's just for brevity. In a real case scenario you will want to use some if let or guard let statements to handle nil values.

View controller won't push

I have a function inside my app delegate which is supposed to push a View Controller when called from a Siri Shortcut.
func pushNewView() {
if let wind = UIApplication.shared.delegate?.window {
if let rootViewController = wind?.rootViewController {
let viewToPush = TripTableViewController()
if let alreadySomeOneThere = rootViewController.presentedViewController {
alreadySomeOneThere.navigationController?.pushViewController(viewToPush, animated: true)
} else {
rootViewController.navigationController?.pushViewController(viewToPush, animated: true)
}
}
}
}
When I called pushNewView() from my AppDelegate, nothing happens.
If I present the view controller instead of pushing it, that does work but I want the view controller pushed.
I'm not using any storyboards, all my code is programmatic.
Does anyone know how to make this function successfully push the View Controller?
Nothing is happening because neither alreadySomeOneThere nor rootViewController are embedded in a navigation controller.
In order to push another view controller, there needs to be an existing navigation controller in place.
You need to do one of two things. Either redo your setup such that alreadySomeOneThere and rootViewController are embedded in a navigation controller, or you need to present the new view controller from alreadySomeOneThere or rootViewController.
Please note that you can't push a navigation controller. If you choose to put alreadySomeOneThere and rootViewController in a navigation controller, then just push viewToPush. Don't create another navigation controller.
If you instead decide to present the new view controller from alreadySomeOneThere or rootViewController, then depending on the needs of viewToPush, you may or may not need viewToPush embedded its own navigation controller.

UINavigationController inside a UITabBarController inside a UISplitViewController presented modally on iPhone

I'm having a UISplitViewController that contains a UITabBarController as master view.
This UITabBarController contains a UINavigationController. The detail view contains a UINavigationController as well.
On the iPad this works as expected. The show detail segue presents the imageview within the navigation controller on the detail view.
On the iPhone on the other hand I expected that the show detail segue pushes the detail view on the stack of the navigation controller of the master view. But actually it is presented modally over the master view.
When removing the UITabBarController from the storyboard and using the UINavigationController directly in the master view this works.
Has anybody an idea how I could present the detail view on the stack of the master's UINavigationController on an iPhone?
The problem with Peter's solution is that it will fall apart with the iPhone 6 +. How so? With that code, if an iPhone 6 + is in portrait orientation - the detail view pushes onto the navigation stack. All is well, so far. Now, rotate into landscape, and then you'll have the detail view showing as the detail view and the master view.
You'll need the split view controller's delegate to implement two methods:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailVC sender:(id)sender
{
UITabBarController *masterVC = splitViewController.viewControllers[0];
if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
[masterVC.selectedViewController showViewController:detailVC sender:sender];
else
[splitViewController setViewControllers:#[masterVC, detailVC]];
return YES;
}
And now, you'll need to return the top view controller from the selected tab's navigation controller:
- (UIViewController*)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController
{
UITabBarController *masterVC = splitViewController.viewControllers[0];
if ([(UINavigationController*)masterVC.selectedViewController viewControllers].count > 1)
return [(UINavigationController*)masterVC.selectedViewController popViewControllerAnimated:NO];
else
return nil; // Use the default implementation
}
With this solution, everything pushes onto the navigation stack when it should and also updates the detail view correctly on the iPad/6+ landscape.
I figured out how to put the detail on to the master's UINavigationController instead of presenting it modally over the UITabBarController.
Using the UISplitViewControllerDelegate method
- splitViewController:showDetailViewController:sender:
In case the UISplitViewController is collapsed get the masters navigation controller and push the detail view onto this navigation controller:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
showDetailViewController:(UIViewController *)vc
sender:(id)sender {
NSLog(#"UISplitViewController collapsed: %d", splitViewController.collapsed);
// TODO: add introspection
if (splitViewController.collapsed) {
UITabBarController *master = (UITabBarController *) splitViewController.viewControllers[0];
UINavigationController *masterNavigationController = (UINavigationController *)master.selectedViewController;
// push detail view on the navigation controller
//[masterNavigationController pushViewController:vc animated:YES];
// push was not always working (see discussion in answer below), use showViewController instead
[masterNavigationController showViewController:vc sender:sender];
return YES;
}
return NO;
}
The answer of #PeterOettl to his own question put me on the right way and is great for that. So the credit belongs to him.
I have nearly the same storyboard structure as him, but as vc is a navigationController I get a runtime error saying
'Pushing a navigation controller is not supported'
As said, that is because vc is the navigationController of the detail view and not the viewController of the detail view.
Note that I am surprised that #PeterOettl does not get that error in his case also, as the segue given in the storyboard picture, points to the navigation controller of the detail view.
Therefore the code should like that (in Swift) simply adding
let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController
and pushing detailViewControllerNavigationController instead of vc
and the whole code is
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
println("UISplitViewController collapsed: \(splitViewController.collapsed)")
if (splitViewController.collapsed) {
let master = splitViewController.viewControllers[0] as UITabBarController
let masterNavigationController = master.selectedViewController as UINavigationController
let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController
masterNavigationController.pushViewController(detailViewControllerNavigationController, animated: true)
return true
} else {
return false
}
}
Also note that this code is put in the AppDelegate.swift of the master-detail example of Xcode where a tab bar is added in the master view.
EDIT
In the comments we discussed with #PeterOettl of the difference between .pushViewController and .showViewController.
Apple documentation says :
showViewController:sender:
This method pushes a new view controller
onto the navigation stack in a similar way as the
pushViewController:animated: method. You can call this method directly
if you want but typically this method is called from elsewhere in the
view controller hierarchy when a new view controller needs to be
shown.
Available in iOS 8.0 and later.
I appreciate this discussion thread when I was implementing exactly the same UI structure app, and furthurmore made it adaptive for iPhone 6 Plus rotation and iPad multitasking (Slide Over/Split View, iOS 9 or later).
We have put the full solution (adaptive UISplitViewController with UITabBarController as primary view controller) open sourced on GitHub indievox-inc/TabBarSplitViewController. Thanks!
I implemented #Dreaming In Binary's answer in Swift:
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
let masterVC = splitViewController.viewControllers[0] as UITabBarController
if splitViewController.traitCollection.horizontalSizeClass == .Compact {
masterVC.selectedViewController?.showViewController(vc, sender: sender)
} else {
splitViewController.viewControllers = [masterVC, vc]
}
return true
}
func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController!) -> UIViewController? {
let masterVC = splitViewController.viewControllers[0] as UITabBarController
if let navController = masterVC.selectedViewController as? UINavigationController {
if navController.viewControllers.count > 1 {
return navController.popViewControllerAnimated(false)
}
}
return nil
}