Unwanted behavior of setViewControllers in a UINavigationController - ios

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.

Related

Why back button is not working to return back from ViewController?

I am using SecondView programmatically. I click the button in ViewController to open SecondView controller, but now I want to back to ViewController from SecondView. I do not have storyboard in SecondView and I want to click the closeButton to go back to ViewController. My code work but when I click the close button it does not work. Any idea?
import UIKit
class SecondView: UIViewController {
var closeButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
closeButton.addTarget(self, action: #selector(dismissActionSheet), for: .touchUpInside)
}
#objc func dismissActionSheet() {
self.navigationController?.popViewController(animated: true)
}
}
You are instantiating a new UIButton, adding a target, but never adding the button to the UI. If you are seeing a button in your UI, it must be one that you added in Interface Builder, not the one you reference here. I suspect your intent was not:
var closeButton = UIButton()
but rather:
#IBOutlet var closeButton: UIButton!
… and hook that button’s outlet in IB.
Or better, rather than adding a target in viewDidLoad, hook up an #IBAction for the button directly in IB:
#IBAction func didTapButton(_ sender: Any) {
navigationController?.popViewController(animated: true)
}
You don't appear to need the closeButton outlet (at least for what you have shared with us thus far).
By the way, popViewController only works if this is embedded in a navigation controller. If you are not using a navigation controller (i.e., you “presented” the view controller rather than “pushing” one), the navigationController will be nil (and navigationController?.popViewController will do nothing). If not using navigation controller, call dismiss rather than popViewController.
If you have created the Second view programmatically:
Why not set a frame to the button and add it to self.view?
eg:
let button = UIButton(frame: CGRect(x: 50, y: 100, width: 80, height: 40))
button.setTitle("Pop VC", for: .normal)
button.addTarget(self, action: #selector(btnTouched), for: .touchUpInside)
self.view.addSubview(button)
then try to pop back:
func btnTouched(sender: UIButton!) {
if let navController = self.navigationController {
navController.popViewController(animated: true)
}
}

Setting UINavigationItem rightBarButtonItems when pushing a viewcontroller into a navigation stack

Just like the title says, I'm trying to replace the rightBarButtonItems for a view controller when pushing a new controller using the navigation view controller. Weirdly, sometimes this works just fine and sometimes not at all.
Swift 4.2, XCode 10.3
The code
Push the VC:
DispatchQueue.main.async {
guard let vc = UIStoryboard(name: "ExampleStoryboard", bundle: Bundle.main).instantiateViewController(withIdentifier: "ExampleViewController") as? ExampleViewController else {
return
}
vc.data = Data(JSON: newDataJson)
vc.delegate = self
vc.images = self.images
self.navigationController?.pushViewController(vc, animated: true)
}
Set the barButtonItems:
override func viewDidLoad() {
super.viewDidLoad()
// [Some other stuff here...]
let btn = UIButton(type: .custom)
btn.setImage(#imageLiteral(resourceName: "icon-image"), for: .normal)
btn.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
btn.imageEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
btn.addTarget(self, action: #selector(performAction), for: .touchUpInside)
self.navigationItem.rightBarButtonItems = [UIBarButtonItem(customView: btn)]
}
}
I've read as many questions regarding this topic on SO and various other websites as well as the developer documentation for navigationItem and UINavigationViewController and anything else I could think of. So far I haven't been able to find anything to solve this.
I've tried putting the bar button code in viewDidLoad, viewWillAppear, viewDidAppear, didMoveTo(parent:)... I also tried putting it in a button in the pushed VC and no dice. But this has worked in this exact situation before on previous builds.
The rootVC has rightBarButtonItems which would be overridden by this. I tried setting the parent items as well.
I've also tried this with/without DispatchQueue.main.
I was able to confirm through debugging that the navigationItem does not match the root navigationItem when this is attempting to be set.
Any further info about what might be happening or how to work around/fix this would be very appreciated.

TabBar to view and when closed come back to previous selected view

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

viewDidAppear() called before view controller is pushed onto navigation stack

I have a basic navigation setup in my Storyboard: a vanilla UIViewController embedded in a UINavigationController. In my main VC I have two buttons that each segue to a UIViewController subclass: LabelledVC. In the subclass's viewDidAppear(_:) method I set the navigation item's titleView to a custom image:
class LabelledVC: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let logoImage = UIImage(named: "apple")
let logo = UIImageView(image: logoImage)
logo.contentMode = .scaleAspectFit
logo.frame = CGRect(x: 0, y: 0, width: 32, height: 32)
navigationItem.titleView = logo
}
}
For some reason LabelledVC's viewDidAppear(_:) method is being called when the app loads (before it is pushed onto the navigation stack) which doesn't make any sense to me. You can find the project here.
Your MainVC is inherit from LabelledVC. So when application did show this controller the system calling viewDidAppear in ViewController but you don't have implementation for this method, so system call this method from parent class.
One other thing. For your example best place to configure NavigationItem is viewDidLoad method.

Navigate to viewcontroller from a subclass function in swift

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.

Resources