4

I am using a Class which is a subclass of MessageView (Swift Message Library) which is inherit from UIView. Inside, I have a UIButton and I want to present programmatically another ViewController through it.

Here is my code below :

import Foundation
import SwiftMessages
import UIKit

class MyClass: MessageView {

    var hideBanner: (() -> Void)?


    @IBAction func helpButtonPressed(_ sender: UIButton) {
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "newViewController") as! NewViewController
        self.present(newViewController, animated: true, completion: nil)

    @IBAction func tryAgainButtonPressed(_ sender: UIButton) {
        hideBanner?()
    }

    open override func awakeFromNib() {

    }

}

I have tried this, but it is not working since the UIView do not have the present method.

Timothy Moose
  • 9,895
  • 3
  • 33
  • 44
Blisko
  • 107
  • 2
  • 9
  • 4
    Possible duplicate of [Using presentViewController from UIView](https://stackoverflow.com/questions/15622736/using-presentviewcontroller-from-uiview) – Abdelahad Darwish May 07 '18 at 07:33
  • 2
    I think it's a bad pattern to do so. You should use delegation which conformed from view controller than you can use present method to present your controller. – serhat sezer May 07 '18 at 07:33
  • Hope you are not struggling with this.. the answers given focus on the idea of the view communicating to the controller that something needs to happen. The controller should take the actual action to make it happen. Don't forget to upvote the answers. – Tommie C. Dec 25 '18 at 13:12

7 Answers7

2

First get top ViewController using this. Then you can present your viewController.

if var topController = UIApplication.sharedApplication().keyWindow?.rootViewController {
    while let presentedViewController = topController.presentedViewController {
        topController = presentedViewController
    }

    // topController now can use for present.
    let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let newViewController = storyBoard.instantiateViewController(withIdentifier: "newViewController") as! NewViewController
    topController.present(newViewController, animated: true, completion: nil)
}
Muhammad Shauket
  • 2,643
  • 19
  • 40
  • This can be somewhat brittle in a number of ways. Multiple storyboards, navigation controllers, etc. Walking the controller/view hierarchy can get very complex. – Tommie C. Dec 25 '18 at 13:03
  • 1
    This one worked for me, had to use self.present....... in order to make it work. – David_2877 Jun 16 '20 at 15:17
1

.present is a method in UIViewController class, that's the reason you cannot present view controller from UIView class.

To achieve this, get the root view controller and present the controller as follows:

let appDelegate  = UIApplication.shared.delegate as! AppDelegate
let viewController = appDelegate.window!.rootViewController as! YourViewController
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "newViewController") as! NewViewController
        viewController .present(newViewController, animated: true, completion: nil)
pkc456
  • 8,350
  • 38
  • 53
  • 109
  • Thanks but MyClass still does not have the present method: "Value of type 'MyClass' has no member 'present'" – Blisko May 07 '18 at 07:39
  • 1
    Replace self with `viewController ` as written in my updated answer. Sorry for typo. – pkc456 May 07 '18 at 07:44
1

The iOS convention is that only a ViewControllers presents another ViewController.

So the answers above - where the View is finds the current ViewController via UIApplication.sharedApplication().keyWindow?.... will work but is very much an anti-pattern.

The preferred way would be:

  • Your MyClass view has presentation code only
  • You must have a ViewController which has a reference to this MyClass view
  • This ViewController has the @IBAction func tryAgainButtonPressed
  • From there, you can present the next ViewController
Matthew
  • 1,363
  • 11
  • 21
1

Try this #simple code.

import Foundation
import SwiftMessages
import UIKit

class MyClass: MessageView {

var hideBanner: (() -> Void)?


@IBAction func helpButtonPressed(_ sender: UIButton) {
    let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let newViewController = storyBoard.instantiateViewController(withIdentifier: "newViewController") as! NewViewController
    UIApplication.shared.keyWindow?.rootViewController?.present(newViewController, animated: true, completion: nil)
}

@IBAction func tryAgainButtonPressed(_ sender: UIButton) {
    hideBanner?()
}

open override func awakeFromNib() {

}

}
SPatel
  • 4,768
  • 4
  • 32
  • 51
  • Would be better if you show the Controller logic driving the storyboard calls. That ought to come from the Controller not the view. Using the closure object to manage the callback is useful to notify the controller object when to make that new presentation. – Tommie C. Dec 25 '18 at 13:04
1

Here is the example code using delegation pattern.

class YourViewController: UIViewController {
  var yourView: MyClass // may be outlet

    override func viewDidLoad() {
        super.viewDidLoad()

        yourView.delegate = self
    }

}

protocol MyClassDelegate:class {
    func tryAgainButtonDidPressed(sender: UIButton)
}

class MyClass: MessageView {

  weak var delegate: MyClassDelegate?


   @IBAction func tryAgainButtonPressed(_ sender: UIButton) {
        delegate?.tryAgainButtonDidPressed(sender: sender)
    }

}
Satish
  • 2,015
  • 1
  • 14
  • 22
1

You can achieve this by two ways

  • Protocol
  • By giving reference of that view controller to the view when you are initializing view
JeeVan TiWari
  • 325
  • 2
  • 15
1

Sorry for the late reply. MessageView already provides a buttonTapHandler callback for you:

/// An optional button tap handler. The `button` is automatically
/// configured to call this tap handler on `.TouchUpInside`.
open var buttonTapHandler: ((_ button: UIButton) -> Void)?

@objc func buttonTapped(_ button: UIButton) {
    buttonTapHandler?(button)
}

/// An optional button. This buttons' `.TouchUpInside` event will automatically
/// invoke the optional `buttonTapHandler`, but its fine to add other target
/// action handlers can be added.
@IBOutlet open var button: UIButton? {
    didSet {
        if let old = oldValue {
            old.removeTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside)
        }
        if let button = button {
            button.addTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside)
        }
    }
}

which is automatically invoked for any button you connect to the button outlet. So the recommended method for presenting another view controller is to have the presenting view controller configure the presentation logic in this callback:

messageView.tapHandler = { [weak self] in
    guard let strongSelf = self else { return }
    let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let newViewController = storyBoard.instantiateViewController(withIdentifier: "newViewController") as! NewViewController
    strongSelf.present(newViewController, animated: true, completion: nil)
}

If your view has more than one button, you can handle them all through buttonTapHandler since it takes a button argument. You just need to configure the target-action mechanism for each button:

otherButton.addTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside)

Or you can add one callback for each button by duplicating the above pattern.

Timothy Moose
  • 9,895
  • 3
  • 33
  • 44