How can I use a protocol in combination with a navigation controller? - swift

Hey guys I have a little question again. How can I use a protocol in combination with a nav controller. So first of all here are my two classes:
First View Controller:
class VC1: UIViewController{
var cons = "nothing"
#IBAction func PRINT(_ sender: Any) {
print(cons)
}
#IBAction func PRESENT(_ sender: Any) {
let VC = storyboard?.instantiateViewController(identifier: "VC") as! VC2
VC.delegate = self
present(VC, animated: true, completion: nil)
} }
extension VC1: data {
func give(text: String) {
cons = text
} }
Second View Controller:
protocol data {
func give(text: String)}
class VC2: UIViewController {
var delegate: data!
#IBAction func SAVEDISMISS(_ sender: Any) {
delegate.give(text: "Hallo")
dismiss(animated: true, completion: nil)
}}
And here is also my storyboard:
enter image description here
So now I will come to my problem. When I run the code there is an error message when I perform any segue on VC2 because by using the protocol, the navigation controller is excluded, but when i present the navigation controller instead of VC2 the protocol isn´t working anymore.
What could I change so that the navigation controller isn´t excluded and the protocol is working both.
Hopefully anyone can help me, and sorry for my bad english again. I hope you can understand everything.
Have a great day or night.

Replace PRESENT function with the following code when you are trying to present a UINavigationController.
#IBAction func PRESENT(_ sender: Any) {
let VC = storyboard?.instantiateViewController(identifier: "VC") as! VC2
VC.delegate = self
let navigationController = UINavigationController(rootViewController: VC)
present(navigationController, animated: true, completion: nil)
}

Related

How do I refresh ViewController after dismiss method is called

For example, I have two viewControllers named A and B. I move to "B view controller" after present method is called. I set some values through UserDefaults.stnadard on "B viewController", and I come back to "A view controller" after dismiss method is called. "A viewController" have to show modified values, but it doesn't show the modified ones. I tried calling viewDidAppear() method on "A viewController", but it didn't work. I tried calling viewDidLoad() method, and it worked well, but I heard that it's not a good way to call the method directly. System can only call it. So, I don't want to call it myself. How should I refresh "A view controller"?
From iOS 13.0 and above, Apple has changed the way viewControllers are presented by default. So, viewWillAppear in ViewControllerA will not be called after dimissing ViewControllerB.
One way for making sure the viewWillAppear method will be called after dismissing ViewControllerB is presenting the viewController setting modalPresentationStyle to fullScreen as:
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ViewControllerB") as! ViewControllerB
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: true, completion: nil)
Sample Code:
ViewControllerA.swift
class ViewControllerA: UIViewController {
private var username: String? {
UserDefaults.standard.string(forKey: "USERNAME")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print(username) //set this to desired view
}
#IBAction func goToViewControllerB(_ sender: UIButton) {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ViewControllerB") as! ViewControllerB
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: true, completion: nil)
}
}
ViewControllerB.swift
class ViewControllerB: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.set("John", forKey: "USERNAME")
}
#IBAction func dismiss(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
If you don't want to change the modalPresentationStyle to fullScreen then you can use closures for passing data to ViewControllerA when you're dismissing ViewControllerB.
I think NotificationCenter or prototype would help get this done.
NotificationCenter example
class ViewControllerA: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(testFunc), name: NSNotification.Name(rawValue: "PeformAfterPresenting"), object: nil)
}
#objc func testFunc() {
//TODO: your task
}
}
class ViewControllerB: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismiss(_ sender: Any) {
NotificationCenter.default.post(name: "PeformAfterPresenting"), object: nil, userInfo: dataDict)
self.dismiss(animated: true, completion: nil)
}
}
A solid way to handle this would be to set up a protocol & delegate, and call it on ViewControllerB's viewWillDisappear.
In ViewControllerB, set up protocol and delegate property:
protocol ViewControllerBDelegate: AnyObject {
func refreshUserDefaults()
}
weak var delegate: ViewControllerBDelegate?
Also in ViewControllerB, call the method in viewWillDisappear
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.delegate?.refreshUserDefaults()
}
Then in ViewControllerA, before you present ViewControllerB, assign ViewControllerA as delegate:
let vc = ViewControllerB()
vc.delegate = self
// present as desired
Also in ViewControllerA, conform to ViewControllerB's protocol:
extension ViewControllerA: ViewControllerBDelegate {
func refreshUserDefaults() {
// do whatever you need here
}
}

