I'm developing an iOS app with swift in which I have a TabBarController with 5 tab bar items. All of them points to a navigation controller and then to a view controller. One of them I want to show a view controller without the tab bar and when the user press cancel it should go back to the previous tab bar item/view that was selected (previously - sorry for the redundancy). They are all linked/referenced by a "Relationship "view controllers" to "name of the view", but I don't have any specific segue or whatsoever.
This is the code for that specific "button" which I call in the viewDidLoad function:
func setupMiddleButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var menuButtonFrame = menuButton.frame
menuButtonFrame.origin.y = self.view.bounds.height - menuButtonFrame.height
menuButtonFrame.origin.x = self.view.bounds.width/2 - menuButtonFrame.size.width/2
menuButton.frame = menuButtonFrame
menuButton.backgroundColor = UIColor.white
menuButton.layer.cornerRadius = menuButtonFrame.height/2
menuButton.setImage(UIImage(named: "klein_fototoestel_2"), for: UIControlState.normal) // 450 x 450px
menuButton.contentMode = .scaleAspectFit
menuButton.addTarget(self, action: #selector(menuButtonAction), for: UIControlEvents.touchUpInside)
self.view.addSubview(menuButton)
self.view.layoutIfNeeded()
}
func menuButtonAction(sender: UIButton) {
self.selectedIndex = 2
}
I tried to perform the behaviour I want by delegating the tab bar controller with the following code but this function is never called when the central button is selected (though the correct view shows up..!):
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
print("the selected index is : \(tabBar.items?.index(of: item))")
}
What I really want to know is what is the correct way to implement that behaviour I want. Remembering that all views have a navigationController before. I read a lot of people suggesting using UserDefaults to store the index of the previous controller but to be honest I really don't think that's appropriate.
Any help is appreciated.
Thanks in advance.
I think you were on the right track - just need to get the correct connections.
In this diagram (it's kinda big - easier to read if you open it in a new tab), you see a "standard" UITabBar structure. The key is putting a default "do-nothing" view controller as the 3rd tab, and then adding a "special" view controller which will be loaded via code:
Then, your "action" function will look something like this:
func menuButtonAction(sender: UIButton) {
// Don't navigate to the tab index
//self.selectedIndex = 2
// instead, load and present the view you really want to see
if let vc = storyboard?.instantiateViewController(withIdentifier: "SpecialVC") as? SpecialViewController {
vc.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(vc, animated: true, completion: nil)
}
}
You can see and download a working example here: https://github.com/DonMag/SWTabsWithSpecialTab
Related
I have a simple VC:
class ViewController: UIViewController {
lazy var button = UIButton(frame: CGRect(x: view.frame.width/2-100, y: view.frame.height/2-25, width: 200, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "ViewController"
view.addSubview(button)
button.configuration = .tinted()
button.configuration?.title = "Click me"
button.configuration?.baseBackgroundColor = .systemPink
button.configuration?.baseForegroundColor = .systemPink
button.addTarget(self, action: #selector(click), for: .touchUpInside)
}
And the function for the button:
#objc
func click() {
let newVC = ViewController()
newVC.title = "ViewController 2"
newVC.view.backgroundColor = .systemCyan
var copyVCS = self.navigationController!.viewControllers
print("\n-----Copied Stack------\n\(copyVCS)")
copyVCS = copyVCS.dropLast()
copyVCS.append(newVC)
print("\n-----Mutated Stack------\n\(copyVCS)")
self.navigationController!.setViewControllers(copyVCS, animated: true)
print("\n-----New Navigation Stack-----\n\(navigationController!.viewControllers)")
}
Basically, I am testing a bug I have in a larger app I'm working on.
The issue is where I am setting the new navigation stack by calling
self.navigationController?.setViewControllers(copyVCS, animated: true)
Seems like the new navigation stack isn't the same as the copyVCS that I pass to the above method's argument.
The console after clicking the button:
-----Copied Stack------
[<VCBugReproduce.ViewController: 0x149d090f0>] // ✓
-----Mutated Stack------
[<VCBugReproduce.ViewController: 0x149f04440>] // ✓
-----New Navigation Stack-----
[<VCBugReproduce.ViewController: 0x149d090f0>, // ˟
<VCBugReproduce.ViewController: 0x149f04440>]
Is there a reason the new navigation stack isn't the same as the mutated stack? for some reason, the popped ViewController still appears in the navigation stack, but it appears now at the first index of the navigation stack array.
The docs say:
If animations are enabled, this method decides which type of
transition to perform based on whether the last item in the items
array is already in the navigation stack. (either a push or a pop...)
Only one transition is performed, but when that transition
finishes, the entire contents of the stack are replaced with the new
view controllers.
Is it that you're reading the value of viewControllers before the animation transition has completed, and that is before the value has in fact changed to the 'after animation' value.
In working on this app with a TabBar at the bottom, NavBar at the top with a Segmented Control:
I have an issue where the View A (Segment One) with a UITableView, upon selecting a cell and displaying a new view with more details, when I click back, the Segmented control at the top will disappear and the TableView from View A will be pushed up.
This doesn't always happen - sometimes after many tries or sometimes just one. I haven't found any correlation to what's causing it.
I have found that if I select View B from the segmented Control, then back to View A, then click on one of the table cells to get to the details screen and then click back, 100% of the time the Top Nav Bar disappears with the segmented control.
TabBarItemOneViewController
let segmentOneVC: SegmentOneViewController
let segmentTwoVC: SegmentTwoViewController
var currentViewController: UIViewController
let viewControllerYLoc = 60 // statusBarHeight + topBarHeight
let viewWidth = Helper.getViewWidth()
let tabBarHeight = 40
func pressedSegItem(segControl: UISegmentedControl){
let viewControllerHeight = Int(self.view.frame.height)
let viewFrame = CGRect(x: 0, y: viewControllerYLoc, width: Int(viewWidth), height: viewControllerHeight)
let selectedIndex = segControl.selectedSegmentIndex
previouslySelectedMyLoadsIndex = selectedIndex
self.currentViewController.removeFromParentViewController()
if(selectedIndex == 0){
currentViewController = segmentOneVC
}
else if(selectedIndex == 1){
currentViewController = segmentTwoVC
}
self.view.addSubview(self.currentViewController.view)
self.currentViewController.didMove(toParentViewController: self)
}
public init() {
segmentOneVC = SegmentOneViewController(nibName: nil, bundle: nil)
segmentTwoVC = SegmentTwoViewController(nibName: nil, bundle: nil)
if(previouslySelectedIndex == 0){
currentViewController = segmentOneVC
}
else{
currentViewController = segmentTwoVC
}
super.init(nibName: nil, bundle: nil)
self.calculateItems()
self.addSegmentedControl()
let viewControllerHeight = (Int(self.view.frame.height) - viewControllerYLoc) - tabBarHeight
let viewFrame = CGRect(x: 0, y: viewControllerYLoc, width: Int(viewWidth), height: viewControllerHeight)
self.currentViewController.view.frame = viewFrame
self.addChildViewController(segmentOneVC)
self.addChildViewController(segmentTwoVC)
self.view.addSubview(self.currentViewController.view)
self.currentViewController.didMove(toParentViewController: self)
}
SegmentOneViewController (note: SegmentTwoViewController is identical)
let cellReuseIdentifier = "ItemDetailTableViewCell"
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let row = indexPath.row
let dataItem = self.dataArray[row]
let itemDetailVC = ItemDetailViewController()
itemDetailVC.dataItem = dataItem
self.present(itemDetailVC, animated: true, completion: nil)
}
func addTableView(){
self.tableView = UITableView()
tableView.register(ItemDetailTableViewCell.self, forCellReuseIdentifier: self.cellReuseIdentifier)
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
tableView.frame = CGRect(x: 0, y: 0, width: Int(viewWidth), height: (Int(self.view.frame.height) - bottomOfTopNavBar) - heightOfTabBar)
self.view.addSubview(tableView)
}
override func viewDidAppear(_ animated: Bool){
super.viewDidAppear(animated)
loadData()
tableView.dataSource = self
tableView.delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
addTableView()
}
ItemDetailViewController
// Connected to a back button in a top Navigation Bar
func goBack(){
self.dismiss(animated: false, completion: nil)
}
Nice graphics BTW... that illustration make it much easier to understand your problem.
Also BTW, I'm an Obj-C person, still learning the nuances of Swift, so please let me know if my syntax or otherwise is incorrect. I'm also relatively inexperienced in using container VC's.
I've written my response in two parts.
The First Part is my attempt to solve your problem.
The Second Part is my suggestion for an alternative for you to consider.
First Part
This is my understanding of the order/sequence of execution in your code...
Parent View with Segmented Control
public init () : on instantiation of the parent view controller, two child VCs (segmentOneVC and segmentTwoVC) are instantiated and depending on previous selection are assigned as currentViewController. Then you add a segmented control to the TabBarItemOneViewController.
User taps a segmented control.
Depending on user input, either the SegmentOneViewController or SegmentTwoViewController view is added as a subview to the TabBarItemOneViewController.view. (Note that this is also done when the VC is initialised.)
Child View
override func viewDidLoad() : once the view did load, you call the function addTableView.
func addTableView() : in this custom function you instantiate your table view and place it within the SegmentOneViewController, which is itself I assume a UIViewController.
override func viewDidAppear(_ animated: Bool) : you call the custom function loadData and set your table view data source and delegate.
Back Button
User taps the back button.
Child VC is dismissed and the TabBarItemOneViewController becomes the active view on screen.
Let's look at what does not happen in the view controller lifecycle when the back button is pressed... Item 1 in the list.
This may explain the inconsistency.
Try this... run the app, tap the tab control to take you to TabBarItemOneViewController. Don't tap the segmented control. Tap a line in your table view. Tap the back button in your child VC. I'd take a guess your segmented control is still there.
Now try this... run the app, tap the tab control to take you to TabBarItemOneViewController. Tap the segmented control. Tap a line in your table view. Tap the back button in your child VC. I'd take a guess your segmented control is no longer there.
Why? Because the custom function pressedSegItem that I assume has a target action assigned to the segmented control, will overwrite your init, which is where you add the segmented control into the tab bar view controller.
So by way of example, try placing the code to instantiate your segmented control instead in an override function of viewWillAppear of the TabBarItemOneViewController VC.
So a couple of concepts to think about...
lazy loading to save memory allocation - only instantiate the objects you need when the user specifically requests that function in the app;
order of execution of each function in the UIViewController lifecycle; and
which functions are executed once and which are executed each time your view becomes first responder / the active view.
Some reading recomendations:
This SO question titled :Looking to understand the iOS UIViewController lifecycle" presents a lot of good information, but understand that some of the information is incorrect due to deprecation of viewDidUnload from iOS 6.
Which is why you should always go to the Apple documentation for UIViewController to refer to the latest API reference.
Second Part
By providing this alternative, I'm not suggesting that your approach is incorrect, however I am suggesting an alternative for you to consider.
I've always used tab bar controllers to change views and segmented controls to filter data sets or change the appearance of the current view.
Think about using a UISegmentedControl to manage or adjust the data set within only one table view. This will alleviate the need for multiple view controllers and the juggling act of managing these.
For example, when writing your data source and delegate methods / functions for your tabel view, you can include the following code to ensure the table view loads and responds accordinagly:
let selectedIndex = segControl.selectedSegmentIndex
if(selectedIndex == 0) {
rowHeight = 20 'for example
} else {
rowHeight = 30 'for example
}
Then you'd need to relaod your table view to effect the changes.
I've created a UINavigationController with a UIToolbar. Inside the UIToolbar there are multiple UIBarButtonItems. The UIToolbar has a subclass which i use to set the toolbar settings and create the UIBarButtonItems.
By pressing a UIBarButtonItem I want to navigate to another ViewController. As you can see in the code below, I've created a function for .addTarget, called "settingsPressed".
//SetToolbar
class ToolbarClass: UIToolbar {
//Set height of toolbar
override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
size.height = 60
return size
}
//Toolbar settings
override func layoutSubviews() {
super.layoutSubviews()
//Default
self.isTranslucent = false
self.barTintColor = UIColor(red: 48/255, green: 148/255, blue: 172/255, alpha: 1)
//Buttons
//Settings
let settingsBtn = UIButton()
settingsBtn.frame = CGRect(x: 0, y: 0, width: 46, height: 46)
settingsBtn.setImage(UIImage(named: "Settings-Button")?.withRenderingMode(.alwaysOriginal), for: .normal)
settingsBtn.addTarget(self, action: #selector(self.settingsPressed), for: .touchUpInside)
let settingsButton = UIBarButtonItem()
settingsButton.customView = settingsBtn
self.setItems([settingsButton], animated: false)
}
func settingsPressed() {
//How to navigate to a viewcontroller?
}
}
I've found some swift codes to navigate to another viewcontroller, but these codes don't work in my situation because i'm using a subclass. In this case the ".self.storyboard?" doesn't make sense:
let secondViewController = self.storyboard?.instantiateViewController(withIdentifier: "ClassesOverviewViewController") as! ClassesOverviewViewController
self.navigationController?.pushViewController(secondViewController, animated: true)
Your implementation breaks MVC principles and causes the problems which you shouldn't face at all.
You should not add controller logic (creating new VC and navigating to it) into view (UIToolbar and it's subclass are view elements).
First way to fix it: add UIToolbar to storyboard view with added settings bar button item, connect action of bar button item with function from your VC, implement this function to navigate.
Second way to fix it: leave subclass of UIToolbar (it is not preferable if you only adding bar button item in subclass), declare public property for settings bar button item, use target and action to set your VC as target and function from VC as action for settings bar button item, implement this function to navigate.
I am working on a small project with a few separate view controllers on my storyboard, and have have created a button the shows a UITableViewController, but I needed a way for the user to get back to the home page. I have created a button on top of the MainTableVC, and when I run the app, it shows the button. What I need is for the button to simply show my main view controller which is called just view controller. I am still learning so every step forward is a leap for me. Here is the code of the viewDidLoad, where I created the button.
let button1 = UIButton(frame: CGRect(origin: CGPoint(x: self.view.frame.width / 2 - 25, y: self.view.frame.height - 70), size: CGSize(width: 50, height: 50)))
button1.backgroundColor = UIColor.blue
self.navigationController?.view.addSubview(button1)
I also do have a UITableViewController listed in the class. Thank you everyone for your help, I appreciate it. So to summarize, I need to connect the button1 I created to the viewcontroller, (because you can not just put a button on top of a tableview and then just control click to the other view and set "show". That actually makes it easier to say, I need to code the aspect of simply cntrl dragging from one button to a view and setting "show". Thanks so much everyone.
As much i understood.. you can manually create a button in your MainViewController using and add Target To it
let button1 = UIButton(frame: CGRect(origin: CGPoint(x: self.view.frame.width / 2 - 25, y: self.view.frame.height - 70), size: CGSize(width: 50, height: 50)))
button1.backgroundColor = UIColor.blue
button1.addTarget(self, action: #selector(MainViewController.actionOpenTableView(_:)), forControlEvents: .TouchUpInside)
self.navigationController?.view.addSubview(button1)
Also create function actionOpenTableView in Your class capturing the
Target of button 1 where you can push/present TableViewController(:here MyTableViewController) using segue or programmically.
func actionOpenTableView(btn:UIButton){
//if using segue of motherboard
// self.performSegueWithIdentifier("MoveToTableView", sender: self)
//if using programmically
let tableVC = self.storyboard?.instantiateViewControllerWithIdentifier("MyTableVC")as! MyTableViewController
self.navigationController?.pushViewController(tableVC, animated: true)
}
if using pprogrammically make sure you provide StoryboardID to your TableviewController in Storyboard.
once you get on your TableViewController you can either use default back button.. or create custom method as follow for getting back to your MainViewController from MyTableViewController
#IBAction func mBackActionBtn(sender: AnyObject) {
self.navigationController?.popViewControllerAnimated(true)
}
Firstly, you shouldn't add a button directly to your navigation controller. If you want to add a custom button, instead use the following:
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Title", style: .plain, target: self, action: #selector(navButtonPressed))
You can then implement the method for the action:
func navButtonPressed() {
// if pushed onto nav vc:
// _ = self.navigationController?.popViewController(animated: true)
// if presented modally (as suggested in your comment:
self.dismiss(animated: true, completion: nil)
}
I'm building an app in which a part of the app should list all the shops in our town and show some details about them. This part works, but it relies on a split view controller, as you can see in these pictures . I also added a video of the problem.
I didn't know how to use the split view controller, really.. So what i did was the following: I set the split view controller as the initial view controller, and connected the navigation controller which should open up first as the detail view controller. The first navigation controller of the table view is set as master view controller.
The problem now is that when i start the app, i arrive at the homepage (which is good, check the video in the drive), but in the upper left corner, you can see that there is a navigation button to the table view. Is there a way to delete that button, and make my homepage navigation controller the initial view controller again?
I guess I'd have to link the split view controller differently, set the first view controller to initial view controller again, and add a segue to the split view controller, but i don't know what that segue should look like or how I should program it. There's a segue to the first view controller of the table view right now.
In my homepage view controller, this is the code for the segue pushing to the first view controller of my table view right now:
func pushRegisterViewShoppen()
{
self.performSegueWithIdentifier("SegueShoppen", sender: self)
}
let shoppen = UIButton(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
shoppen.setTitle("Shoppen", forState: .Normal)
shoppen.setTitleColor(UIColor.whiteColor(), forState: .Normal)
shoppen.addTarget(self, action: #selector(ViewController.pushRegisterViewShoppen),
forControlEvents: .TouchUpInside)
let BergStraatFoto = UIImage.init(named: "Bergstraat")
shoppen.setBackgroundImage(BergStraatFoto!, forState: .Normal)
tempView.addSubview(shoppen)
This is the prepareForSegue in the tableViewController:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let winkel: Winkel
if searchController.active && searchController.searchBar.text != "" {
winkel = filteredWinkels[indexPath.row]
} else {
winkel = winkels[indexPath.row]
}
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.detailWinkel = winkel
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
controller.navigationItem.setHidesBackButton(false, animated: true)
}
}
}
Does anyone know how i could fix this? Thanks in advance!
Try this UISplitViewController's delegate method, the detail view controller gets displayed as there is not much space in the iPhone's portrait view so you need to override that using the below delegate method.
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController:UIViewController, ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
//handle it efficiently to decide based on certain conditions.
return true
}
Try this thread which elaborates more on the problem.