I have an app with a custom tab bar controller, and a data model powered by a stateController. I want to share data from my model across the tabs.
Few callouts:
My app uses both storyboards & programmatic (Swift 5). Assume storyboard here
When the app loads, I check userDefaults to see if it's the first app launch. If it is, firstTimeVC is the rootVC
If it's not, my Tab Bar is the rootVC. And this logic lives in the Scene Delegate
I'm using Dependency Injection to create a shared instance of my data model across tabs. People suggest injecting the data model into my TabBar & childVCs using the App / Scene Delegate. Problem is, the TabBar isn't always the rootVC.
My questions are:
Can I inject my data model into the TabBar's child VCs directly from the TabBar? Instead of the Scene Delegate?
If I use Scene Delegate, then how do I inject the data model into the TabBar's childVCs when the TabBar is NOT the rootVC (ie first app launch)
Or should the TabBar always be the rootVC, and I move the first-launch-check to a childVC instead of Scene Delegate
What's the right way to do this?
My first time app launch check (currently inside Scene Delegate):
if isFirstLaunch {
rootViewController = "FirstTimeUserViewController"
} else {
rootViewController = "TabBarController"
}
window?.rootViewController = rootViewController
window?.makeKeyAndVisible()
My dependency injection (currently inside Scene Delegate):
let tabBarController = storyboard.instantiateViewController(withIdentifier: "CustomTabBarController") as! CustomTabBarController
for child in tabBarController.viewControllers ?? [] {
if let top = child as? StateControllerProtocol {
top.setStateController(stateController: stateController)
}
}
A screenshot of my storyboard & VC setup:
Related
My app launches with an initial view controller (lets call it as StartVC). Now when user presses a continue button, I am presenting a navigation stack (lets call it as RegisterVC) on top of StartVC. This navigation stack will contain 5 view controllers which I am pushing on it whenever user moves forward with button actions. After the 5th view controller, I am starting a new navigation stack (lets call it as LoginVC).
Now my use case is I dont want the StartVC & RegisterVC to reside in the memory as they are of no use once user has completed his registration. In order to achieve this, I am changing the AppDelegate window's root view controller to LoginVC
Below are the options which I tried on the 5th view controller of RegisterVC:
1) Changing the keywindow
UIApplication.shared.keyWindow?.rootViewController = LoginVC
UIApplication.shared.keyWindow?.makeKeyAndVisible()
2) Changing the window
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = LoginVC
appDelegate.window?.makeKeyAndVisible()
3) Making the previous root view controller as nil before assigning a new one.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = nil
appDelegate.window?.rootViewController = LoginVC
appDelegate.window?.makeKeyAndVisible()
4) I also tried the above options directly from the AppDelegate instead of doing it from the 5th view controller.
With all the above options, I tried debugging by looking at deinit on all view controllers, but none of them got deallocated. Also, I can see that 5th view controller under LoginVC in the xcode Debug View Hierarchy.
Because of not removing them from memory, the actual problem which I am facing is after presenting the LoginVC, I have a view controller whose background color alpha is less. Because of this I am seeing the RegisterVC 5th view controller underneath it.
Any help on this appreciated...
I think the rootViewController setting is not the problem. Perhaps you have a retain cycle in your other view controllers that stops them from being deallocated.
There are many ways you could accidentally do this (capturing strong references to self in blocks, not marking delegates or other back references as weak, etc).
You might be able to figure it out with Instruments. Here's a tutorial: http://samwize.com/2016/05/30/finding-retain-cycle-with-instruments/
When presenting or dismissing VC, I do not want to keep hiding and showing tabBar because it creates a poor user experience. Instead, I want present the next VC straight over the tab bar such that when I dismiss the nextVC by dragging slowly from left to right, I can see the tabBar hidden behind the view (As shown in image below)
Note, my app has two tabs with two VCs(VCA,VCB) associated to it. Both VC also have navigation bar embedded. VCA segues to VCA1 and VCB segues to VCB1. At the moment, inside VCA and VCB I am calling the following function to segue with some hiding and unhiding done when viewWillappear (Code below).
self.navigationController?.showViewController(vc, sender: self)
// Inside ViewWillAppear Only reappear the tab bar if we successfully enter Discover VC (To prevent drag back half way causing tab bar to cause comment entry to be floating). This code check if we have successfully enters DiscoverVC
if let tc = transitionCoordinator() {
if tc.initiallyInteractive() == true {
tc.notifyWhenInteractionEndsUsingBlock({(context: UIViewControllerTransitionCoordinatorContext) -> Void in
if context.isCancelled() {
// do nothing!
}
else {
// not cancelled, do it
self.tabbarController.tabBar.hidden = false
}
})
} else {
// not interactive, do it
self.tabbarController.tabBar.hidden = false
}
} else {
// not interactive, do it
self.tabbarController.tabBar.hidden = false
}
----------Working solution from GOKUL-----------
Gokul's answer is close to spot on. I have played with his solution and came up with the following improvement to eliminate the need to have a redundant VC and also eliminate the initial VC being shown for a brief second before tabVC appears. But without Gokul, I would never ever come up with this!!
Additionally, Gokul's method would create a bug for me because even though I do have a initial "normal" VC as LoginVC before tabVC is shown. This loginVC is ONLY the rootVC if the user needs to login. So by setting the rootVC to tabVC in most cases, the navVC will never be registered.
The solution is to embed navigation controller and tabBar controller to one VC. But it ONLY works if the navVC is before the TabBarVC. I am not sure why but the only way that allowed me to have navVC-> tabVC-> VC1/VC2 is to embed VC1 with a navVC first than click on VC1 again to embed tabVC (It wouldn't allow me to insert one before tabVC and I also had to click the VC1 again after embedding the NavVC).
For your requirement we need to make some small changes in your given view hierarchy
Let me explain step by step,
To meet your requirement we have to add a UIViewController(let's say InitialVC) embedded with a UINavigationController and make it as initial viewcontroller.
Then add a UITabbarController with 2 VC (VCA,VCB) // IMPORTANT: Without any navigationcontroller embedded.
Add a segue between InitalVC and TabbarController with an unique identifier(ex: Initial)
In viewWillAppear of InitalVC perform segue as below (InitialVC is unnecessary to our design we are using this just to bridge navigationController and tabbarController).
self.performSegueWithIdentifier("Initial", sender: nil)
In TabbarControllerclass hide your back button, this ensures that InitialVC is unreachable.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true
}
Now add a segue from a button between VCA and VCA1, thats it build and run you will see VCA1 presenting over VCA's tabbar.
What we have changed?
Instead of adding UINavigationController inside UITabbarController we have done vice versa. We can't directly add Tabbar inside navigation to do that we are using InitialVC between them.
Result:
1st way is create a image of the tabbar using UIGraphicsGetImageFromCurrentImageContext and set it on the bottom of the other view...
2nd way is show the next view in another new window that is above the tabbar, that way you wont need to hide the tabbar anymore, but seems like its in the navigation controller so this way doesnt seems available
Hiding and unhiding the tab bar is unnecessary. You only need to embed the UITabBarController inside the UINavigationController. That is, UINavigationController as the initial vc, UITabBarController as the root vc of UINavigationController.
I had to add a navigation controller to my app so that I could use the left drawer menu (using SWRevealViewController) but its messing up my segues. My initial design had a login screen that segued to one of 4 different scenes depending on an a status indicator.
Now that I had to add a navigation controller it looks like i'll have to take the user sequentially through through each screen in the stack until they reach the relevant one. Is there a way I can jump past the first screen or 2? Or a way to not show them as I navigate through.
I tried putting the performSegue in the viewWillLoad delegate method but the screen still loads before segueing to the next scene.
Add Storyboard IDs to all View/Navigation Controllers that will eventually be pushed:
Now to push the desired view it depends whether your current View Controller stands: within or outside the Navigation Controller's stack:
If your VC is already in the Navigation Controller stack
From your current ViewController push the desired view:
if let myViewController = self.storyboard?.instantiateViewControllerWithIdentifier("myViewController") as? MyViewControllerClassName {
self.navigationController?.pushViewController(myViewController, animated: true)
}
Note: as? MyViewControllerClassName is only required if your View Controller's class is not the default UIViewController but a custom one that extends it instead.
If your VC is NOT in the Navigation Controller stack
Same principle apply, only this time you need to push the Navigation Controller itself before pushing the desired View Controller:
if let newNavController = self.storyboard?.instantiateViewControllerWithIdentifier("myNavigationController") as? UINavigationController {
self.view.window?.rootViewController = newNavController
// Now push the desired VC as the example above, only this time your reference to your nav controller is different
if let myViewController = self.storyboard?.instantiateViewControllerWithIdentifier("myViewController") as? MyViewControllerClassName {
newNavController.pushViewController(myViewController, animated: true)
}
}
i have an app with a signin viewController which modally presents a tabBarController containing several tabs. Each tab has a navigationController and a stack of views. One of those tabs is for settings and has a logout button.
when the user presses the logout button I would like to dismiss all navigation stacks of all tabs and the tabBarController and go back to the initial login viewController. Essentially I want to restore the app to the initial state. Was wondering what the best practice would be to achieve this.
thanks
If you want to reset all of the tabs and return the apps to it's initial state after logging out, all you have to do is reset the UITabBarController's viewControllers property.
So if you are subclassing UITabBarController the following code should restore your app to its original state.
self.viewControllers = #[self.viewControllerOne, self.viewControllerTwo, self.viewControllerThree];
From the documentation:
If you change the value of this property at runtime, the tab bar controller removes all of the old view controllers before installing the new ones. The tab bar items for the new view controllers are displayed immediately and are not animated into position.
This is the code I currently use in the app I am working on to move between the "Login" and "Main UI"
let toViewController = // Login view controller here
let fromView = UIApplication.sharedApplication().keyWindow!.rootViewController!.view
UIApplication.sharedApplication().keyWindow?.rootViewController = toViewController
let toView = toViewController.view
toView.addSubview(fromView)
UIView.animateWithDuration(0.38, delay: 0.2, options: [], animations: {
fromView?.transform = CGAffineTransformMakeTranslation(-UIScreen.mainScreen().bounds.width, 0)
}) { finished in
fromView.removeFromSuperview()
}
iOS 11 - Swift 4
Building up on jstn's answer that worked great for me. I've only mapped the VCs to embed them in navigation controllers.
let rootViewController = UIApplication.shared.keyWindow?.rootViewController
guard let tabController = rootViewController as? TabController {
return
}
//create all your vcs here
// ...
// ...
let vcs = [vc1, vc2, vc3, vc4]
tabController.viewControllers = vcs.map {UINavigationController(rootViewController: $0)}
// Do additional clean up here (i.e. clean cache, UserDefaults, userData objects etc.)
I've got an app with a mapViewController embedded in a navController. In the mapVC ive got a single bar button item which when clicked I want to conditionally "push" segue to one of a number of different view controllers. To achieve that ive set up an ibaction on the button and have the conditional "performSegueWithIdentifier" code in the relevant buttons ibaction method ie
#IBAction func addButtonClicked(sender: UIBarButtonItem) {
let lastAdd = "addItem"
if lastAdd == "addItem"{
self.performSegueWithIdentifier("addItem", sender: self)
} else {
self.performSegueWithIdentifier("addEvent", sender: self)
}
}
this will take me to either the addItemVC or the addEventVC. in each of those viewControllers (ie the addItemVC and the addEventVC) I want to have a segmented control in the navigation bar which, when clicked, will take me to the alternative VC ie if addItemVC is currently displayed, and the addEvent section of the segmented control is clicked, I want to display the addEventVC. Im following Red Artisans page on how to do this but in his example he is instantiating all view controller options upfront in the app delegate and so can easily get reference to each view controller and link it to the clicked segement of the segmented control within his rootVC
Where im confused is .. seeing Im using conditional code before performing each segue, i assume that im only instantiating one viewController at a time when the bar button item is pressed. So how can i get an array of view controllers to pass to the VC im segueing to so that i can create the required segmented control in that VC. I assume i could manually create the destination VC array in my mapViewController and pass these across but wouldnt that mean im instatiating a different instance to the ones automatically created by the segue process?
Yes, you are right: if you manually create the two VCs in your mapViewController, they will be different instances from those created by a segue. So if you want to stick with Red Artisan's solution, present the VCs using code rather than segues. You can still design the two VCs in your storyboard, give them each a unique identifier and then use the instantiateViewControllerWithIdentifier function of self.storyboard to create the instances.
You can use most of Red Artisan's app delegate code in your mapViewController, but with a few tweaks: eg. to use the existing navigation controller (in which your mapViewController is embedded), and the [window ...] lines are superfluous. The thing to watch out for will be the indexDidChangeForSegmentedControl function, which assumes that the VCs you are switching between are the rootViewControllers for the navigation controller (ie. that they are the only item in the navigation controller's viewControllers array). In your case you have mapViewController as (I assume) the rootViewController, so you will have to amend the indexDidChangeForSegmentedControl function to create an array with the mapViewController at index 0 and the relevant (addItem or addEvent) VC at index 1. I don't know how well this method will animate, nor whether back buttons etc will be properly set.
If you want to stick with segues, there are a couple of solutions: one would be to use a UITabBarController (and hide the tabBar). You would have the addItem and addEvent VCs as separate tabs, and when you segue to the tabBarController, you could set which tab is selected. But my preferred solution would be to segue to a UIPageViewController. You would could either create the VCs in mapViewController and pass them as part of the segue, or just pass an indicator as to which was selected, and have the pageViewController instantiate them and present the relevant one. You could then use the UISegmentedControl to trigger switching between VCs. See this answer for something similar.
thanks pbasdf for your detailed instructions. its taken me quite a while but i seem to be close to getting it working. i followed most of your instructions and you were spot on with what you said.
first from the mapVC on press of the + bar button item i create the addItem and addEvent VCs using instantiateViewControllerWithIdentifier and create the segmented control.
#IBAction func addButtonClicked(sender: UIBarButtonItem) {
//at this stage just manually set default target VC
let lastAdd = "addItem"
//get an array of the target viewcontrollers
var viewControllers = segmentViewControllers()
//initz the segmentscontoller with the current navcontroller if doesnt already exist
if segmentsController == nil {
segmentsController = SegmentsController(navController: self.navigationController!, viewControllers: viewControllers)
segmentedControl.addTarget(segmentsController, action: "indexDidChangeForSegmentControl:", forControlEvents: UIControlEvents.ValueChanged)
//add the segmented control to the VC by setting first user experience which calls indexdidchangeforsegmentedcontrol
firstUserExperience()
}
segmentsController?.indexDidChangeForSegmentControl(segmentedControl)
}
//create an array of the target view controller. called from addbutton clicked
func segmentViewControllers() -> [UIViewController] {
//create an instance of the viewcontrollers
let addItemVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("AddItemVC") as ViewController
let addEventVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("AddEventVC") as ViewController
var viewControllers = [addItemVC, addEventVC]
return viewControllers
}
in the segmentsController i created 2 arrays, 1 to hold the different VCs and one to hold the navigation stack. i also set the passed the segmented control to the incoming VCs
class SegmentsController: NSObject {
var navController: UINavigationController?
var viewControllersOptionsArray: [UIViewController] = []
var viewControllersNavArray: [UIViewController] = []
//MARK: - INITIALIZER
init(navController: UINavigationController, viewControllers: [UIViewController]) {
self.navController = navController
self.viewControllersOptionsArray = viewControllers
}
//MARK: SEGMENT INDEX METHOD
func indexDidChangeForSegmentControl(segmentedControl: UISegmentedControl) {
var index = segmentedControl.selectedSegmentIndex
var incomingViewController = viewControllersOptionsArray[index]
//set the viewControllersNavArray
if let mapVC = navController?.viewControllers[0] as? MapViewController {
viewControllersNavArray = [mapVC, incomingViewController]
}
//set the navcontroller with a new array of viewcontrollers
navController?.setViewControllers(viewControllersNavArray, animated: true)
//set the title of the incoming view controller
incomingViewController.navigationItem.titleView = segmentedControl
//set the seg control variable of the incoming VCs
if let iVC = incomingViewController as? AddItemViewController {
iVC.segmentedControl = segmentedControl
} else if let iVC = incomingViewController as? AddEventViewController {
iVC.segmentedControl = segmentedControl
}
}
}
In the addItem and addEvent VCs i figured i needed to pass the current selectedSegmentIndex back to the mapVC if the user presses the back button - wasnt sure how to do this and ended up using an extension i downloaded called UIViewController+BackButtonHandler to handle it.
Im sure my code could be much better written but the only thing that im still having trouble with is that i want the VC transitions to be animated. the navigation seems to work fine if i set animated to false in the navController?.setViewControllers(viewControllersNavArray, animated: true) line but if i set it to true, the segmentedControl briefly flashes an appearance on the nav bar of the incoming VC but then disappears. Its still there so i can still navigate but you cant see it. i figure that it has something to do with setting it before the view has properly loaded but even if i put the code to set it ie
incomingViewController.navigationItem.titleView = segmentedControl
in the new top VC in viewDidLoad or viewDidAppear i still get the same problem. i also thought i might be able to fix it if i could put the incomingViewController.navigationItem.titleView = segmentedControl code in a completion block but the setViewControllers method doesnt appear to have a completion handler.
Any suggestions?