Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

troop231

macrumors 603
Original poster
Jan 20, 2010
5,826
561
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];
}
 
Last edited:
What is the value of self.navigationController in the push method?

If it's nil, what happens?

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:
 
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)

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.
 
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.

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;
 
Aha, I misinterpreted the word "inherit". You used it to mean "has an ancestor controller in the presentation hierarchy", not "inherits from a superclass".

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.
 
Last edited:
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.

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).
 
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).

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.
 
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.

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
Is reinitializing the view controller that's already present on the stack, and therefore setting it's navController property to nil.
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:
DetailViewController *detailView = [[DetailViewController alloc]init];
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.
 
Last edited:
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:

Subclassing Notes
You might decide to subclass UIApplication to override sendEvent: or sendAction:to:from:forEvent: to implement custom event and action dispatching. However, there is rarely a valid need to extend this class; the application delegate (UIApplicationDelegate is sufficient for most occasions. If you do subclass UIApplication, be very sure of what you are trying to accomplish with the subclass.

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

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.
 
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
 
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

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.
 
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.

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;
}
 
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.


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;
}
 
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:

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];
}
 
@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.
 
@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.

My DetailViewController already has a navigation controller, so why can't I push the WebViewController using the already available one?
 
My DetailViewController already has a navigation controller, so why can't I push the WebViewController using the already available one?

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.
 
@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.
 
Code:
@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.

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];
}
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.