Swift NavigationController push method request doesn't work from PopUp view

The new PopUp ViewController is presented .overCurrentContext
There are 2 buttons with navigation for 2 other views from PopUp view.
A simple action such as print goes well, but when I try to segue programmatically (from xib file, popUpViewController) nothing happens.
let vc = RegisterEmailViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
Goes well from any other view.
What could be wrong?
PopUpViewController code:
// Created by ᴀʟᴇxᴀɴᴅʀ ᴢʜᴇʟɪᴇᴢɴɪᴀᴋ on 23.12.2019.
// Copyright © 2019 ᴀʟᴇxᴀɴᴅʀ ᴢʜᴇʟɪᴇᴢɴɪᴀᴋ. All rights reserved.
import UIKit
class ViewController: UIViewController, Storyboarded {
weak var coordinator: MyCoordinator?
#IBAction func dismissPopUp(_ sender: Any) {
dismiss(animated: false, completion: nil)
}
#IBAction func buttonEmail(_ sender: Any) {
let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "EmailViewController") as? EmailViewController
self.navigationController?.pushViewController(vc!, animated: true)
}
#IBAction func buttonPhone(_ sender: Any) {
let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "PhoneViewController") as? PhoneViewController
self.navigationController?.pushViewController(vc!, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
There is MyCoordinator which inits all navigations and doesn't let me simply to segue/navigate by my own.
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
Therefore, a reference to the controller solved the issue. It is possible to navigate aside, until new subViews, separate views are called.
Code that worked in this case:
let vc = RegisterEmailViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
Conclusion. Simple there are two ways:
To delete all dependencies and other methods which use
UINavigationController
To declare only by new methods for global usage.

iOS 13 - Right Bar Button Item goes offset in Modal Sheet presentation

I have this strange problem with iOS 13 and its new sheet cards style modal presentation.
From ViewController1 I modal present ViewController2 embedded in a NavigationController, and everything works fine.
From ViewController2, I then modal present ViewController3 embedded in a NavigationController, and I get the Right Bar Button offset.
Here is a video of the problem: does anybody have a fix?
Main View Controller
import UIKit
let vc1identifier = "vc1identifier"
let vc2identifier = "vc2identifier"
class ViewController: UIViewController {
#IBAction func tap1(_ sender: UIButton) {
if let navigation = self.storyboard?.instantiateViewController(withIdentifier: vc1identifier) as? UINavigationController,
let nextVC = navigation.contentViewController as? UnoViewController {
//self.navigationController?.pushViewController(nextVC, animated: true)
self.present(navigation, animated: true, completion: nil)
}
}
}
extension UIViewController {
var contentViewController: UIViewController {
if let navcon = self as? UINavigationController {
return navcon.visibleViewController!
} else {
return self
}
}
}
Second View Controller
import UIKit
class UnoViewController: UIViewController {
#IBOutlet weak var barButton: UIBarButtonItem!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
barButton.title = "GotoVC2"
}
#IBAction func pressThis(_ sender: UIBarButtonItem) {
if let navigation = self.storyboard?.instantiateViewController(withIdentifier: vc2identifier) as? UINavigationController,
let nextVC = navigation.contentViewController as? DueViewController {
self.present(navigation, animated: true, completion: nil)
}
}
}
I came across the same issue.
Solution is easy, you just need to tell navigationbar it needs layout like this
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 13.0, *) {
navigationController?.navigationBar.setNeedsLayout()
}
}

Swift: How to dismiss a ViewController programmatically?

