CosmicMind / Material

A UI/UX framework for creating beautiful applications.
http://cosmicmind.com
MIT License
11.99k stars 1.26k forks source link

Possibility to make the SnackbarController from a UITabBarController ? #611

Closed Arkezis closed 7 years ago

Arkezis commented 7 years ago

Hi,

I want to use your awesome SnackBar by using the SnackbarController but my main VC is a UITabBarController ! Can you help me on this ?

Moreover, do I need to instanciate my UITabBarController manually in AppDelegate ? (it's not the case for now ...)

Thanks a lot :)

daniel-jonathan commented 7 years ago

I'll be back later today and can help you out :)

daniel-jonathan commented 7 years ago

Hey, so to do what you are looking for is relatively easy. In your AppDelegate, you can instantiate a SnackbarController and set the rootViewController to your UITabBarController. For example:

import UIKit
import Material

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func applicationDidFinishLaunching(_ application: UIApplication) {
        window = UIWindow(frame: Screen.bounds)
        window!.rootViewController = SnackbarController(rootViewController: MyTabBarController())
        window!.makeKeyAndVisible()
    }   
}

If you are using storyboards, you will want to do something like this:

import UIKit
import Material

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func applicationDidFinishLaunching(_ application: UIApplication) {
        window = UIWindow(frame: Screen.bounds)
        window!.rootViewController = SnackbarController(rootViewController: UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MyTabBarController"))
        window!.makeKeyAndVisible()
    }   
}

Once that is all working, you can access the SnackbarController with the optional UIViewController property snackbarController?.snackbar.

For your initial view controller, be mindful that you will probably need to access the snackbarController?.snackbar property from the viewWillAppear method because the viewDidLoad method will not have access because the MyTabBarController won't be connected to the SnackbarController just yet.

You can look at this Programmatic SnackbarController Sample for a programmatic approach, and this Storyboard SnackbarController Sample for a storyboards approach.

Hope that helps and feel free to reopen or create a new issue if you need any further help :)

Arkezis commented 7 years ago

Ok, it's fine now, I didn't really understood how I can handle it with all my storyboards. The main info here is that the root must be a SnackbarController :)

Thanks for your help !

daniel-jonathan commented 7 years ago

You can place the SnackbarController at any layer you like, and in this case, I think it makes most sense to be the lowest layer at the window's rootViewController. In some cases you may even want to add the SnackbarController as the base of each tab. So feel free to explore combinations, the SnackbarController is designed to fit anywhere based on needs.

codemodouk commented 7 years ago

Hi, sorry for hijacking this thread, but, it's relevant to my issue. I have a similar setup where I have a UITabBarController and I add that as the rootVC of my AppSnackbarController in appDidFinishLaunching. This is all working fine. However, if I navigate to one of my UITabBarController children, and then navigate to a child of that VC via a segue in my Storyboard, my snackbarController property is no longer visible to the child VC. Can you tell me how I make the snackbarController visible to VCs at lower levels in my app?

daniel-jonathan commented 7 years ago

@codemodouk are you trying to access the Snackbar through the viewDidLoad method? If so, it may not be accessible because the view controller has not been added to the hierarchy at that time. Try the viewWillAppear method, and use the optional snackbarController property found as a UIViewController extension.

codemodouk commented 7 years ago

I will double check when I'm in front of my desktop, but If I recall, I'm calling this long after the viewDidLoad event.

I take it from your response that I should be able to get a handle to the snackbar controller object on all child navigation controllers of the UITabBarController. Is that correct?

Sent from my iPhone

On 9 Mar 2017, at 18:34, Daniel Dahan notifications@github.com wrote:

@codemodouk are you trying to access the Snackbar through the viewDidLoad method? If so, it may not be accessible because the view controller has not been added to the hierarchy at that time. Try the viewWillAppear method, and use the optional snackbarController property found as a UIViewController extension.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

daniel-jonathan commented 7 years ago

