TabBarController: always jump to root NavigationController - swift

I have the following hierarchy in my app:
there is a TabBarController (X)
each of its items points to a NavigationController (A, B, C...)
each of those NavigationControllers starts a hierarchy (e.g. A1, A2, A3,...) of TableViewControllers
Now when the user is, say, in A3, they can press another TabBar item and jump to another hierarchy (say, the one of B). However, when they press the item for A, they will jump back to A3.
What I do want to happen, is for them to jump to A1 instead, which is essentially the "parent" UIView in the A hierarchy. Similarly, if they press B, they should jump to B1, not to wherever they were in the B hierarchy. I do not want to force the user to go back to e.g. A1 manually by hiding the bottom bar (the one from the TabBarController).
What is the best way to achieve this?
For visualisation:
/- A - A1 - A2 - A3
X ---- B - B1 - ...
\- C - ...
Programmatically, I currently have custom classes for X (TabBarController, TabBarControllerDelegate) and the TableViewControllers A1,...,B1,... . When the user presses an item, I can by debugging see that the target VC for the segue would be A/B/... and not A1/B1/... so I cannot control the process this way.
EDIT: check storyboard image below.

I'm not sure if I understood you correctly, but the bottomline is whenever the user taps on the tab bar you want the user to be able to see the root controller of each of the UINavigationController, correct?
You mentioned that you already have a custom TabBarController so have you tried the shouldSelect method?:
class CustomTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
(viewController as? UINavigationController)?.popToRootViewController(animated: true)
return true
}
}
This seems to be working when I tried it on my playground.
Here's the full code I tried it with:
import PlaygroundSupport
import UIKit
class A: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "A"
let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
label.text = "A"
self.view.addSubview(label)
let button = UIButton(frame: CGRect(origin: .init(x: 100, y: 200), size: .init(width: 200, height: 100)))
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
button.backgroundColor = .black
self.view.addSubview(button)
}
#objc func pressed(_ sender: UIButton) {
let a1 = A1()
self.navigationController?.pushViewController(a1, animated: true)
}
}
class A1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
label.text = "A1"
self.view.addSubview(label)
}
}
class B: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "B"
let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
label.text = "B"
self.view.addSubview(label)
let button = UIButton(frame: CGRect(origin: .init(x: 100, y: 200), size: .init(width: 200, height: 100)))
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
button.backgroundColor = .black
self.view.addSubview(button)
}
#objc func pressed(_ sender: UIButton) {
let b1 = B1()
self.navigationController?.pushViewController(b1, animated: true)
}
}
class B1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
label.text = "B1"
self.view.addSubview(label)
}
}
class CustomTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
(viewController as? UINavigationController)?.popToRootViewController(animated: true)
return true
}
}
let nav1 = UINavigationController(rootViewController: A())
let nav2 = UINavigationController(rootViewController: B())
let tabbarVC = CustomTabBarController()
tabbarVC.view.frame = CGRect(origin: .zero, size: CGSize(width: 500, height: 500))
tabbarVC.addChild(nav1)
tabbarVC.addChild(nav2)
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = tabbarVC

Related

How can I properly reload the UIViewController after dismiss method is called?