I got a little problem.
On my main view controller I got a bar button that opens a slide menu, which is a regular view controller using a slide in transition. The slide menu has a button to open another view controller. When the new view controller is opened, you have the option to cancel, which dismisses the current view controller. The problem is, that the user ends up in the menu view once again, instead of the main view controller. Would be very happy to know what I am doing wrong :)
func openSupport() {
guard let creditViewContoller = storyboard?.instantiateViewController(withIdentifier: "support") as? CreditViewController else { return }
present(creditViewContoller, animated: true)
}
#IBAction func buttonSupport(_ sender: UIButton) {
let menuView = MenuViewController()
menuView.dismiss(animated: true, completion: nil)
openSupport()
print("Tap on Support")
}
you can dismiss view controller simply by using
self.dismiss(animated: true, completion: nil)
Consider
#IBAction func buttonSupport(_ sender: UIButton) {
let menuView = MenuViewController() // (1)
menuView.dismiss(animated: true, completion: nil) // (2)
openSupport() // (3)
print("Tap on Support")
}
This:
Creates new MenuViewController but never presents it;
Calls dismiss on view controller that was never presented; and
Calls openSupport from this MenuViewController instance (which was never dismissed).
Bottom line, you want to let the main view controller that presented the menu do the presenting. So, the menu view controller should:
Define a protocol for it to inform the presenting view controller to transition to the next scene:
protocol MenuViewControllerDelegate: class {
func menu(_ menu: MenuViewController, present viewController: UIViewController)
}
And then the menu view controller can, when it’s done dismissing, tell its delegate what it should present:
class MenuViewController: UIViewController {
weak var delegate: MenuViewControllerDelegate?
#IBAction func didTapSupport(_ sender: Any) {
dismiss(animated: true) {
guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "support") else { return }
self.delegate?.menu(self, present: controller)
}
}
#IBAction func didTapCancel(_ sender: Any) {
dismiss(animated: true)
}
}
Then the main view controller needs to
Make sure to set the delegate of the menu view controller:
class ViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? MenuViewController {
destination.delegate = self
}
}
}
and
Make sure to present the view controller that the menu controller asked it to:
extension ViewController: MenuViewControllerDelegate {
func menu(_ menu: MenuViewController, present viewController: UIViewController) {
present(viewController, animated: true)
}
}
There are lots of different ways of achieving this, so don’t get lost in the details here. But the idea is to have some system by which the menu view controller can request whomever is to present the support view to do so, not try to do it itself.

Prepare For Segue on return

So the code I am trying to implement in Swift is based upon this answer here for passing data back from a ViewController:
Passing Data with a Callback
Now my issue is after I have called:
self.navigationController?.popViewController(animated: true)
The Prepare For Segue function isn't called in my original View Controller. I assume it shouldn't be called anyway but from that answer I assume there is a possible way to do so?
First View Controller Snippets
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//ignore this segue identifier here, this function works when I am showing a new VC
if(segue.identifier == "certSegue"){
let certVC = segue.destination as! CertificateViewController
certVC.profileModel = profileModel
}
//this is what I need to be called
if(segue.identifier == "dpSegue"){
print("dpSegue")
let dpVC = segue.destination as! DatePickerViewController
dpVC.callback = { result in
print(result)
print("Data")
// do something with the result
}
//dpVC.dailyBudgetPassedThrough = "Test"
}
}
func showDatePicker(){
let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "DatePickerVC") as? DatePickerViewController
self.navigationController?.pushViewController(vc!, animated: true)
}
Second View Controller
import UIKit
class DatePickerViewController: UIViewController {
var callback : ((String)->())?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func sendBackUpdate(){
print("Callback")
callback?("Test")
}
#IBAction func cancelButton(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
}
#IBAction func updateButton(_ sender: Any) {
sendBackUpdate()
self.navigationController?.popViewController(animated: true)
}
}
prepareForSegue is called if in Interface Builder a segue is connected
from a table/collection view cell to a destination view controller and the cell is tapped.
from a source view controller to a destination view controller and performSegue(withIdentifier:sender:) is called in the source view controller.
It's not called when a view controller is going to be presented with pushViewController
In your case assign the callback after instantiating the controller in showDatePicker, prepare(for segue is not needed.
func showDatePicker(){
let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "DatePickerVC") as! DatePickerViewController
vc.callback = { result in
print(result)
print("Data")
// do something with the result
}
self.navigationController?.pushViewController(vc, animated: true)
}