Say we have a custom control, that at some state should pop the option list for user to choose from (UIAlertController). Usually when we are in a view controller we use presentViewController method, but in this case we have no access to a parent UIViewController which holds this method. Although there's seems to be some way to get to UIViewController from subviews, it's considered against MVC design pattern. Then how would you do this?
I want to keep view controller as lean as possible and custom view (uicontrol) self sufficient, so I wouldn't want to move this logic to view controller.
Although all your advice and suggestions are welcome and appreciated.
You can always use application rootviewController to present your alert view controller :) You dont always have to opt for your view controller's view :) rootview controller is always accessible no matter where your control is :)
Here is what you can do :)
Objective C
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[delegate.window.rootViewController presentViewController:your_alert_view_controller animated:YES completion:nil];
Swift
let appDelegate : AppDelegate? = UIApplication.sharedApplication().delegate as? AppDelegate
if let unwrappedAppdelegate = appDelegate {
unwrappedAppdelegate.window!.rootViewController! .presentViewController(alert, animated: true, completion: nil)
}
Hope my answer helped you :)
I would prefer delegation or target-action. It's much cleaner way because in this case you don't use an AppDelegate. Also it's not a subview's responsibility to show something. Example with delegation:
protocol MyViewDelegate: class {
func somethingHappenedToMyView(view: MyView)
}
class MyView: UIView {
weak var delegate: MyViewDelegate?
func somethingHappened() {
delegate?.somethingHappenedToMyView(self)
}
}
class ViewController: UIViewController, MyViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
myView.delegate = self
}
func somethingHappenedToMyView(view: MyView) {
presentViewController(someViewController, animated: true, completion: nil)
}
}
Example with target-action:
class MyView: UIControl {
func somethingHappened() {
sendActionsForControlEvents(.ValueChanged) // .ValueChanged is an example. Maybe other events will be more meaningful for your situation
}
}
class ViewController: UIViewController, MyViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
myView.addTarget(self, action: #selector(self.somethingHappenedToMyView(_:)), forControlEvents: .ValueChanged)
}
func somethingHappenedToMyView(view: MyView) {
presentViewController(someViewController, animated: true, completion: nil)
}
}
Presenting from delegate root view controller will not work in many cases (nested presentation, tab's more controller etc).
Better find parent UIViewController responder by looping through view's responder chain. Use this macro:
#define UIViewParentController(__view) ({ \
UIResponder *__responder = __view; \
while ([__responder isKindOfClass:[UIView class]]) \
__responder = [__responder nextResponder]; \
(UIViewController *)__responder; \
})
In view:
UIViewController *parentViewController = UIViewParentController(self);
Related questions: one, two.
Related
I have the following code to go back to the last view controller
self.navigationController?.popViewController(animated: true)
How do I send data back to the last view controller as I do this?
Swift relies a lot on the delegate pattern and this is a good place to use it.
class FirstViewController: UIViewController {
func pushToSecondViewController() {
let second = SecondViewController()
second.firstViewControllerDelegate = self // set value of delegate
navigationController?.pushViewController(second, animated: true)
}
func someDelegateMethod() {
print("great success")
}
}
class SecondViewController: UIViewController {
weak var firstViewControllerDelegate: FirstViewController? // establish a delegate
func goBackToFirstViewController() {
firstViewControllerDelegate?.someDelegateMethod() // call delegate before popping
navigationController?.popViewController(animated: true)
}
}
One common way is to use delegate pattern. Pass the viewController back with some data using the delegate method and dismiss it from “parent” ViewController.
See these links for extra daya about delegates
link1
link2
It is possible in some cases (iPhone X, iOS 13) to dismiss presented view controllers with a gesture, by pulling from the top.
In that case, I can't seem to find a way to notify the presenting view controller. Did I miss something?
The only I found would be to add a delegate method to the viewDidDisappear of the presented view controller.
Something like:
class Presenting: UIViewController, PresentedDelegate {
func someAction() {
let presented = Presented()
presented.delegate = self
present(presented, animated: true, completion: nil)
}
func presentedDidDismiss(_ presented: Presented) {
// Presented was dismissed
}
}
protocol PresentedDelegate: AnyObject {
func presentedDidDismiss(_ presented: Presented)
}
class Presented: UIViewController {
weak var delegate: PresentedDelegate?
override func viewDidDisappear(animated: Bool) {
...
delegate?.presentedDidDismiss(self)
}
}
It is also possible to manage this via notifications, using a vc subclass but it is still not satisfactory.
extension Notification.Name {
static let viewControllerDidDisappear = Notification.Name("UIViewController.viewControllerDidDisappear")
}
open class NotifyingViewController: UIViewController {
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: .viewControllerDidDisappear, object: self)
}
}
There must be a better way to do this?
From iOS 13 Apple has introduced a new way for the users to dismiss the presented view controller by pulling it down from the top. This event can be captured by implementing the UIAdaptivePresentationControllerDelegate to the UIViewController you're presenting on, in this case, the Presenting controller. And then you can get notified about this event in the method presentationControllerDidDismiss. Here is the code example :-
class Presenting: UIViewController, UIAdaptivePresentationControllerDelegate {
func someAction() {
let presented = Presented()
presented.presentationController?.delegate = self
present(presented, animated: true, completion: nil)
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Note:
This method only gets triggered for dismissing by swiping from the top and not for the programmatic dismiss(animated:,completion:) method.
You don't need any custom delegate or Notification observer for getting the event where the user dismisses the controller by swiping down, so you can remove them.
Adopt UIAdaptivePresentationControllerDelegate and implement presentationControllerDidAttemptToDismiss (iOS 13+)
extension Presenting : UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentingViewController.presentedDidDismiss(self)
}
}
UIPresentationController has a property presentingViewController. The name is self-explanatory. You don't need the explicit delegate protocol.
The method is actually called to be able to show a dialog for example to save changes before dismissing the controller. You can also implement presentationControllerDidDismiss()
And do not post notifications to controllers which are related to each other. That's bad practice.
I have a following class:
class HomeViewController: UIViewController {
override func viewdidload() {
super.viewdidload()
callOtherVC()
}
func callOtherVC() {
let viewController = StepsViewController()
let rootViewController = UINavigationController(rootViewController: viewController)
self.presentViewController(rootViewController, animated: true, completion: nil)
}
}
StepsViewController is just another viewcontroller. In StepsViewController, I try to dismiss current StepsViewController and present other viewcontroller. Following is code.
class StepsViewController: UIViewController {
override func viewdidload() {
super.viewdidload()
callSecondOtherVC()
}
func callSecondOtherVC() {
let vc = ViewController()
self.addChildViewController(vc)
self.parentViewController!.dismissViewControllerAnimated(true, completion: nil)
vc.callOtherVC()
}
}
I initialize ViewController() because I need to call same function callOtherVC from ViewController. Basically the model in ViewController changes but I'm essentially calling same UINavigationController from callOtherVC function.
Whenever I do this, I get an error like below:\
Warning: Attempt to present (UINavigationController: 0x7d991600) on
(HomeViewController: 0x7a6e00a0) whose view is not in the window
hierarchy!
UINavigationController is from callSecondOtherVC and HomeViewController is as it is.
How should I order the VCs? And if someone can more explain about the view hierarchy, I would greatly appreciate.
I think what you need to do here, is call your method from viewDidAppear, rather than viewDidLoad. The reason for this is that the view is not in the view hierarchy at the time of viewDidLoad.
I have a view controller which is nested inside of another view controller using a container view. Is it possible for me to segue from the view which is currently in the container view and replace it with another view controller in the same container view. I.e. the content that is around the container view is not removed by another view controller taking up the entire view.
Yes it is. You can read about that in the Apple Docs.
Considering your containerView currently only has one viewcontroller, here is a very basic example:
func loadVCWithId(idToLoad: String){
childViewControllers[0].willMoveToParentViewController(nil)
childViewControllers[0].view.removeFromSuperview()
childViewControllers[0].removeFromParentViewController()
let secondViewController = self.storyboard?.instantiateViewControllerWithIdentifier(idToLoad)
UIView.transitionWithView(yourContainer, duration: 0.5, options: UIViewAnimationOptions.TransitionFlipFromRight, animations: {self.yourContainer.addSubview((secondViewController?.view)!)}, completion: nil)
secondViewController!.view.frame = firstContainer.bounds
// do initialization of secondViewController here
secondViewController?.didMoveToParentViewController(self)
}
loadVCWithId(idToLoad:String)is a method within your host viewcontroller.
In this code fragment I delete the current content of the container (probably not the best way to just access index 0, but for the sake of this example, this should be enough), instantiate a new ViewController by ID (this one is present in my storyboard but not accessbile yet), animate the transition and actually add the new VC to the container.
Hope this helps.
this my solution maybe helpful for
first i create a protocol on childViewController
protocol ChildViewControllerDelaget
{
func performForSegue(SegueIdentifier:String)
}
class ChildViewController: UIViewController {
var delaget:ChildViewControllerDelaget?
override func viewDidLoad() {
super.viewDidLoad()
}
init()
{
}
#IBAction func myAction(sender: AnyObject) {
if delaget != nil {
deleget.performForSegue("mySegueIdentifier")
}
}
and on MainViewController
class ViewController: UIViewController,ChildViewControllerDelaget {
override func viewDidLoad()
{
super.viewDidLoad()
let child = ChildViewController()
child.delaget = self
}
func performForSegue(segueIdentifier:String)
{
self.performSegueWithIdentifier(segueIdentifier, sender: nil)
}
}
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.