I have 2 viewControllers named vcA and vcB.
vcA has navigationController and vcB is just viewController which don't have navigationController.
I saved text data in a file in vcB, and I wanna show that text in vcA when I come back to vcA after the dismiss method is called.
I thought reloading vcA after dismiss is called is the proper way to show text, but I figured out that it might be difficult to do that between vcA and vcB, because vcA has navigationController but vcB doesn't have navigationController.
How can I refresh vcA?
You can use self.present over self.dismiss. Like this:
let vc = vcA()
And here you can pass data. For example, if vcA() has a var called text that is a string you can use vc.text = "something". And this data will go with the self.present method
vc.modalPresentationStyle = .fullScreen
self.present(vc,animated: true, completion: nil)
You can pass the value back to the presenting view controller using the protocol/delegate pattern. Following is an example of how to pass a text from a text field in vcB to vcA as vcB is being dismissed so you can substitute that with the value you want to pass.
protocol ValuePassingDelegate {
func didPassValue(of text: String)
}
class ViewControllerA: UIViewController, ValuePassingDelegate {
let label = UILabel(frame: CGRect(origin: CGPoint(x: 0, y: 100), size: CGSize(width: 200, height: 100)))
let button = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(label)
label.text = "Initial text"
label.textAlignment = .center
button.frame = CGRect(origin: CGPoint(x: 0, y: 300), size: CGSize(width: 200, height: 100))
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(buttonHandler), for: .touchUpInside)
view.addSubview(button)
}
#objc func buttonHandler() {
let vcB = ViewControllerB()
vcB.delegate = self
self.navigationController?.pushViewController(vcB, animated: true)
}
func didPassValue(of text: String) {
DispatchQueue.main.async {
self.label.text = text
}
}
}
class ViewControllerB: UIViewController {
let textField = UITextField(frame: CGRect(origin: .zero, size: CGSize(width: 200, height: 100)))
var delegate: ValuePassingDelegate?
override func viewDidLoad() {
super.viewDidLoad()
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.black.cgColor
view.addSubview(textField)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
delegate?.didPassValue(of: textField.text ?? "")
}
}
The way that I followed for this purpose is :
in vcB I added:
internal var senderVC: UIViewController?
in vcA I added:
vcB.senderVC = self
in vcB I added:
if let firstVC = self.senderVC as? vcA { firstVC.tableView.reloadData(); }

How to add another button to a button?

Everything works fine, but when I try to call the createSecondContainer () method from configureAddButton() it doesn't work. That is, pressing the button itself does not work.
override func viewDidLoad() {
super.viewDidLoad()
configureMainContainer()
}
func configureMainContainer(){
view.addSubview(mainContainer)
let frame = CGRect(x: view.bounds.midX - 60, y: view.bounds.midY, width: 120, height: 40)
mainContainer.frame = frame
mainContainer.addTarget(self, action: #selector(showAlert), for: .touchUpInside)
configureAddButton()
}
func configureAddButton(){
mainContainer.addSubview(addButton)
let frame = CGRect(x: mainContainer.bounds.maxX + 5, y: mainContainer.bounds.midY / 2, width: 20, height: 20)
addButton.frame = frame
addButton.addTarget(self, action: #selector(createSecondContainer), for: .touchUpInside)
}
#objc func createSecondContainer(){
print("something to complete")
}
You can use this Code working fine
import UIKit
class ViewController: UIViewController {
let mainContainer = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
configureMainContainer()
}
func configureMainContainer(){
let frame = CGRect(x: view.bounds.midX - 60, y: view.bounds.midY, width: 120, height: 40)
mainContainer.frame = frame
mainContainer.addTarget(self, action: #selector(showAlert), for: .touchUpInside)
mainContainer.backgroundColor = UIColor.red
self.view.addSubview(mainContainer)
configureAddButton()
}
#objc func showAlert() {
// Your Alert Code here
}
func configureAddButton(){
let addButton = UIButton()
let frame = CGRect(x: mainContainer.bounds.midX, y: mainContainer.bounds.midY / 2, width: 20, height: 20)
addButton.frame = frame
addButton.addTarget(self, action: #selector(createSecondContainer), for: .touchUpInside)
addButton.backgroundColor = UIColor.blue
mainContainer.addSubview(addButton)
}
#objc func createSecondContainer(){
print("something to complete")
}
}

Use Uitapgesture recognizer on multiple image views that are not declared

