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.
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.
I Embedded a ViewController in a UINavigationController and after that, I added a new ViewController and connected it with a segue to the first ViewController that is embedded in a UINavigationController. then I wanted to add a BarButtonItem to that second view, but when I let go the bar button item in the top of the view next to the title, it inserts it at the button, and when I run the app it only shows the back button to the first screen and the title. (see the picture of how it looks when running and how it looks after inserting it at the top)
I hope you understood the question and you can help me!
thanks in advance!
Benji
write this code in your viewDidLoad Method of controller where you want to add button
let navBtn = UIButton(type: .custom)
navBtn.setImage(UIImage(named: "side_drawer"), for: .normal)
navBtn.frame = CGRect(x: 0, y: 0, width: 40, height: 100)
navBtn.addTarget(self, action: #selector(openMessagingThread(_:)), for: .touchUpInside)
let rightNavBarItem = UIBarButtonItem(customView: navBtn)
self.navigationItem.setRightBarButton(rightNavBarItem, animated: true)
//Action for barButton
#objc
func openMessagingThread(_ sender: UIButton) {
}
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
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 have a UINavigationController which contains a UITableViewController. This navigation controller pushes other UITableViewControllers around and eventually these table view controllers will have a prompt.
The problem is when I set this prompt programmatically it overlaps the content of the table view underneath it.
(A search bar is being hidden by the navigation bar)
I was looking around in SO and found this answer. I tried the suggestion there in two different ways in the affected view controller but nothing changed:
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = .None;
self.extendedLayoutIncludesOpaqueBars = false;
self.navigationItem.title = NSLocalizedString("Add Anime or Manga", comment: "")
self.navigationItem.prompt = NSLocalizedString("Search media belonging to this series.", comment: "")
}
-
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = NSLocalizedString("Add Anime or Manga", comment: "")
self.navigationItem.prompt = NSLocalizedString("Search media belonging to this series.", comment: "")
self.edgesForExtendedLayout = .None;
self.extendedLayoutIncludesOpaqueBars = false;
}
A comment in that same answer linked to this Apple guide on preventing views from overlapping each other. The problem is UITableViewController doesn't appear to have top/bottom layout guides so I can't create a constraint (another SO answer says having said layouts in table view controllers is irrelevant).
As such I have exhausted all my options.
I have tried to reproduce your problem and it seems that when not all the viewControllers have a prompt the navigationBar is somehow not resizing properly.
It seems you need to somehow trigger the layouting for the UINavigationController. The only way I could make it work properly was by adding this in viewWillAppear:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:YES];
[self.navigationController setNavigationBarHidden:NO];
}
Maybe this prompt is meant to be used consistently across the entire application (meaning having one for all viewControllers or none of them), that's why the UINavigationController does not layout it's subviews when it changes.
Hope this works for you too.
Select your TableViewController from document outline and change the value to translucent navigation bar of top bar in attributes inspector. Be sure that you will not select uitableview you should select your your table view controller(aka File's Owner) from document outline.
You have to set prompt only if view did appear, then it works:
override func viewDidAppear(_ animated: Bool) {
navigationItem.prompt = "your prompt here"
}
It's 2019 and this is still not fixed. Slow Clap. I refuse to be cowed by such things so I hammered iOS into submission with the dirtiest trick in the book. I fixed this by doing a disgusting -44 "y trick" on the UINavigation while placing the UITableView in top 44, I know it's stupid, but it works.. I am sure new fangled phones will ruin my genius.. but hey ho.. I have lazily left irrelevant code (because I am idle) but hopefully you can see what I did.
WITHOUT THE y: -44 Hack
WITH THE y: -44 Hack
let screenSize: CGRect = UIScreen.main.bounds
let navBar = UINavigationBar(frame: CGRect(x: 0, y: -44, width: screenSize.width, height: 44)) //<<--note minus 44
navBar.barTintColor = Globals.Color_BackgroundGrey()
navBar.isTranslucent = false
tableView.contentInset = UIEdgeInsets(top: 44, left: 0, bottom: 0, right: 0); //<--note plus 44
self.edgesForExtendedLayout = []
let navItem = UINavigationItem(title: "Boaty Mc Boatface")
let doneItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(done))
navItem.rightBarButtonItem = doneItem
navBar.setItems([navItem], animated: false)
view.addSubview(navBar)