I have programmed UIBarButtonItem so that it does some action before switching to a previous view. I was wondering how do I get the viewcontroller of that transitioning scene from my UIBarButtonItem?
So, scene 1 -> scene 2 (current scene) -> scene 1 (after clicking the UIBarButtonItem button)
I've tried to pass the previous scene variables (that I need) to the current scene to perform action on (sense I don't think the transitioning scene is instantiating a new view, but that doesn't work
override func viewDidLoad() {
super.viewDidLoad()
loadTuple()
let addButton: UIBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(saveExercise(_: )))
self.navigationItem.setRightBarButton(addButton, animated: true)
}
#objc func saveExercise(_ sender: UIBarButtonItem) {
self.addNewTupleToDB(element: self.getNewTuple())
self.saveData()
debugPrint("saveExercise")
self.exerciseVCTableView?.reloadData() // tried to pass the table view from the previous scene to call here
self.navigationController?.popViewController(animated: true)
// Want to save reload the table data of the scene this button transitions to
}```
You may use delegate pattern for solving this. Delegate pattern is something, to delegate some work to other and return to the work after delegation is done.
Suppose ViewController1 has UIBarButton , goes to ViewController2, some function done and return to ViewController1
let us take a protocol
protocol MyProtocol {
func myFunction()
}
then in ViewController2 add a delegate method. Assuming in ViewController2, you have to call a method doMyWork and some work will be done here, then you have to pop.
class ViewController2 {
var delegate : MyProtocol?
override func viewDidLoad() {
super.viewDidLoad()
doMyWork()
}
func doMyWork() {
// my works
delegate?.myFunction()
self.navigationController.popViewController()
}
}
now the viewController1 have to receive the delegate work has done.
in viewController1, in barButtonItem
class ViewController1 {
#objc func barButton(_sender : UIBarButton) {
let viewController = ViewController2()
viewController.delegate = self
self.naviagtionController.pushViewController(viewController, animated : true)
}
}
now you have to implement protocol method
extension ViewController1 : MyProtocol {
func myFunction() {
self.tableView.reloadData()
}
}
Related
I have a UICollectionViewController displaying an array of data called countdowns. I have a UIViewController that is presented modally where the user can create a new countdown. When the user taps "Add" (a UIBarButtonItem), I want to add the new countdown from UIViewController to the array (and thus the collection view) on UICollectionViewController.
I am building my app completely programmatically (no storyboard files). Every article/stack overflow post/YouTube tutorial I can find involves using a UIStoryBoardSegue and/or is about pushing data from a CollectionView to another ViewController, but not the other way around. I have not found a way to do this 100% programmatically.
During my hunt I've read is generally bad to create and use segues purely programmatically. Is this true? If so, do I just need to use a delegate? How would I implement that?
Here's some code:
Countdown Class: (has other properties, but for sake of simplicity I'm only including the title property)
class Countdown {
var title: String!
init?(title: String!) {
// Fail initialization if string is blank
if title.isEmpty {
return nil
}
// Initializing property
self.title = title
}
}
CountdownCollectionViewController: (this is the main view of the app)
class CountdownCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
var countdowns = [Countdown]()
override func viewDidLoad() {
super.viewDidLoad()
loadSampleCountdowns()
}
private func loadSampleCountdowns() {
// created 3 sample countdowns here
countdowns += [countdown1, countdown2, countdown3]
}
UIViewController: (presented modally from CountdownsCollectionViewController)
class ViewController: UIViewController {
var countdown: Countdown?
// here I have a textfield property where the user enters a title for a new countdown
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .done, target: self, action: #selector(addTapped))
}
#objc func addTapped(_ sender: UIBarButtonItem) {
let title = titleTextField.text ?? ""
countdown = Countdown(title: title)
// here is where I want an action to send the newly created countdown to CountdownCollectionViewController
}
I have tried using this function in CountdownCollectionViewController that uses a segue, but since I do not have storyboards it of course does not work. However, I think I'm headed in the right direction with the code that is inside of it:
func unwindToCountdownList(_ sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? ViewController, let countdown = sourceViewController.countdown {
// Add a new countdown:
let newIndexPath = IndexPath(row: countdowns.count, section: 0)
// Append the new countdown from UIViewController to the array:
countdowns.append(countdown)
// Add new item to collectionView:
collectionView.insertItems(at: [newIndexPath])
}
I would use a delegate here.
Create a ViewControllerDelegate:
protocol ViewControllerDelegate: class {
func didCreateCountDown(vc: ViewController, countDown: CountDown)
}
Add a delegate property to ViewController and call it at the appropriate time:
weak var delegate: ViewControllerDelegate?
#objc func addTapped(_ sender: UIBarButtonItem) {
let title = titleTextField.text ?? ""
countdown = Countdown(title: title)
delegate.didCreateCountDown(vc: self, countDown: countDown)
dismiss(animated: true, completion: nil)
}
Make CountdownCollectionViewController conform to ViewControllerDelegate:
extension CountdownCollectionViewController : ViewControllerDelegate {
func didCreateCountDown(vc: ViewController, countDown: CountDown) {
// Add a new countdown:
let newIndexPath = IndexPath(row: countdowns.count, section: 0)
// Append the new countdown from UIViewController to the array:
countdowns.append(countdown)
// Add new item to collectionView:
collectionView.insertItems(at: [newIndexPath])
}
}
Although you did not show this, there should be a place in CountdownCollectionViewController that you call present to present ViewController:
let vc = ViewController()
...
present(vc, animated: true, completion: nil)
Just before present, you can set self as the delegate:
vc.delegate = self
I have the method to check when the back button in navigation bar is press and the method go back to root page but for some reason when self.navigationController?.popToRootViewController(animated: true) it only go back to the previous page. do anyone know how to go back to the root when navigation bar back button is pressed?
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
if parent == nil{
self.navigationController?.popToRootViewController(animated: true)
}
}
In this question he is asking how to what method can he use to customise his back button. In my code its able to detect when user press on back button and self.navigationController?.popToRootViewController(animated: true)
is suppose to bring the page back to the root page, however there are somethings in the system preventing my app to go back to the root page.
i think the best way is to create your own custom back button at this page
override func viewDidLoad {
super.viewDidLoad()
navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(YourViewController.back(sender:)))
navigationItem.leftBarButtonItem = newBackButton
}
func back(sender: UIBarButtonItem) {
// Perform your custom actions
// ...
// Go back to the root ViewController
_ = navigationController?.popToRootViewController(animated: true)
}
credit to this answer by 'fr33g' : Execute action when back bar button of UINavigationController is pressed
Personally I would not recommend what you are trying to achieve, but anyways here is a different solution without customizing the back button.
Steps to implement
Create CustomNavigationController by subclassing
UINavigationController
Override popViewController(animated:)
When ViewController conforms to Navigationable and
shouldCustomNavigationControllerPopToRoot() returns true, call super.popToRootViewController
Otherwise proceed with normally popping the ViewController
Source Code
Custom Navigation Controller
import UIKit
class CustomNavigationController: UINavigationController {
// MARK: - Initializers
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
initialSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialSetup()
}
// MARK: - Setups
private func initialSetup() {
// DISCLAIMER: This code does not support `interactivePopGestureRecognizer`, therefore we disable it
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func popViewController(animated: Bool) -> UIViewController? {
if shouldNavigationPopToRoot {
return super.popToRootViewController(animated: animated)?.last
}
return super.popViewController(animated: animated)
}
// MARK: - Helpers
private var shouldNavigationPopToRoot: Bool {
return (topViewController as? Navigationable)?.shouldCustomNavigationControllerPopToRoot() == true
}
}
View Controller conforming to Navigationable
import UIKit
protocol Navigationable: class {
func shouldCustomNavigationControllerPopToRoot() -> Bool
}
class ViewController: UIViewController, Navigationable {
// MARK: - Protocol Conformance
// MARK: Navigationable
func shouldCustomNavigationControllerPopToRoot() -> Bool {
return true
}
}
Output
I'm wondering what's the best way to achieve the following. My application when launches goes to the following tableview:
When you select a category, a segue to another tableview is made and looks like this:
What I want to do, is eventually have the 'Basket' barButtonItem in the first view to update with the total price of the basket amount. I also want the button to be visible from the entire navigation controller cycle.
Is there a way that I can have the Basket button show on every stage of the navigationcontroller process?
So for example I'd like to have the button show on the second table view.
Yes you can achieve it by use of take class of UINavigationController like below
class CustomNavigationController: UINavigationController, UINavigationControllerDelegate{
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
// MARK: Private Functions
private func addRightBarButtonTo(viewController: UIViewController){
barButtonItem = UIBarButtonItem(title: "Basket", style: .plain, target: self, action: #selector(CustomNavigationController.dismiss(_:)))
viewController.navigationItem.rightBarButtonItem = barButtonItem
}
// MARK: UINavigationController Delegate
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
self.addRightBarButtonTo(viewController)
}
#objc func dismiss(sender: Any){
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
}
Use CustomNavigationController as rootView Controller of the Window.
Second Way
Take extension of UIViewController
extension UIViewController {
func addRightButtonItem() {
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Basket", style: .done, target: self, action: #selector(barButtonMethod(_:)))
}
#objc func barButtonMethod(_ sender: UIBarButtonItem) {
// Your code
}
}
and call below method in viewWillAppear of each and every viewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.addRightButtonItem()
}
I'd like to have a close button on each view controller that appears in the navigation stack. I've read here that I need to create an object that is a uinavigationdelegate, I think this object will have a method like didTapCloseButton?
Questions:
Should I create a protocol and make everything confirm to it, i.e.:
protocol CustomDelegate: UINavigationControllerDelegate {
func didTapCloseButton()
}
public class ViewController: CustomDelegate {
func didTapCloseButton() {
//not sure what goes in here?
}
}
How do I get the close button to show on the navigation bars of every view?
When the user clicks the close button, how do I get that to dismiss every view on that stack?
Thanks for your help!
Here a simple solution. Create UINavigationController subclass and override pushViewController method.
class NavigationController: UINavigationController {
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
super.pushViewController(viewController, animated: animated)
let closeBarButtonItem = UIBarButtonItem(
title: "Close",
style: .done,
target: self,
action: #selector(self.popViewController(animated:)))
viewController.navigationItem.rightBarButtonItem = closeBarButtonItem
}
}
Not sure if this is what you intended but you can do:
protocol CustomDelegate: UINavigationControllerDelegate {
func didTapCloseButton()
}
extension CustomDelegate where Self : UIViewController{
func didTapCloseButton(){
// write your default implementation for all classes
}
}
now for every UIViewController class you have you can just do :
class someViewController: CustomDelegate{
#IBAction buttonClicked (sender: UIButton){
didTapCloseButton()
}
}
How can a prepare a segue properly from inside a UIView NOT UIViewController
My UIViewController has a container view and inside that container view has a button.
class myInnerView: UIView {
...
func myButton(gesture: UIGestureRecognizer){
//calling a perform segue from another UIViewController does not recognize the SegueID
//ViewController().self.showProfile(self.id) -- DOES NOT WORK
}
}
class ViewController: UIViewController {
...
func showProfile(id: String){
...
self.performSegueWithIdentifier("toViewProfile", sender: self)
}
}
Your view should not handle a button tap. That should be handled by the controller. (This is why it is called "controller", while the UIView is called "view").
MVC as described by Apple.
If the button is a subview of your view controller's view, you should be able to drag an IBAction onTouchUpInside from the button to your view controller. You can then initiate the segue from that method.
One of the solution is to add UITapGestureRecognizer to your button but from inside the UIViewController :
class ViewController: UIViewController {
var myInnerView = myInnerView()
override func viewDidLoad() {
let tap = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTapGesture))
self.myInnerView.myButton.addGestureRecognizer(tap)
}
#objc func handleTapGesture(){
performSegue(withIdentifier: "toViewProfile", sender: nil)
}
}
class myInnerView: UIView {
// make outlet of your button so you can add the tapGestureRecognizer from your ViewController and thats all
#IBOutlet public weak var myButton: UIButton!
}
You need to tap gesture for view to navigate
let customView = myInnerView()
let gestureRec = UITapGestureRecognizer(target: self, action: #selector (self.someAction (_:)))
myView.addGestureRecognizer(customView)
func someAction(_ sender:UITapGestureRecognizer){
let controller = storyboard?.instantiateViewController(withIdentifier: "someViewController")
self.present(controller!, animated: true, completion: nil)
// swift 2
// self.presentViewController(controller, animated: true, completion: nil)
}