My swift code uses func addBox to add and append image views to the uiview controller. All I want to do is when one of the image views are tapped is for func viewClicked to be activated. Right now nothing is happening and nothing is being written into the debug area.
import UIKit
class ViewController: UIViewController {
var ht = -90
var ww = 80
var hw = 80
var arrTextFields = [UIImageView]()
var b7 = UIButton()
override func viewDidLoad() {
[b7].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
$0.backgroundColor = .systemOrange
}
b7.frame = CGRect(x: view.center.x-115, y: view.center.y + 200, width: 70, height: 40)
b7.addTarget(self, action: #selector(addBOx), for: .touchUpInside)
for view in self.arrTextFields {
view.isUserInteractionEnabled = true
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(viewClicked)))
}
}
#objc func viewClicked(_ recognizer: UITapGestureRecognizer) {
print("tap")
}
//func that adds imageview.
#objc func addBOx() {
let subview = UIImageView()
subview.isUserInteractionEnabled = true
arrTextFields.append(subview)
view.addSubview(subview)
subview.frame = CGRect(x: view.bounds.midX - 0, y: view.bounds.midY + CGFloat(ht), width: CGFloat(ww), height: 35)
subview.backgroundColor = .purple
ht += 50
arrTextFields.append(subview)
}
}
You need to enable user interation
for view in self.arrTextFields {
view.isUserInteractionEnabled = true
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(viewClicked)))
}
#objc func viewClicked(_ recognizer: UITapGestureRecognizer) {
print("tap")
}

Create segue objects programmatically