@codemodouk yes, you should be able to get a handle so long as the view controller is a child of the SnackbarController in the view hierarchy. Keep me posted :)

codemodouk commented 7 years ago

@danieldahan I've tested this further and can provide some additional information.

I have a Storyboard setup as follows:

UITabBarController -> UINavigationController -> UIViewController_Tab1 -> UINavigationController -> UIViewControllerChild

My app delegate instantiates the UITabBarController from the Storyboard and makes this the rootController of my AppSnackbarController object.

If I show the snackbar in UIViewController_Tab1 this works perfectly.

If I navigate to UIViewControllerChild via a modal segue that was created in the Storyboard and attempt to show the snackbar, nothing is displayed.

Is that expected behaviour?

codemodouk commented 7 years ago

@danieldahan - I don't mean to pressure, but any thoughts on this?

daniel-jonathan commented 7 years ago

No worries. Where are you accessing the snackBarController property in the UIViewControllerChild?

codemodouk commented 7 years ago

Yes, in UIViewControllerChild.

daniel-jonathan commented 7 years ago

If you are accessing it in the viewDidLoad it may not actually be connected yet, which means that the optional snackbarController won't be available. So put that code in the viewWillAppear and it should be accessible.

Let me know if that works for you. Otherwise, can you share your code in that view controller so I can see what is going on.

Please reopen the issue if you need :)

codemodouk commented 7 years ago

SnackbarTest.zip

I've uploaded a sample project which demonstrates my setup. You will see that I'm accessing the snackbarController from buttons in the view controllers, so the property should have been initialised.

codemodouk commented 7 years ago

Sorry to hassle, but, do you have any update on this at all? If you've got concerns about opening the zip, I understand, and would be happy to provide by another channel if that helps. Please let me know.

daniel-jonathan commented 7 years ago

No worries, I am testing it again now.

daniel-jonathan commented 7 years ago

So there are two issues here:

  1. Because you are presenting the view it actually doesn't have access to the SackbarController, which can be remedied by doing this:
func animateSnackbar() {
        guard let sc = self.snackbarController ?? presentingViewController?.snackbarController else {
            return
        }

        _ = sc.animate(snackbar: .visible, delay: 1)
        _ = sc.animate(snackbar: .hidden, delay: 4)
    }

    func showSnackbar(withText text: String) {
        guard let snackbar = self.snackbarController?.snackbar ?? presentingViewController?.snackbarController?.snackbar else {
            return
        }

        snackbar.text = text
        animateSnackbar()
    }

You will see that I am using the presentingViewController to access the snackbarController if it doesn't find it in the current view controller.

  1. The second issue is that the presented view is above the view of the presenting view hierarchy. I would try presenting the view controller using the snackbarController itself, so you will need to update your storyboard.

The other option is to wrap the presented UINavigationController with another SnackbarController, and you will be able to reuse the same code in your extension. I recommend this one.

Let me know what you choose and if you need, please reopen the issue. Thank you!

codemodouk commented 7 years ago

Thanks for looking into this for me. The approach you suggest of wrapping the UINavigationController in a SnackbarController I am doing elsewhere in my app when I present the childVC from a parentVC that implements the UIPopoverPresentationDelegate protocol.

However, in general, I'm trying to use the Storyboard where possible. I've attempted your recommended approach in the prepare(for segue: sender), but if I wrap the segue.destination (A UINavigationController) in a new SnackbarController, I get the error:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally an active controller <SnackbarTest.GreenViewController: 0x7fdc4540a0a0>.'

I also tried subclassing UINavigationController to create a SnackbarNavigationController. However, this complains too because the snackbarController isn't open, so I get the error:

Overriding non-open var outside of it's defining module.

Have I misunderstood your suggestion? Sorry to be a noob pain, I would really like to make use of this feature, but keep hitting brick walls.

daniel-jonathan commented 7 years ago

Hey, no worries, trying is how you learn. You should in your segue launch the SnackbarController that wraps the UINavigationController, and not have it do it from storyboards as well. Try that, and let me know.

