I'm basically trying to create a custom UITabBarController since I need some specific functionality. The TabBar itself is done and working, but I don't quite know how to display ViewControllers in this CustomTabBarViewController itself.
Assuming i have the following method:
func tabSelected(_ index: Int) {}
and knowing the height of my TabBar through tabbar.frame.size, how do I instantiate two ViewControllers above the TabBar and switch between them when the tabSelected method is called? A transition animation would be even nicer, but not really necessary.
NOTE: my TabBar doesn't inherit from UITabBarController, only from the regular UIViewController, to avoid further confusion.
Here I created sample project:
CustomTabBarViewController
You should have container view for child ViewControllers
Then you should have array with embed ViewControllers
You should call method in
CustomTabBarViewController which change ViewController inside
container view to ViewController from array of VCs at index which you pass as parameter of this method
Start with declaring outlet collection for your TabBar buttons and also get reference for container view where your ViewControllers will be showed
#IBOutlet var tabBarButtons: [UIButton]!
#IBOutlet weak var container: UIView!
then create array for your tab bar items
var items: [UIViewController]?
next create lazy variables for your controllers
private lazy var aVC: A = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "a") as! A
}()
private lazy var bVC: B = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "b") as! B
}()
.... this can be simplified by creating method which returns ViewController depending on VC’s identifier
After that append ViewControllers to your items array and also each add as child of your TabBarViewController
override func viewDidLoad() {
super.viewDidLoad()
items = [aVC, bVC]
items!.forEach { addChild($0) }
}
continue with declaring method for setting ViewController
private func setViewController(_ viewController: UIViewController) {
items!.forEach { $0.view.removeFromSuperview(); $0.willMove(toParent: nil) }
container.addSubview(viewController.view)
viewController.view.frame = container.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
viewController.didMove(toParent: self)
}
now add action for your tab bar buttons and get index of button. Then with this index call your tabSelected method
#IBAction func buttonPressed(_ sender: UIButton) {
if let index = tabBarButtons.index(of: sender) {
tabSelected(index)
}
}
inside tabSelected set VC from items depending on index of sender tab bar button
func tabSelected(_ index: Int) {
if let item = items?[index] {
setViewController(item)
}
}
finally in viewDidLoad set first item
override func viewDidLoad() {
...
tabSelected(0)
}
Now you can fully customize your ViewController and make other epic stuff which you know from UITabBarController
Here's another approach:
1. In your CustomTabBarViewController define an array to hold the ViewControllers:
var viewControllers: [UIViewController]
Instantiate the view controllers and add them to the array:
// If you're not using storyboard:
let homeViewController = HomeViewController()
// If using storyboard:
let searchViewController = storyboard.instantiateViewController(withIdentifier: "SearchViewController")
viewControllers = [homeViewController, searchViewController, ...]
2. Define a variable to keep track of the tab button that is selected:
var selectedIndex: Int = 0
3. Implement your tabSelected method like so. I've explained each line in code:
func tabSelected(_ index: Int) {
let previousIndex = selectedIndex
selectedIndex = index
// Use previousIndex to access the previous ViewController from the viewControllers array.
let previousVC = viewControllers[previousIndex]
// Remove the previous ViewController
previousVC.willMove(toParentViewController: nil)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
// Use the selectedIndex to access the current ViewController from the viewControllers array.
let vc = viewControllers[selectedIndex]
// Add the new ViewController (Calls the viewWillAppear method of the ViewController you are adding)
addChildViewController(vc)
vc.view.frame = contentView.bounds
// contentView is the main view above your tab buttons
contentView.addSubview(vc.view)
// Call the viewDidAppear method of the ViewController you are adding using didMove(toParentViewController: self)
vc.didMove(toParentViewController: self)
}
Related
Is it possible to animate views of a parent VC in Swift?
I've got a root/master VC with a UIView which I'm using as a sort of a UITabBarController, so the rest of my 4 main VCs are children of the root.
On some of the child VCs, I have subviews that should take up the whole screen, without seeing the custom tab bar (UIView) from the root VC, but it still floats above.
I would like to have it slide off the screen via Y axis whenever I open the fullscreen subviews, but I can't seem to access or manipulate the root VCs properties as it returns nil on runtime.
Here's the custom tab bar root VC so you can understand the structure of the code:
class RootVC: UIViewController {
//This is where we pull all of our content from other VCs
//when a tab bar button is selected
#IBOutlet weak var contentView: UIView!
//The custom tab bar itself with an array of button outlets
#IBOutlet public weak var customTabBarContainer: UIView!
#IBOutlet var tabBarButtons: [UIButton]!
//4 main view VCs that are reflected in the tab bar
public var mapVC: UIViewController!
public var favoritesVC: UIViewController!
public var chatVC: UIViewController!
public var profileVC: UIViewController!
//Array for the VCs above
public var viewControllers: [UIViewController]!
//Index of the selected button determend by their tags
public var selectedIndex: Int = 0
#IBOutlet weak var loadingLogo: UIImageView!
override public func viewDidLoad() {
//Populating viewControllers array with
//initiated VCs in Main storyboard
let storyboard = UIStoryboard(name: "Main", bundle: nil)
mapVC = storyboard.instantiateViewController(withIdentifier: "MapVC")
favoritesVC = storyboard.instantiateViewController(withIdentifier: "FavoritesVC")
chatVC = storyboard.instantiateViewController(withIdentifier: "ChatVC")
profileVC = storyboard.instantiateViewController(withIdentifier: "ProfileVC")
viewControllers = [mapVC, favoritesVC, chatVC, profileVC]
//Custom tab bar + buttons visual properties
customTabBarContainer.layer.cornerRadius = customTabBarContainer.frame.height / 2
customTabBarContainer.layer.shadowColor = UIColor.darkGray.cgColor
customTabBarContainer.layer.shadowOffset = CGSize.zero
customTabBarContainer.layer.shadowRadius = 10
customTabBarContainer.layer.shadowOpacity = 0.9
tabBarButtons[0].imageView?.contentMode = .scaleAspectFit
tabBarButtons[1].imageView?.contentMode = .scaleAspectFit
tabBarButtons[2].imageView?.contentMode = .scaleAspectFit
tabBarButtons[3].imageView?.contentMode = .scaleAspectFit
}
override public func viewDidAppear(_ animated: Bool) {
loadingLogo.popOut()
//Loads the initial VC
contentView.addSubview(mapVC.view)
mapVC.view.frame = self.view.frame
mapVC.didMove(toParentViewController: self)
customTabBarContainer.isHidden = false
//Selects the inital home button
tabBarButtons[0].isSelected = true
}
#IBAction func didTabButton(_ sender: UIButton) {
//Keeps a track of which bar button is selected
let previousIndex = selectedIndex
selectedIndex = sender.tag
//Deselects the previous bar button
tabBarButtons[previousIndex].isSelected = false
//Removes the previous VC
let previousVC = viewControllers[previousIndex]
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
print("switced to \(viewControllers[selectedIndex])")
//Selects the tapped bar button
tabBarButtons[selectedIndex].isSelected = true
tabBarButtons[selectedIndex].popIn()
//Brings up the selected VC
let nextVC = viewControllers[selectedIndex]
contentView.addSubview(nextVC.view)
nextVC.view.frame = self.view.frame
nextVC.didMove(toParentViewController: self)
}
}
And here's the code I'm trying to use to manipulate the customTabBarContainer from a child of the MapVC:
UIView.animate(withDuration: 0.4, animations: {
let root = self.parent?.parent as! RootVC
root.customTabBarContainer.frame.origin.y -= root.customTabBarContainer.frame.height
}, completion: nil)
why are you trying to access the parent of the parent then?
self.parent?.parent as RootVC
assuming you are using an extension like this one to find your parentVC:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if parentResponder is UIViewController {
return parentResponder as! UIViewController!
}
}
return nil
}
}
you should be able to access parent via
let root = self.parentViewController as! RootVC
I've figured out an answer, just in case anyone else encounters a similar problem. It will not take you to the VCs immediate parent, but instead, to its most distant ancestor, which solves my particular problem in this case.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let rootVC = appDelegate.window?.rootViewController as! RootVC
rootVC.customTabBarContainer.isHidden = true
I'm trying to pass data from my main ViewController to another ViewController in a Tab Bar.
I have tried using the following code , and got an error Could not cast value of type 'Test.FirstViewController' to 'Test.ViewController'
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
let tab1Controller = self.tabBarController?.viewControllers?.first as! ViewController
print(tab1Controller.test)
}
I just used the following code which just worked fine for me on Xcode 9 with swift 4.0. The following method is declared in the View Controller class which just presents the First View Controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "sendingToTabBar" {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabVC = storyboard.instantiateViewController(withIdentifier: "tabVC") as! UITabBarController
self.present(tabVC, animated: true, completion: {
let vc = tabVC.selectedViewController as! FirstViewController
vc.dataLBL.text = self.dataTF.text!
})
}
}
You can access the tab bar controllers in your ViewController prepare method and set your values.
Prepare for segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let barViewControllers = segue.destination as! UITabBarController
let destinationViewController = barViewControllers.viewControllers?[0] as! FirstViewController
destinationViewController.test = "Hello TabBar 1"
// access the second tab bar
let secondDes = barViewControllers.viewControllers?[1] as! SecondViewController
secondDes.test = "Hello TabBar 2"
}
Then in your tab bar ViewControllers declare variables, you want to set the values to.
#IBOutlet weak var label: UILabel!
var test: String?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
label.text = test
}
FirstViewController
SecondViewController
You could do a number of things, what I would do is to just make a global variable so that both view controllers can access it. Another option is to give each view controller a separate global variable, and when the view is loaded, the variable is set to self, then make a variable that can be set by the other view controller.
example:
var data:Any?
viewDidLoad() {
viewControllerA = self
}
So im using this pod 'SwipeViewController' (https://github.com/fortmarek/SwipeViewController) that really gives me the effect that I want but the problem is that it only runs as the main navigation controller and I want it to work in a container view because I want to use it for this social app in the profile menu like twitter does with "My Tweets", "Likes", "Repost"...
so for example I need this...
https://camo.githubusercontent.com/f4eb2a8ba0a11e672d02a1ef600e62b5272a7843/687474703a2f2f696d6775722e636f6d2f5344496b6634622e676966
to work in here:
So just an explanation, to make the pod work you need to add this line of code to the appDelegate
let pageController = UIPageViewController(transitionStyle: .Scroll, navigationOrientation: .Horizontal, options: nil)
let navigationController = YourViewControllerName(rootViewController: pageController)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
wich creates a new window with the SwipeController, I need a way to make it work in a View controller.
I did something similar last night using the tutorial here:
https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
My associated code is below. I created an IBOutlet from the container view to my ViewController and then the code below adds the appropriate view controller to the container view depending on the setting of my Bool called buttonDefault. Make sure to add the child view to your container view and not the main view from the view controller.
#IBOutlet weak var containerView: UIView!
// MARK: Container View
// https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
lazy var remoteViewController: RemoteViewController = {
// Load Storyboard
let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "RemoteViewController") as! RemoteViewController
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController)
return viewController
}()
lazy var gestureRemoteViewController: GestureRemoteViewController = {
// Load Storyboard
let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "GestureRemoteViewController") as! GestureRemoteViewController
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController)
return viewController
}()
func add(asChildViewController viewController: UIViewController) {
// Add Child View Controller
addChildViewController(viewController)
// Add Child View as Subview
containerView.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = containerView.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
func remove(asChildViewController viewController: UIViewController) {
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
func updateView() {
let settings = Settings()
if settings.buttonDefault {
remove(asChildViewController: gestureRemoteViewController)
add(asChildViewController: remoteViewController)
} else {
remove(asChildViewController: remoteViewController)
add(asChildViewController: gestureRemoteViewController)
}
}
Once you add this just call updateView() in your viewDidLoad and any time the user selects a new option to view.
I hope this helps.
In various tutorials on how to use SegmentControllers, TabBarControllers, etc. it is configured such that the variable representing the view gets its value from an instantiation of the storyboard:
private lazy var summaryViewController: SummaryViewController = {
// Load Storyboard
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "SummaryViewController") as! SummaryViewController
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController)
return viewController
}()
Why does this code not just get an instance of SummaryViewController?
Adding an instance of a VC from your Storyboard, adds all of the logic and outlets you add in the storyboard. Let's say you have the following (obviously simple) VC:
class MyVC : UIViewController {
func viewDidLoad() {
}
#IBAction buttonPressed(sender : UIButton) {
/// Do something
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "mySegue"{
var vc = segue.destinationViewController as! WhateverViewController
}
}
}
where the buttonPressed: func is connected to a button in IB, and you also have a segue with a 'mySegue' identifier. Initializing your VC from the storyboard gives you access to all of these things. You absolutely can instantiate and push a VC, without the use of the storyboard, but you should not do so, when the VC you are pushing has wired IBOutlets, IBActions, etc.... If you want to do this in code, try the following:
let myNewVC = PushedViewController()
self.navigationController?.pushViewController(myNewVC, animated : true)
This will push the myNewVC onto your navigation stack, back button and all, and without using the storyboard.
I'm trying to add a series of UINavigationControllers to a single UIPageViewController as if adding swipe nav between pages. I did this with some success with standard view controllers, but I'm now trying to add nav controllers instead. Here's my Storyboard:
What I'd like to do is instantiate swipe nav (like Snapchat) between all pages in the 3rd row. Since they all lead to other view controllers, each is embedded within it's own nav controller. Here's my code:
class ViewController: UIViewController, UIPageViewControllerDataSource { // <-- Error here
// Sets up UIPageViewController. Must add to array count and instantiate new VCs.
var myNavControllers = Array(count: 3, repeatedValue:UINavigationController())
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let pvc = segue.destinationViewController as! UIPageViewController
pvc.dataSource = self // <-- Error here
let storyboard = UIStoryboard(name: "Main", bundle: nil);
var vc0 = storyboard.instantiateViewControllerWithIdentifier("Nav1") as! UINavigationController
var vc1 = storyboard.instantiateViewControllerWithIdentifier("Nav2") as! UINavigationController
var vc2 = storyboard.instantiateViewControllerWithIdentifier("Nav3") as! UINavigationController
var vc3 = storyboard.instantiateViewControllerWithIdentifier("Nav4") as! UINavigationController
self.myNavControllers = [vc0, vc1, vc2, vc3]
pvc.setViewControllers([myNavControllers[1]], direction:.Forward, animated:true, completion:nil)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UINavigationController) -> UINavigationController? {
var currentIndex = find(self.myNavControllers, viewController)!+1
if currentIndex >= self.myNavControllers.count {
return nil
}
return self.myNavControllers[currentIndex]
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UINavigationController) -> UINavigationController? {
var currentIndex = find(self.myNavControllers, viewController)!-1
if currentIndex < 0 {
return nil
}
return self.myNavControllers[currentIndex]
}
}
Problem is, I'm getting two errors. One at class: ViewController declaration: Type 'ViewController' does not conform to protocol 'UIPageViewControllerDataSource'. The other at pvc.dataSource = self, with error: Cannot assign a value of type 'ViewController' to a value of type 'UIPageViewControllerDataSource?'
Can what I'm trying to do be accomplished? If so, do I need to alter my code somewhere or achieve this an entirely different way?
The protocol you have defined is in ViewController but that should be defined in UIPageViewController class.
class PageViewController: UIPageViewController, UIPageViewControllerDataSource {
}
For this, you need to create a new class which should be associated with Page View Controller that you have taken on storyboard.