Pushing to a new Viewcontroller from an inactive Viewcontroller (programatically) - ios

Short explanation.
I have a ContainerViewController that I'm pushing to the navigationStack.
The ContainerViewController has 2 child ViewControllers. A SlidePanelViewController (a slide-out menu) and a CenterViewController (the content)
I have a button in my menu to "sign Out". When this button is clicked I want to push ContainerViewController (and it's 2 childViewControllers) to my LandingPageViewController.
Here's the function I am trying to call:
func signOut() {
println("signOut")
// Set up the landing page as the main viewcontroller again.
let mainTableViewController = LandingPageVC()
mainTableViewController.navigationItem.setHidesBackButton(true, animated: false)
mainTableViewController.skipView = false
self.navigationController!.pushViewController(mainTableViewController, animated: true)
// Disable menu access
menuEnabled = false
// change status bar style back to default (black)
UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.Default
}
At first I tried putting this in my SlidePanelViewController. That didn't work. So I put it where I'm assuming it belongs in the ContainerViewController.
However when I click my signOutButton in my menu. I'm presented with the error:
fatal error: unexpectedly found nil while unwrapping an Optional value
When looking into the error. This is the line causing it:
self.navigationController!.pushViewController(mainTableViewController, animated: true)
After the error I checked that the function works, by adding a UINavigationBarButtonItem that called the function (in my ContainerViewController). It did exactly what I wanted.
However when I call this function from my Menu (again my menu is a childViewController of the ContainerViewController). It does not work.
I'm attempting to call it like so:
ContainerViewController().signOut()
I also tried adding a Delegate to my SidePanelViewController like this:
Before the class:
#objc protocol SidePanelViewControllerDelegate {
optional func needsSignOut(sender: SidePanelViewController)
optional func toggleLeftPanel()
optional func collapseSidePanels()
}
in viewDidLoad():
// Make sure your delegate is weak because if a ContainerViewController owns
// a reference to a SidePanelViewController and the container view controller
// is its delegate, you'll end up with a strong reference cycle!
weak var delegate: SidePanelViewControllerDelegate?
in my tap gesture function:
func signOutTapGesture() {
println("signOutTapGesture")
selectView(signOutView)
delegate?.needsSignOut?(self)
println(delegate)
}
before my ContainerViewController class:
var leftViewController: SidePanelViewController?
my ContainerViewController class:
class ContainerViewController: UIViewController, CenterViewControllerDelegate, SidePanelViewControllerDelegate, UIGestureRecognizerDelegate {
in my ContainerViewController's viewDidLoad()
leftViewController?.delegate = self
And I changed the signOut function in the ContainerViewController class to this:
func needsSignOut(sender: SidePanelViewController) {
println("needsSignOut called")
self.signOut()
}
However using the delegate like above, doesn't seem to do anything either.
Any help as to How I can successfully push my LandingPageVC from the menu would be greatly appreciated! (I'm not using storyboards)

You're attempting to call signOut with ContainerViewController().signOut(). This will create a new ContainerViewController and because you haven't pushed it onto the navigation controller's stack, navigationController is nil. Try just calling self.signOut(). (I'm assuming signOut in a method of ContainerViewController)
Update - delegates
Your delegate property should go in SidePanelViewController. I'll give you and example of how to implement it:
SidePanelViewController:
(Note - the protocol doesn't have to go here but I think it keeps things organised)
#objc protocol SidePanelViewControllerDelegate {
optional func needsSignOut(sender: SidePanelViewController)
}
class SidePanelViewController: UIViewController {
// Make sure your delegate is weak because if a ContainerViewController owns
// a reference to a SidePanelViewController and the container view controller
// is its delegate, you'll end up with a strong reference cycle!
weak var delegate: SidePanelViewControllerDelegate?
// Called when the UIButton is pressed.
func myButtonWasPressed() {
delegate?.needsSignOut?(self)
}
}
ContainerViewController:
class ContainerViewController: UIViewController {
var sidePanel: SidePanelViewController!
// Setup the side panel...
override func viewDidLoad() {
super.viewDidLoad()
sidePanel.delegate = self
}
func signOut() {
// Sign out stuff here.
}
}
// The ContainerViewController needs to conform to the SidePanelViewControllerDelegate
// protocol if we want the delegate to work. (This could have gone in the initial
// class declaration.)
extension ContainerViewController : SidePanelViewControllerDelegate {
func needsSignOut(sender: SidePanelViewController) {
self.signOut()
}
}
Hope that helps.

The problem seems to be that navigationController is nil and you're trying to force unwrap it (as indicated by your error).
One problem I discussed in my other answer.
Another problem may be you haven't added a navigation controller. To do this you need to:
If you're using Storyboards
You need to make sure you've embedded your UINavigationController. After that, when you use navigationController it won't be nil and you'll be able to push your view controller.
When you're on your storyboard:
Also, if you're using storyboards, have you considered using segues to move around instead of calling presentViewController? I've found it makes everything much easier.
If you're not using Storyboards
Have a look at this post: Programatically creating UINavigationController in iOS

Related

View Controller1 in a TabBarController won't automatically update view after ViewController2 is dismissed

SCENARIO
Xcode 11.5, Swift 5
Using Core Data
User wants to update their profile. VC2 is dismissed after user taps save. VC1 area highlighted in yellow should reflect the change.
PROBLEM
Data is being saved correctly. However, VC1 elements highlighted in yellow doesn't automatically update. If I go to another tab then come back, the view elements refresh with the updated changes.
MY CODE
I have a setupUI() method that lays out the elements and have tried adding it to VC1's viewWillAppear method, but no luck.
//VC1:
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
setupUI()
}
viewWillAppear is not called when you dimiss the modal that fill the data you need to use a delegate
1- When you show the modal vc
let vc = SomeVC()
vc.delegate = self // declare property delegate inside the modal of that type / protocol
// present
2- when you dimiss the modal
self.delegate?.setupUI()
// dimiss
You could use a delegate method to perform some changes in VC1 in response to some action in VC2. In this case you will set the delegate in VC1 and call the delegate method in VC2. Ideal place to make this call would be in completion block of dismiss.
//VC1
public protocol MyProtocol: class {
func delegateMethod()
}
In the viewDidLoad method set the delegate for VC2
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
setupUI()
//VC2 is the instance of view controller you are going to push from this page
VC2.delegate = self
}
Make sure VC1 confirms to MyProtocol protocol
extension VC1: MyProtocol {
func delegateMethod() {
// reload view here
}
}
Declare the delegate in VC2
//VC2
var delegate: MyProtocol?
Then call the delegate method in completion of dismiss
self.dismiss(animated: false, completion: {
self.delegate?.delegateMethod()
})
Alternatively you could use observers to respond to any changes as well, but that might be an overkill. Check out this article, they discuss the whole thing in detail.
setting vc2.modalPresentationStyle = .fullScreen will solve it without the need to make any delegates.