codemodouk commented 7 years ago

You've totally thrown me now! Can you provide an example of how I can wrap the UINavigationController in the segue?

In the example project I uploaded, I added the following:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let navVC = segue.destination as? UINavigationController {
            let _ = AppSnackbarController(rootViewController: navVC)       
        }
    }

But I got this error:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally an active controller <SnackbarTest.GreenViewController: 0x7fdc4540a0a0>.'

Thanks once again for any help you can provide.

daniel-jonathan commented 7 years ago

Seems like it can't find this GreenViewController. Maybe you need to remove that reference as that is probably from the sample code. As for the code, that looks like the correct idea if not the correct way. I don't do much with storyboards. See if you can remove the GreenViewController reference, and if you still have an issue, please let me know. Thank you!

codemodouk commented 7 years ago

Hey, thanks for the feedback I really appreciate it.

Sent from my iPhone

On 28 Mar 2017, at 21:17, Daniel Dahan notifications@github.com wrote:

Seems like it can't find this GreenViewController. Maybe you need to remove that reference as that is probably from the sample code. As for the code, that looks like the correct idea if not the correct way. I don't do much with storyboards. See if you can remove the GreenViewController reference, and if you still have an issue, please let me know. Thank you!

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

carbonimax commented 7 years ago

Hi @codemodouk,

After all, did you find a solution ? I have the same issue.

daniel-jonathan commented 7 years ago

If anyone would like to send a sample over that reproduces this issue, I'd be happy to help. Thanks!

codemodouk commented 7 years ago

Hi @carbonimax, unfortunately, I haven't solved this yet by using Storyboards and segues. I can workaround it by conforming my source VC to UIPopoverPresentationControllerDelegate and presenting my target VC as a popover. Using this approach, I can make use of UIPopoverPresentationControllerDelegate function

func presentationController(UIPresentationController: UIModalPresentationStyle)

and wrap my targetVC in a SnackbarController and make that the rootViewController of a UINavigationController

So something like this:

func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
        let navigationController = UINavigationController(rootViewController: AppSnackbarController(rootViewController: controller.presentedViewController))
return navigationController
}

func presentationController(UIPresentationController: UIModalPresentationStyle)

@danieldahan - I assume @carbonimax is struggling with the same issue I presented in the example project contained in the SnackbarTest.zip attached to this thread.

codemodouk commented 7 years ago

Hi @carbonimax, you must have given me some inspiration. Try this, I've just got it working:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let navVC = segue.destination as? UINavigationController {
            let asc = AppSnackbarController(rootViewController: navVC.topViewController!)
            navVC.viewControllers.insert(asc, at: 0)
        }
}

Let me know if it works for you too.

carbonimax commented 7 years ago

Hi,

Thank you for your replies.
I found a working solution. I use a custom segue in IB with this class :

import Material

class SnackbarSegue: UIStoryboardSegue {
    override func perform() {
        let firstVCView = self.source.view as UIView!
        let secondVCView = self.destination.view as UIView!
        let screenWidth = UIScreen.main.bounds.size.width
        let screenHeight = UIScreen.main.bounds.size.height
        secondVCView?.frame = CGRect(x: 0.0, y: screenHeight, width: screenWidth, height: screenHeight)
        let window = UIApplication.shared.keyWindow
        window?.insertSubview(secondVCView!, aboveSubview: firstVCView!)
        self.source.present(SnackbarController(rootViewController: self.destination as UIViewController), animated: true, completion: nil)
    }
}
codemodouk commented 7 years ago

I prefer your approach to mine as it keeps the implementation details outside of the view controller. I've modified it to suit my needs, so, thought I would share in case anyone else has a similar requirement:

class SnackbarSegue: UIStoryboardSegue {
    override func perform() {
        self.source.present(SnackbarController(rootViewController: self.destination as UIViewController), animated: true, completion: nil)
    }
}