4

I have a subclass of UIViewController that is responsible for a single UIWebView.

Since this is a simple case, I override -(void)loadView, instantiate the UIWebView and assigning it the controller's view property:

- (void)loadView 
{
    UIWebView *wv = [[[UIWebView alloc] initWithFrame:self.frame] autorelease];
    // other configuration here...  
    self.view = wv;
}

This is fine until I call a method of UIWebView's. For example...

[self.view loadHTMLString:HTMLString baseURL:baseURL];

...leads to a compiler warning...

warning: 'UIView' may not respond to '-loadHTMLString:baseURL:'

...since the view property is declared as UIView.

NOW the warning is easily solved with a cast...

[(UIWebView *)self.view loadHTMLString:HTMLString baseURL:baseURL];

...but what I'd like to do is provide the correct type hint in the interface. I tried overriding the view property in MyViewController.h but this upsets the compiler too:

warning: property 'view' type does not match super class 'UIViewController' property type

Is there any way of telling the compiler (and my fellows) that this is what I'm doing, and that I know that this is what I'm doing and that it's all okay? (If not I guess I'll stick with the cast.)

TIA

EDIT: I tried redeclaring the view property as per marcus.ramsden's answer: this eliminated the warning (and the need for the cast) but stopped the view appearing at all! I'm not sure why this should be as the controller will still return a UIView (subclass) when asked for it...

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
Carlton Gibson
  • 7,278
  • 2
  • 36
  • 46
  • Why don't you add `UIWebView` instance as subview? – knuku Nov 23 '10 at 10:09
  • @NR4TR Two reasons: the extra UIView would add nothing except complexity, and I'd want an extra `@property` to hold a reference to the web view anyway. It's cleaner just to use the UIWebView directly and reuse the view property in the process. (All I need is a way to tell the compiler that so I can do away with the casts.) – Carlton Gibson Nov 23 '10 at 10:38

6 Answers6

5

The short answer is there's no reasonable way to do this. And finding an unreasonable way to do it will create more problems than it will solve. I realize this isn't the answer you want, but for the benefit of anyone who reads this question in the future I'll explain why I believe you shouldn't do this.

The view property is part of the public interface of UIViewController, and the type of the return value is part of that interface. Changing the public interface of a class which you've subclassed is smelly. If that's not reason enough for you, consider that the view property of a view controller is a fundamental bit of UIKit; there are few things you could mess with that could potentially have a more systemic effect on the framework. Who knows what sort of dark machinations lie under the façade of that simple interface getter? Based on some of the comments already left here it seeems that redefining this property breaks things in obvious ways; I would stay away.

If you come from a language like ruby, the fact that you can't send an arbitrary message to the object returned by the view property without a compiler warning probably seems unsatisfying. However, for better or for worse, Objective C is a statically (if not particularly strongly) typed language. When you're working with a particular tool it's worth trying to work to that tool's strengths, and avoid its weaknesses, rather than trying to shoehorn it into another tool's idioms.

If, on the other hand, you come from a strongly typed language like C++, that cast should feel extremely unsatisfactory as well. Casts subvert the type system that statically typed languages lean heavily on, and are nearly always a nasty code smell. C-style casts are the nuclear weapon of the C type system; they override absolutely everything, and they are nearly always the wrong thing to use.

What if you decide in the future that you want to add a label to your view along with the web view? In that case you'll need to move the web view to be a subview; but if you're casting the view controller's base view to UIWebView everywhere, then your casts are now lies. Sadly, the compiler won't help you find those lies, because C-style casts are unchecked; you can make a UIWebView out of a UIView, an int, a block, a function pointer, or a rhinocerous and the compiler won't make a peep. Your app will crash mightily, though.

Finally, you say you're interested in avoiding complexity. Consider that adding your web view as a subview of the base view would require only that you declare (and synthesize) a single property and add the subview in viewDidLoad. Given that small amount of effort, all calls to your web view look like this:

[self.webView loadHTMLString:HTMLString baseURL:baseURL];

Alternately, if you stick with casting, each call to your web view will look like this:

[(UIWebView *)self.view loadHTMLString:HTMLString baseURL:baseURL];

The former seems less complex to me, and less brittle.

Adam Milligan
  • 2,826
  • 19
  • 17
  • 1
    I came here to say this. In our apps that have a dedicated WebController (subview of UIViewController) class we add the UIWebView to self.view and create a property for the web view in the WebController interface. Typecasting has its place to be sure but in this instance the truly elegant solution favors clearer, non-hackish code rather than worrying about the overhead of one additional container view (which is so minimal as to be insignificant). – par Dec 01 '10 at 02:26
  • Thank you for your answer and explanation. It all makes sense. @par: Thanks for your comments on this question, they have been very helpful. – Carlton Gibson Dec 01 '10 at 09:31