Not able to show View Container in Container View in iOS

I am taking reference from tutorial here.
I have one View Controller which contains a Container View. On this Container View I have added Custom Segue i.e. FirstViewController. That means when I opens View Controller it by default shows FirstViewController. And on FirstViewController, I have a Button. And by clicking on this button I want to show SecondViewController but I am not able to get this achieved. I have also added print command on Button click and it prints on console and not switch to another View. Please help.
I have created a delegate in FirstViewController and aa function which reference through ViewController.
Code for FirstViewController.swift
protocol FirstViewDelegate: class {
func sendToSecondViewController()
}
class FirstViewController: UIViewController {
weak var delegate: FirstViewDelegate? = nil
#IBAction func goToSecond(_ sender: UIButton) {
print("test1")
delegate?.sendToSecondViewController()
}
}
Code for ViewController.swift
extension ViewController: FirstViewDelegate{
func sendToSecondViewController() {
container.segueIdentifierReceivedFromParent("second")
}
}
And main.storyboard
Seems you have not set delegate of FirstViewController in ViewController. Something like:
<object of FirstViewController>.delegate = self (Inside ViewController)

Delegation from ContainerView to parent ViewController

I have the following setup:
StartViewController has a ContainerView that contains ContainerViewController
I try to find a way to hidden an element in StartViewController after a task is performed in ContainerViewController.
For this I try to use delegation method like this:
StartViewController
class StartViewController: UIViewController, showBannerAdDelegate {
#IBOutlet weak var bannerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
bannerView.hidden = false
}
func bannerAdHidden(status: Bool) {
bannerView.hidden = status
}
}
ContainerViewController
protocol showBannerAdDelegate: class {
func bannerAdHidden(status: Bool)
}
class ContainerViewController: UIViewController {
weak var delegate: showBannerAdDelegate! = nil
#IBAction func buttonPressed(sender: UIButton) {
delegate.bannerAdHidden(true)
}
}
If I presented the ContainerViewController I could do in prepareForSegue
let destination = segue.destinationViewController as! ContainerViewController
destination.delegate = self
But in this case both View Controller are always present.
What code should I add to the View Controller to make it work?
Thank you,
If one of the view controllers is inside a container view then it is loaded with an embed segue, which fires when the containing view controller is first loaded. The prepareForSegue method still gets called, so you can set up a delegate exactly as you've described. I always thought embed segues were a little odd (it's not really a segue, more like loading a child view controller) but that's how it works.

Swift - Passing value back to ViewController using protocol and delegate

