Resolved Can't push view controller after overriding openURL

Discussion in 'iOS Programming' started by troop231, Jun 26, 2013.

  1. troop231, Jun 26, 2013
    Last edited: Jun 27, 2013

    troop231 macrumors 603

    Joined:
    Jan 20, 2010
    #1
    I'm trying to override my application's openURL method to intercept link clicks in a UITextView. The UITextView is in a navigation based DetailViewController, and when the user clicks the link, I want to push a web view onto the navigation stack.

    I verified that my method is being called by logging the intercepted url to the console, but the navigation controller is not pushing my WebViewController at all. I made a button in interface builder and added it onto the same view with the textview just to verify the WebView gets pushed. The button was just for testing purposes.

    The problem seems that the navigationController pushViewController code isn't getting fired when I call the method from the AppDelegate, even though the NSLog shows I'm getting the valid intercepted url.

    I appreciate any help offered! Code:

    Inside AppDelegate.m:

    Code:
    - (BOOL)openURL:(NSURL *)url
    {
        DetailViewController *detailView = [[DetailViewController alloc]init];
    
        detailView.url = url;
    
        [detailView push];
    
        return YES;
    }
    DetailViewController.h:

    Code:
    #import <UIKit/UIKit.h>
    
    @interface DetailViewController : UIViewController <UIGestureRecognizerDelegate>
    
    @property (nonatomic, strong) NSURL *url;
    
    - (void)push;
    
    @end
    DetailViewController.m:

    Code:
    - (void)push
    {
        WebViewController *webView = [[WebViewController alloc]    initWithNibName:@"WebViewController" bundle:[NSBundle mainBundle]];
    
        webView.url = self.url;
    
        NSLog(@"%@",self.url);
    
        [self.navigationController pushViewController:webView animated:YES];
    }
     
  2. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #2
    What is the value of self.navigationController in the push method?

    If it's nil, what happens?
     
  3. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #3
    self.navigationController is inherited in the detail controller, as all view controllers in the app inherit from a class called PKRevealController (nice project on github)

    The navigation controller is nil (confirmed by an NSLog) but I don't know why. The method only performs the logging of the url and the WebView doesn't get pushed. If I wire a button to the same method, it pushes wonderfully. :confused:
     
  4. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #4
    Something's wrong here.

    Your posted code:
    Code:
    @interface DetailViewController : [COLOR="Red"]UIViewController[/COLOR] <UIGestureRecognizerDelegate>
    
    Note the superclass in read.

    So either the statement "all view controllers in the app inherit from a class called PKRevealController" is wrong, or the code designating DetailViewController's superclass is wrong.

    Either way, you need to ask yourself the question, "How does the navigationController property get set?", and then discover the answer. You may have to use the debugger, or other debugging and logic skills to find an answer. The answer may be in your code, in the PKRevealController code, or in Apple's documentation.
     
  5. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #5
    Nothing's wrong, in the AppDelegate the reveal controller is set:

    AppDelegate.h
    Code:
    #import <UIKit/UIKit.h>
    
    @class RevealController;
    
    @interface AppDelegate : NSObject <UIApplicationDelegate>
    
    @property (nonatomic, strong, readwrite) RevealController *revealController;
    @property (nonatomic, strong) UIWindow *window;
    
    -(BOOL)openURL:(NSURL *)url;
    
    @end
    
    AppDelegate.m
    Code:
    UINavigationController *frontViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]];
        UIViewController *menuViewController = [[MenuViewController alloc] init];
        
        NSDictionary *options = @{
                                  RevealControllerAllowsOverdrawKey : [NSNumber numberWithBool:YES],
                                  RevealControllerDisablesFrontViewInteractionKey : [NSNumber numberWithBool:YES],
                                  RevealControllerRecognizesPanningOnFrontViewKey : [NSNumber numberWithBool:NO]
                                  };
        
        self.revealController = [RevealController revealControllerWithFrontViewController:frontViewController
                                                                         leftViewController:menuViewController
                                                                                    options:options];
        
        self.window.rootViewController = self.revealController;
     
  6. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #6
    Aha, I misinterpreted the word "inherit". You used it to mean "has an ancestor controller in the presentation hierarchy", not "inherits from a superclass".
     
  7. troop231, Jun 26, 2013
    Last edited: Jun 26, 2013

    troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #7
    No problem :) This has me stumped. Edit: My hunch is that by calling:
    Code:
    - (BOOL)openURL:(NSURL *)url
    {
        DetailViewController *detailView = [[DetailViewController alloc]init];
    
        detailView.url = url;
    
        [detailView push];
    
        return YES;
    }
    Is reinitializing the view controller that's already present on the stack, and therefore setting it's navController property to nil.

    How can I access the DetailViewController's navController without alloc initing the controller again? As I believe this is the problem.
     
  8. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #8
    If there's a DetailViewController already present on the stack, you need to get it and use it.

    You seem to be a little confused on the difference between a class (e.g. DetailViewController) and an instance of a class (e.g. the result of [DetailViewController alloc]). In particular, you seem to think the two things are interchangeable. They aren't.

    Since you haven't explained the controller hierarchy of your app, or what you're trying to accomplish, you should probably start by doing that. Without context, any suggestions for a remedy are either guesses or generalized (e.g. use a debugger).
     
  9. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #9
    Sure, I'll try to explain better. The DetailViewController is currently on the stack, and it contains a UITextView that has a link in it. The goal was to intercept this url and then push the WebViewController onto the stack and the webView loads it.

    I'm just not sure how to get to the current DetailViewControllers navigationController because calling
    Code:
    DetailViewController *detailView = [[DetailViewController alloc]init];
    in the AppDelegate sets what is already on the stack to nil.
     
  10. chown33, Jun 26, 2013
    Last edited: Jun 26, 2013

    chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #10
    Let me rephrase part of what you've written:
    There is a DetailViewController currently on the stack.
    It contains a UITextView, whose text is a link (or URL).​
    Note the use of "a DetailViewController", which means "an instance of DetailViewController", and does NOT mean "the class DetailViewController". You can't just make DetailViewController instances willy-nilly. You made an instance, so use the instance you made.

    At this point, you start writing about goals and intercepting a URL without bothering to explain what control is clicked, or what action is bound to the UITextView, or whether there are any controls at all. I'm hoping there's a button or some other control involved, otherwise I don't see how you get from "a UITextView in a DetailViewController" to doing something with the text that's in the UITextview.

    In any case, since there is a DetailViewController, and it contains a UITextView, and the text of that UITextView is the string for the URL of interest, that string should be available to that DetailViewController. After all, that is the fundamental purpose of controllers: to coordinate the views it contains, their contents, and their actions.

    I don't see why you don't connect a control or the UITextView itself to an IBAction method in the DetailViewController itself. That seems like the logical way to modularize this. Why take a side-trip through an AppDelegate method only to arrive at what should be the same DetailViewController instance that started things out? Why is AppDelegate's openURL: method being used at all? Everything you need already appears to be available in the DetailViewController instance, so why go elsewhere?

    Or maybe I'm missing something about the connections between your controllers.


    EDIT
    Nothing on the view-controller stack is being reinitialized. At least not from the posted code.

    You're making a new instance of DetailViewController, and expecting it to somehow magically take on all the same properties and characteristics of an existing DetailViewController. That's not how classes or instances work (except in very specialized and intentionally programmed circumstances, which do not appear here).

    When you do this:
    you get a completely new instance of DetailViewController. The purpose of the +alloc method is to create new instances.

    The -init method is then called on the new instance, which tells that new instance to initialize itself. The resulting new initialized instance is then assigned to the detailView variable you declared.

    At this point, nothing has happened to the view-controller stack, unless there's something magic in the -init method. I know of no such magic, but I could be wrong.

    You should probably look at the docs for the superclass of DetailViewController, i.e. UIViewController, and see whether it's valid to use the plain -init method on it, or whether there's a designated initializer you ought to use instead.
     
  11. Duncan C macrumors 6502a

    Duncan C

    Joined:
    Jan 21, 2008
    Location:
    Northern Virginia
    #11
    chown,

    I think the OP is hoping to intercept the system action that opens a clickable link in a UITextView and handling it himself.

    To the OP:

    I've never tried to do this, but looking at the docs, it looks like you would need to create a subclass of UIApplication and override it's implementation of openURL.

    The UIApplicationDelegate Protocol doesn't have a method

    - (BOOL)openURL:(NSURL *)url

    So if you put a method like that in your application delegate, I would not expect it to get called at all.

    The docs on the UIApplication class do say that you can override it, but pretty strongly discourage it. To quote the docs:

    I've been doing iOS development pretty much full time since 2009, and never had an occasion to subclass UIApplication.

     
  12. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #12
    Duncan C understands what I'm trying to do, as it's required to subclass UIApplication's openURL so that links embedded within a UITextView don't open up in the Safari app. I've spent all day trying to fix this to no avail
     
  13. Duncan C macrumors 6502a

    Duncan C

    Joined:
    Jan 21, 2008
    Location:
    Northern Virginia
    #13
    So post the steps you used in order to subclass UIApplication.

    It looks like you need to change your call to UIApplicationMain so the third parameter is the name of your custom subclass of UIApplication.

    Then you would need to include .h and .m files for your custom subclass of UIApplication. For your setup you might only need to implement the OpenURL method in that custom subclass (not in your application delegate glass, but in your custom UIApplication subclass, which is something different.)

    Disclaimer: As I said, I've never had to do this, and the above is from about 2 minutes poking around in the docs.
     
  14. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #14
    Hi Duncan, here is the code I'm using to override and sublcass UIApplication:

    main.m:
    Code:
    #import <UIKit/UIKit.h>
    #import "Application.h"
    
    int main(int argc, char *argv[])
    {
        @autoreleasepool
        {
            return UIApplicationMain(argc, argv, NSStringFromClass([Application class]), NSStringFromClass([AppDelegate class]));
        }
    }
    Application.h:
    Code:
    #import "AppDelegate.h"
    
    @interface Application : UIApplication {}
    
    @end
    
    @implementation Application
    
    -(BOOL)openURL:(NSURL *)url{
        if  ([(AppDelegate*)self.delegate openURL:url])
            return YES;
        else
            return [super openURL:url];
    }
    @end
    AppDelegate.h:
    Code:
    #import <UIKit/UIKit.h>
    
    @class RevealController;
    
    @interface AppDelegate : NSObject <UIApplicationDelegate>
    
    @property (nonatomic, strong, readwrite) RevealController *revealController;
    @property (nonatomic, strong) UIWindow *window;
    
    - (BOOL)openURL:(NSURL *)url;
    
    @end
    and finally in Application.m:
    Code:
    #import "AppDelegate.h"
    #import "RevealController.h"
    #import "ViewController.h"
    #import "DetailViewController.h"
    
    @implementation AppDelegate
    
    - (BOOL)openURL:(NSURL *)url
    {
        DetailViewController *detailView = [[DetailViewController alloc]init];
    
        detailView.url = url;
        
        [detailView push];
        
        return YES;
    }
     
  15. Duncan C macrumors 6502a

    Duncan C

    Joined:
    Jan 21, 2008
    Location:
    Northern Virginia
    #15
    At a glance it looks reasonable. Interesting idea passing the openURL message to the app delegate.

    One suggestion: Add a call to respondsToSelector before you send the openURL to the app delegate. OpenURL is not part of the UIApplicationDelegate protocol, so you shouldn't assume the app delegate responds to it.

    By doing that, if your app delegate implements an openURL call it will get called, and it won't if it doesn't.

    Your modified Application class implementation might look like this:

    Code:
    @implementation Application
    
    -(BOOL)openURL:(NSURL *)url{
        if  ([self.delegate respondsToSelector: @selector(openURL:) &&
        [(AppDelegate*)self.delegate openURL:url])
            return YES;
        else
            return [super openURL:url];
    }
    @end
    [/CODE]

    You should probably also add a prefix to the name of your Application class. That name is common enough you are likely to get a "namespace collision".

    If your initials are JBC, call the class JBCApplication, or use the letters of your company name for work projects.

    In my company, WareTo, we use "WT" as a class name prefix when we need to be concerned about name conflicts.


     
  16. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #16
    Thank you for the helpful tips Duncan! I verified that all the methods are getting called, but the nav controller is still nil when I try to push the webView in the DetailViewController:

    DetailViewController.m:
    Code:
    - (void)push
    {
        NSLog(@"This is being called");
        
        WebViewController *webView = [[WebViewController alloc] initWithNibName:@"WebViewController" bundle:[NSBundle mainBundle]];
        
        webView.url = self.url;
        
        if (self.navigationController == nil)
        {
            NSLog(@"Confused");
        }
        
        [self.navigationController pushViewController:webView animated:YES];
    }
    
     
  17. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #17
    @troop, You don't understand the view controller life cycle. The whole idea of a view controller pushing itself onto its own navigation controller is flawed. A view controller doesn't get assigned a navigation controller until it's been pushed. This needs to be done another way.
     
  18. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #18
    My DetailViewController already has a navigation controller, so why can't I push the WebViewController using the already available one?
     
  19. Duncan C macrumors 6502a

    Duncan C

    Joined:
    Jan 21, 2008
    Location:
    Northern Virginia
    #19
    You're very confused. A navigation controller is a container. Think of a navigation controller as a stack of plates. You create a new plate (view controller) and push it onto the stack.

    A view controller can't contain it's own navigation controller. That would be like a plate containing it's stack. It doesn't make sense.

    You don't tell a view controller to push itself. You create a view controller and push it onto an already existing navigation controller.

    Diagram out your view controller hierarchy. What is the root view controller that everything attaches to? Where is/ the navigation controller(s), and what view controllers will your navigation controller contain?

    If you can't diagram it, you won't be able to code it.
     
  20. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #20
    @troop, yes your existing DetailViewController has a navigation controller. This new one that you're making in your push method does not.

    Apparently, you need to communicate to your existing DetailViewController to push the web view. You could do that with a NSNotification or with a direct method call.
     
  21. troop231 thread starter macrumors 603

    Joined:
    Jan 20, 2010
    #21
    Code:
    
    
    PhoneyDeveloper, thank you so much! NSNotification was the answer that solved this mess for me. I can't believe I overlooked it.

    Works beautifully now. Code for others who find this thread later:

    AppDelegate.m:
    Code:
    - (BOOL)openURL:(NSURL *)url
    {
        [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:@"WebViewNotification" object:url]];
        
        return YES;
    }
    DetailViewController.m:
    Code:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(webViewNotification:) name:@"WebViewNotification" object:nil];
    }
    
    - (void)webViewNotification:(NSNotification *)notification
    {
        NSURL *url = [notification object];
        
        WebViewController *webView = [[WebViewController alloc] initWithNibName:@"WebViewController" bundle:[NSBundle mainBundle]];
        
        webView.url = url;
        
        [self.navigationController pushViewController:webView animated:YES];
    }
    
    
     

Share This Page