2

Since it's a subclass, you can override the view property/method to be a UIWebView. Since a UIWebView is a subclass of UIView, and you're the only user of your subclass, this should be completely safe.

@interface MyAbstractViewController : UIViewController {
    UIWebView *_webView;
}
@property (nonatomic, retain) UIWebView *view;
@end

@implementation MyAbstractViewController    
@dynamic view;
- (void)setView:(UIWebView *)webView {
  [super setView:webView];
  _webView = webView;
}

- (UIWebView *)view {
  return _webView;
}
@end
August Lilleaas
  • 54,010
  • 13
  • 102
  • 111
  • This was the type of thing I was originally aiming for. I tried overriding the `view` property but ended up getting the second type of warning mentioned in my question -- two differences from my attempt are the naming of the `ivar` and the use of `@dynamic`; I'm going to go with but Adam Milligan's answer but I will experiment with your suggestion. Always learning. Thank you! – Carlton Gibson Dec 01 '10 at 09:27
  • With `@dynamic` here, it's actually not required that you provide an implementation. You can just re-declare the property to represent a `UIWebView*` and as long as you know what you're doing, the compiler won't complain. – stiv Apr 19 '16 at 16:08
2

You can define a property

@property (nonatomic, readonly) UIWebView *webView;

and do this:

- (UIWebView*)webView {
   return (UIWebView*)self.view;
}

- (void)loadView 
{
    UIWebView *wv = [[[UIWebView alloc] initWithFrame:self.frame] autorelease];
    // other configuration here...  
    self.view = wv;
}

Then you can access your WebView subsequently by self.webView without any casting. Essentially, you're centralizing the cast to one specific location in your code. If you ever want to insert another view like a label, you'd just have to modify a single location in your code.

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
0

From the documentation it looks like it is possible to do what you want through the use of categories. For example;

// UIWebViewController.h
@interface UIWebViewController : UIViewController {

}

@end

@interface UIWebViewController ()

@property (nonatomic,retain) UIWebView *view;

@end

// UIViewController.m
@implementation UIWebViewController

@synthesize view;

- (void)viewDidLoad {
    UIWebView *wv = [[UIWebView alloc] initWithFrame:webViewFrame];
    self.view = wv;
    [self.view loadHTML:@"<h1>Hello World</h1>" baseURL:baseURL];
    [wv release];
}

@end

There's a bit about this in http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocProperties.html in the Declared Properties > Using Properties > Property Re-declaration.

Tried this out and I didn't get any compiler complaints.

marcus.ramsden
  • 2,633
  • 1
  • 22
  • 33
  • Ah! It turns out this doesn't quite work: Redeclaring the view property causes the view not to be displayed. I don't know why it doesn't show (yet) but, I'd rather have a compiler warning AND a view than no view at all! :) – Carlton Gibson Nov 25 '10 at 11:22
  • Hmm just built and then ran rather then just building it and it appears to kill things off. I wonder if this is breaking things further up the UIKit chain. Might retract this answer if it doesn't fully work. – marcus.ramsden Nov 25 '10 at 15:18
  • 2
    Why would you still expect it to work when you have overridden UIViewController's accessor methods for view? – hooleyhoop Nov 30 '10 at 13:58
0

Cast the value or add a new UIWebView instance variable to you viewController.

It is not the job of UIViewController's view method to tell your fellows what kind of subclass you have used.

If you could do what you wanted to do you would just have to cast back to UIView in all the places UIKit expects UIViewController's view method to return a UIView.

hooleyhoop
  • 9,128
  • 5
  • 37
  • 58
-1

As has been mentioned, while it is possible to do this, it may or may not be wise.

You could declare a category on UIView itself, suggesting that it may respond to the selectors you're trying to call. This could be done in the implementation file of your custom view controller. It would look something like this:

@interface UIView (WebViewFakery)

- (void)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL

@end

You'd have to add all the UIWebView methods you're trying to call on view. Maybe not the most elegant solution, but another one to consider.

James J
  • 6,428
  • 6
  • 35
  • 45
  • If you do this you will get a compiler warning that UIView loadHTMLString:baseURL: is unimplemented and the point of the original question was to eliminate a compiler warning and add elegance. The spirit with which you are answering the question, which is to say trying to come up with a creative solution to the OP's question and be helpful is not missed, but this leads to code that is not at all understandable. When reading someone else's code, you should be able to make the assumption that a category method will do something to all classes of the category's type. This just causes confusion. – par Dec 01 '10 at 02:34
  • Actually, this builds fine, except for the missing semicolon at the end of the method signature. It does suppress the compiler warning. – James J Dec 01 '10 at 02:42