I'm new to swift and my problem right now is I want to pass the value from RoutePreviewController page back to display on ViewController page after clicking on the Navigate Button. As below picture, you can see that the RoutePreviewController is not directly segue from the ViewController.
This is my story board of the swift project on xcode
However, I tried using protocol and delegate in the code.
Here are codes that I have add to the ViewController (MainPage)
protocol HandleSelectedPath{
func selectedPathDraw(selectedRoute:[(line:Int?,node:Int,time:Double)])
}
in viewDidLoad of ViewController
let routePreviewPage = storyboard!.instantiateViewControllerWithIdentifier("RoutePreviewController") as! RoutePreviewController
routePreviewPage.handleSelectedPathDelegate = self
and an extension outside the ViewController class
extension ViewController : HandleSelectedPath{
func selectedPathDraw(selectedRoute:[(line:Int?,node:Int,time:Double)]){
print("7687980")
selectedPath = selectedRoute
routeDraw()
}
}
And in RoutePreviewController, I have delegation of the protocol.
var handleSelectedPathDelegate: HandleSelectedPath? = nil
and the Navigate button action
#IBAction func navigateButtonAction(sender: AnyObject) {
handleSelectedPathDelegate?.selectedPathDraw(previewPath)
dismissViewControllerAnimated(true, completion: nil)
definesPresentationContext = true
}
As a result, after clicking the Navigate button, it does send me back to the ViewController page but the selectedPathDraw function of the protocol is not performed. I also tried printing some random string but nothing came up as an output.
The reference for your RoutePreviewController according to your code above only exist inside your viewDidLoad, you have to set as property instead like this:
var routePreviewPage: RoutePreviewController!
Always it's a good practice implement your delegate as a weak reference to avoid retain-cycles, so the correct way of implement your delegate and protocol should be as the following code:
protocol HandleSelectedPath: class {
func selectedPathDraw(selectedRoute:[(line:Int?,node:Int,time:Double)])
}
RoutePreviewController:
weak var handleSelectedPathDelegate: HandleSelectedPath?
#IBAction func navigateButtonAction(sender: AnyObject) {
handleSelectedPathDelegate?.selectedPathDraw(previewPath)
dismissViewControllerAnimated(true, completion: nil)
definesPresentationContext = true
}
viewDidLoad of ViewController:
routePreviewPage = storyboard!.instantiateViewControllerWithIdentifier("RoutePreviewController") as! RoutePreviewController
routePreviewPage.handleSelectedPathDelegate = self
I hope this help you.

How can you reload a ViewController after dismissing a modally presented view controller in Swift?

I have a first tableViewController which opens up a second tableViewcontroller upon clicking a cell. The second view controller is presented modally (Show Detail segue) and is dismissed with:
self.dismissViewControllerAnimated(true, completion: {})
At this point, the second view controller slides away and reveals the first view controller underneath it. I would then like to reload the first view controller. I understand that this may require use of delegate functions, but not sure exactly how to implement it
Swift 5:
You can access the presenting ViewController (presentingViewController) property and use it to reload the table view when the view will disappear.
class: FirstViewController {
var tableView: UITableView
present(SecondViewController(), animated: true, completion: nil)
}
In your second view controller, you can in the viewWillDisappear method, add the following code:
class SecondViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let firstVC = presentingViewController as? FirstViewController {
DispatchQueue.main.async {
firstVC.tableView.reloadData()
}
}
}
}
When you dismiss the SecondViewController, the tableview of the FirstViewController will reload.
I solved it a bit differently since I don't want that dependancy.
And this approach is intended when you present a controller modally, since the presenting controller wont reload when you dismiss the presented.
Anyway solution!
Instead you make a Singleton (mediator)
protocol ModalTransitionListener {
func popoverDismissed()
}
class ModalTransitionMediator {
/* Singleton */
class var instance: ModalTransitionMediator {
struct Static {
static let instance: ModalTransitionMediator = ModalTransitionMediator()
}
return Static.instance
}
private var listener: ModalTransitionListener?
private init() {
}
func setListener(listener: ModalTransitionListener) {
self.listener = listener
}
func sendPopoverDismissed(modelChanged: Bool) {
listener?.popoverDismissed()
}
}
Have you Presenting controller implement the protocol like this:
class PresentingController: ModalTransitionListener {
//other code
func viewDidLoad() {
ModalTransitionMediator.instance.setListener(self)
}
//required delegate func
func popoverDismissed() {
self.navigationController?.dismissViewControllerAnimated(true, completion: nil)
yourTableViev.reloadData() (if you use tableview)
}
}
and finally in your PresentedViewController in your viewDid/WillDisappear func or custom func add:
ModalTransitionMediator.instance.sendPopoverDismissed(true)
You can simply reaload your data in viewDidAppear:, but that might cause the table to be refreshed unnecessarily in some cases.
A more flexible solution is to use protocols as you have correctly guessed.
Let's say the class name of your first tableViewController is Table1VC and the second one is Table2VC. You should define a protocol called Table2Delegate that will contain a single method such as table2WillDismissed.
protocol Table2Delegate {
func table2WillDismissed()
}
Then you should make your Table1VC instance conform to this protocol and reload your table within your implementation of the delegate method.
Of course in order for this to work, you should add a property to Table2VC that will hold the delegate:
weak var del: Table2Delegate?
and set its value to your Table1VC instance.
After you have set your delegate, just add a call to the delegate method right before calling the dismissViewControllerAnimated in your Table2VC instance.
del?.table2WillDismissed()
self.dismissViewControllerAnimated(true, completion: {})
This will give you precise control over when the table will get reloaded.

Resources