I created a single view app with 2 view controllers. In the first view controller, I have a label and 2 buttons that were programmatically created. I have 2 segues from these buttons to the second view controller. I wish that each button carries different information to the second view controller. From creating these buttons and segues programmatically, I haven’t been able to assign the identifier to the segues. So, I can’t use performSegue(withIdentifier: sender:).
How can I do this?
First ViewController:
import UIKit
class ViewController: UIViewController {
var myLabel: UILabel!
var leftButton: UIButton!
var rightButton: UIButton!
// bellow are the 2 variable which I’d like to transport into the second ViewController
var leftButtonText = "I'm the left button."
var rightButtonText = "I'm the right button."
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let homeNavBar = UINavigationBar()
homeNavBar.frame = CGRect(x: 0, y: 0, width: 320, height: 45)
homeNavBar.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(homeNavBar)
let homeNavItem = UINavigationItem(title: "Home")
homeNavBar.setItems([homeNavItem], animated: false)
myLabel = UILabel()
myLabel.frame = CGRect(x: 165, y: 150, width: 200, height: 42)
myLabel.text = "Press one of the button bellow!"
self.view.addSubview(myLabel)
leftButton = UIButton(type: .custom)
leftButton.frame = CGRect(x: 80, y: 300, width: 70, height: 70)
leftButton.backgroundColor = UIColor.cyan
leftButton.setTitleColor(UIColor.black, for: .normal)
leftButton.setTitle("Left Button", for: .normal)
leftButton.layer.cornerRadius = 20
leftButton.layer.borderWidth = 1
leftButton.layer.masksToBounds = true
leftButton.addTarget(self, action: #selector(ViewController.leftButtonAction(_:)), for: .touchUpInside)
self.view.addSubview(leftButton)
rightButton = UIButton(type: .custom)
rightButton.frame = CGRect(x: 240, y: 300, width: 70, height: 70)
rightButton.backgroundColor = UIColor.cyan
rightButton.setTitleColor(UIColor.black, for: .normal)
rightButton.setTitle("Right button", for: .normal)
rightButton.layer.cornerRadius = 20
rightButton.layer.borderWidth = 1
rightButton.layer.masksToBounds = true
rightButton.addTarget(self, action: #selector(ViewController.rightButtonAction(_:)), for: .touchUpInside)
self.view.addSubview(rightButton)
let margins = self.view.layoutMarginsGuide
homeNavBar.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
homeNavBar.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
homeNavBar.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
myLabel.topAnchor.constraint(equalTo: homeNavBar.bottomAnchor).isActive = true
leftButton.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
leftButton.topAnchor.constraint(equalTo: myLabel.bottomAnchor).isActive = true
rightButton.topAnchor.constraint(equalTo: myLabel.bottomAnchor).isActive = true
rightButton.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
}
#objc func leftButtonAction (_ sender: UIButton) {
let segueSecondViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "idSecond")
self.show(segueSecondViewController, sender: nil)
print("I'm activated by the left button.")
}
#objc func rightButtonAction (_ sender: UIButton) {
let segueSecondViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "idSecond")
self.show(segueSecondViewController, sender: nil)
print("I'm activated by the right button.")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Second ViewController:
import UIKit
class SecondViewController: UIViewController {
var mySecondLabel: UILabel!
var mySecondLabelText = "I'm the second page."
var closeButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let secondNavBar = UINavigationBar()
secondNavBar.frame.size = CGSize(width: 320, height: 45)
secondNavBar.translatesAutoresizingMaskIntoConstraints = false
let secondNavItem = UINavigationItem(title: "Second page")
secondNavBar.setItems([secondNavItem], animated: false)
self.view.addSubview(secondNavBar)
mySecondLabel = UILabel()
mySecondLabel.frame = CGRect(x: 165, y: 200, width: 200, height: 42)
mySecondLabel.backgroundColor = UIColor.yellow
mySecondLabel.textColor = UIColor.black
mySecondLabel.text = "\(mySecondLabelText)"
mySecondLabel.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(mySecondLabel)
closeButton = UIButton(type: .custom)
closeButton.frame = CGRect(x: 10, y: 50, width: 50, height: 50)
closeButton.backgroundColor = UIColor.red
closeButton.setTitleColor(UIColor.black, for: .normal)
closeButton.setTitle("Close", for: .normal)
closeButton.layer.cornerRadius = 30
closeButton.layer.borderWidth = 1
closeButton.layer.masksToBounds = true
closeButton.addTarget(self, action: #selector(SecondViewController.closeButtonAction(_:)), for: .touchUpInside)
self.view.addSubview(closeButton)
let margins = self.view.layoutMarginsGuide
secondNavBar.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
secondNavBar.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
secondNavBar.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
closeButton.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
closeButton.topAnchor.constraint(equalTo: secondNavBar.bottomAnchor).isActive = true
mySecondLabel.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
mySecondLabel.topAnchor.constraint(equalTo: closeButton.bottomAnchor).isActive = true
mySecondLabel.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
}
#objc func closeButtonAction (_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
print("Second page closed.")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
You can do it by giving your View Controller an Identifier. This isn't a segue, but it will Instantiate your target VC:
let vc = UIStoryboard(name:"Main", bundle:nil).instantiateViewControllerWithIdentifier("identifier") as! SecondViewController
self.navigationController?.pushViewController(vc, animated:true)
You can set the VC Identifier on the Identity Inspector tab on the StoryBoard.
I just read in the documentation that you can't call the performSegue method if you don't have a segue with an identifier in the Storyboard, so WRT your requirement, you can't do it programmatically.
identifier The string that identifies the triggered segue. In
Interface Builder, you specify the segue’s identifier string in the
attributes inspector.

Present viewController after touchUp GIDSignInButton()!

I make a Google Auth for my App, but I don't know how to present a new viewController after touchup GIDSignInButton!
Here how I make GIDSignInButton:
viewDidLoad (){
let googleBtn = GIDSignInButton()
googleBtn.frame = CGRect(x: 16, y: 500 + 66, width: view.frame.width - 32, height: 35)
view.addSubview(googleBtn)}
Here's a code example that will present a second, programmatically generated view controller using a standard UIButton. Obviously, you could do the same with your GIDSignInButton:
class MyViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
let button = UIButton(frame: CGRect(x: 10, y: 250, width: self.view.frame.width - 20, height: 35))
button.setTitle("Go to VC2", for: .normal)
button.backgroundColor = UIColor.blue
button.addTarget(self, action:#selector(self.buttonClicked), for: .touchUpInside)
self.view.addSubview(button)
}
func buttonClicked(sender: UIButton!)
{
let secondViewController = MySecondViewController()
present(secondViewController, animated: true, completion: {})
}
}
class MySecondViewController:UIViewController
{
override func viewDidLoad() {
self.view.backgroundColor = UIColor.darkGray
}
}
Note, however, that if you are presenting multiple views, you are advised to embed them in a Navigation Controller, as per Apple's Documentation