Here's the situation: UINavigationController with UINavigationBar -- and no nib. Created in code.
I want to layer some drawing on top of all or part of the standard UINavigationBar: the normal drawing should happen underneath what I want to draw. But I can't use a subclass, since there's no nib.
There are two approaches that I know of -- but I'm hoping there's a third way that doesn't suck.
1. Get into the view hierarchy and do things. Ugh. (Hard to get right, I've found, and it feels icky, since it's a private view hierarchy.)
2. Swizzle methods (drawRect and my own drawing method). That feels even worse. But it works perfectly and uses much less code.
Please don't say, "Hey, Brent, use a nib already, wouldja?" -- I have an excellent reason for not using a nib (otherwise I'd be happy to).
Third way? It exists?
-Brent
PS We're up to 61 members now.
What kind of drawing are you hoping to do? If it's just additional
views, there shouldn't be a problem adding a subview to a navigation
bar. If it's rendering in the wrong order, you can always adjust the
zPosition of the subview's layer.
Drawing an image, usually.
I've found that this approach is easier said than done -- given the presence of other buttons and views, and given that we're hacking into a private view hierarchy we don't own, it didn't always work as expected. Often, for instance, a back button would get drawn under my image view after navigating.
Even if it worked, it's wrong -- as is method-swizzling. Which is why I was hoping someone knew of a third, non-icky way of doing this.
-Brent
Swizzling is how I've recently done it; is it really that bad an option? It seems rather tidy to me, in the absence of any hooks provided by Apple. I used the tintColor to determine whether to apply my modification to the navigation bar, so that I had the option of using system-standard bars too, instead of the effect being applied to all bars.
I just wanted a navigation bar without a background, so I just check for [[self tintColor] isEqual:[UIColor clearColor]].
Cheers =)
Michael
> I believe I've gotten it work with a category, sort-of -- except I can
> only draw the entire navigation bar. Often I want to draw on top of
> the standard navigation bar drawing, not over the entire thing.
Oh, can't you just call the original (swizzled) drawRect method to do the base rendering? Or have I misunderstood?
> 2. Swizzle methods (drawRect and my own drawing method). That feels even worse. But it works perfectly and uses much less code.
>
> Please don't say, "Hey, Brent, use a nib already, wouldja?" -- I have an excellent reason for not using a nib (otherwise I'd be happy to).
>
> Third way? It exists?
You can achieve nib-like class substitution by archiving and unarchiving. Roughly:
1) Subclass UINavigationBar and override whatever you need.
2) Create your UINavigationController.
3) Serialize it using NSKeyedArchiver.
4) Create an NSKeyedUnarchiver and use -setClass:forClassName: to point it at your UINavigationBar subclass.
5) Use that unarchiver to deserialize the UINavigationController.
6) There is no step six!
Mike
Oh *hell* yes. Thanks, Mike. Just what I wanted.
-Brent
Dave
> Oh *hell* yes. Thanks, Mike. Just what I wanted.
Glad to be of service. I had to pay you back for setting up this list. :)
Mike
On Sep 5, 2010, at 10:25am, st3fan wrote:
> I've been using:
>
> @implementation UINavigationBar (CustomImage)
> - (void)drawRect:(CGRect)rect
> {
> UIImage *image = [UIImage imageNamed: @"Background.png"];
> [image drawInRect:CGRectMake(0, 0, self.frame.size.width,
> self.frame.size.height+5)];
> }
> @end
>
> To get a custom background in a UINavigationBar. If you add a call to
> super there, then the original drawing will also happen.
If I'm interpreting your technique correctly, you're relying on undocumented and unreliable behavior of category methods in Objective-C.
Since you are not subclassing UINavigationBar, you can't safely override its drawRect: method without swizzling its implementation out. Your category implementation is being reached probably only because it happens to get loaded after the standard UINavigationBar method.
I'm not sure why [super drawRect:] works for you, if it does. It doesn't seem likely to me that UINavigationBar's own drawRect: would ever get reached. When you call [super drawRect:] from your category method on UINavigationBar, you are actually calling through to UIView's drawRect.
Perhaps what's happening here is UINavigationBar draws very little of its own content, and when you draw through to [UIView drawRect:], it probably causes all the subviews to get drawRect: messages, which subtantially makes it look like the superclass drawRect is being called.
I would not rely on this kind of approach personally, because any change in Apple's implementation of Objective-C class loading could cause the technique to stop working, and that would be especially embarrassing for apps that are already in the App Store.
Daniel
I agree that [super drawRect:] probably does *not* work as intended in
that example, and could very easily break in a future SDK update;
however, using categories to override existing methods is a well-known
and well-defined feature of Objective-C. The real uncertainty would be
if another category on UINavigationBar existed (hypothetically) that
defined the same method, in which case load order is indeterminate.
> I agree that [super drawRect:] probably does *not* work as intended in
> that example, and could very easily break in a future SDK update;
> however, using categories to override existing methods is a well-known
> and well-defined feature of Objective-C. The real uncertainty would be
> if another category on UINavigationBar existed (hypothetically) that
> defined the same method, in which case load order is indeterminate.
Thanks, Justin for the clarification. I had to look it up in the documentation to appreciate that, in fact, you can reliably use a category method to completely override a non-category method.
For others who want to review the documentation on this point, just search on "strongly discouraged" :)
The fact that so much thinking is required in order to do it without getting burned, is one reason I would always prefer a subclassing technique.
Daniel
Granted, it's a terrible thing to do. But even terrible things are
useful sometimes!
--
Justin Spahr-Summers
Another reason is that there is no guarantee that the original method you're trying to override isn't itself implemented in a category. It's legitimate to use categories to simply split up the implementation of a class for design purposes, and Apple may well do this (or start doing it later).
Mike
Fair to say, though the same issue exists with method swizzling at the
time of +load or +initialize, which I've seen a fair few examples of.
--
Justin Spahr-Summers
> On Sep 5, 2010, at 1:46pm, Justin Spahr-Summers wrote:
>
>> I agree that [super drawRect:] probably does *not* work as intended in
>> that example, and could very easily break in a future SDK update;
>> however, using categories to override existing methods is a well-known
>> and well-defined feature of Objective-C. The real uncertainty would be
>> if another category on UINavigationBar existed (hypothetically) that
>> defined the same method, in which case load order is indeterminate.
>
> The fact that so much thinking is required in order to do it without getting burned, is one reason I would always prefer a subclassing technique.
Hehe, naughty st3fan— I'd have thought listening to me rant on this sort of subject at Kobo for the last few months would have tempered this behaviour by now ;o)
I'd ALWAYS go for the subclassing route if possible, or if difficult I would use swizzling, which the new ObjC runtime API makes very lovely and easily, through things like this (thanks to my Apress co-author Mike Ash for the snippet I'm basing this on) —
void MethodSwizzle( Class c, SEL origSEL, SEL newSEL )
{
Method origMethod = class_getInstanceMethod( c, origSEL );
Method newMethod = class_getInstanceMethod( c, newSEL );
// first try adding the new method
// if it worked, then it's an override of the superclass
if( class_addMethod(c, origSEL,
method_getImplementation(newMethod),
method_getTypeEncoding(newMethod)) )
{
// bring the superclass method down into this class
// under the new selector, so it can get called that way
// note that we copy the original method using the new name,
// purely so we can call it from our override
class_replaceMethod( c, newSEL,
method_getImplementation(origMethod),
method_getTypeEncoding(origMethod) );
}
else
{
// it failed, so the method is implemented in this class
// exchange the two and we're done
method_exchangeImplementations( origMethod, newMethod );
}
}
@implementation UINavigationBar (my_swizzled_drawing_stuff)
- (void)my_overridden_drawRect:(CGRect)rect
{
[self my_overridden_drawRect:rect]; // now calls the original function, since selectors were swapped
UIImage *image = [UIImage imageNamed: @"Background.png"];
[image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height+5)];
}
@end
int main(void)
{
// …
MethodSwizzle( [UINavigationController class], @selector(drawRect:), @selector(my_overridden_drawRect:) );
// …
}
Also, since Mike's own reply just came in, mentioning the use of categories which aren't listed in official Apple header files, I can verify that there are a good number of categories we don't know about, especially in some of the UIView-type classes. I can't name any right now (not at my work computer to check) but within the last couple of days I noticed something in my class-dumped headers that made me go 'ooh, it's in a category is it?'
Cheers,
-Jim
--
Justin Spahr-Summers
Right, not swizzling +load, but swizzling IN +load. If you do that
within a category, for instance, the order in which that category
loads is indeterminate, so the same double-implementation problem
exists. I'd also wager that it could still conflict with methods
defined in categories, since the runtime almost certainly uses the
equivalent of class_replaceMethod() under the hood.
--
Justin Spahr-Summers
Yeah I don't know where that remark about super was coming from. Ignore it.
I've been using the above category since 2.2.1 or so and it works fine in practice.
I agree that there is a potential problem with another category doing the same thing, but I am pretty sure that same uncertainty is there with method swizzling. You simply can't know for sure.
I just prefer this because it is more concise.
S.
> On 2010-09-05, at 1:59 PM, Daniel Jalkut wrote:
>
>> On Sep 5, 2010, at 1:46pm, Justin Spahr-Summers wrote:
>>
>>> I agree that [super drawRect:] probably does *not* work as intended in
>>> that example, and could very easily break in a future SDK update;
>>> however, using categories to override existing methods is a well-known
>>> and well-defined feature of Objective-C. The real uncertainty would be
>>> if another category on UINavigationBar existed (hypothetically) that
>>> defined the same method, in which case load order is indeterminate.
>>
>> The fact that so much thinking is required in order to do it without getting burned, is one reason I would always prefer a subclassing technique.
>
> Hehe, naughty st3fan— I'd have thought listening to me rant on this sort of subject at Kobo for the last few months would have tempered this behaviour by now ;o)
No, I must have been working at home that day.
S.
> I want to layer some drawing on top of all or part of the standard UINavigationBar: the normal drawing should happen underneath what I want to draw. But I can't use a subclass, since there's no nib.
I like fancy pants stuff as much as the next guy but I think there’s a simpler route here.
Consider simply creating a CALayer with the contents you want and adding it as a sublayer of the navigation bar’s view. If you’re concerned with keeping it on top of anything else that is added give it a suitably high zPosition. You’re not even really messing with the view hierarchy in anyway that isn’t supported by the APIs. Method swizzling -drawRect: on UINavigationBar is less helpful I think and Mike’s archive / unarchive trick sounds like it may end up problematic (though probably works like a charm now).
UINavigationBar is backed by a CALayer. This CALayer has it’s delegate set to the UINavigationBar itself. I just tried making some other object the layer delegate and calling back into the UINavigationBar but this doesn’t work because UIKit is assuming it’s talking to a view for that layer. Specifically it dies trying to call _invalidateSubviewCache which seems a little unfair. So that potential solution is out the window.
I think just adding a layer is the simplest, easiest to debug and least surprising way to get what you want done. The method swizzle way takes out the drawRect across the whole app, which I’m pretty sure isn’t something Brent wants. Ken’s suggestion of running the view hierarchy and messing with the isa pointer of UINavigationBars is good but if you add an ivar to your subclass you’ll find yourself with some really annoying bugs to track down. I’d really stick with simple until it’s proven not to work.
- Guy
> I think this is actually more likely to cause problems than traversing the private view hierarchy and swizzling the isa of any UINavigationBar you encounter. That is, you should not feel clean about doing this. :-)
That's OK, I rarely feel clean when coding these days.
> The really good thing about a nib as compared to just archiving something is that it forms a clear segmentation between what is and what is not supposed to be replicated. With bindings on the desktop, for example, there are some bindings where the thing you're binding to should be part of the archive, and some where it's a global thing where each unarchived copy should hook back up to the same global thing. While archives do have scope for handling that kind of thing, just archiving a single root object doesn't cut it, and I don't think there's any way you can know what the root objects need to be here, is there? The Cocoa frameworks have a few places where they attempt to duplicate view hierarchies using archiving, and it causes nothing but problems. There are fewer than there used to be.
>
> Basically, if you archive and unarchive the tree, there's a larger surface area for possible problems than a more targeted change would have.
I completely understand what you're getting at in terms of the problems with archiving/unarchiving. This is one of the things I really hated about NSCollectionView in 10.5, and caused no end to problems there.
However, does it really apply in this particular situation? What I propose is to alloc/init the view and then immediately archive/unarchive it without doing anything else to it. It seems to me that, since it hasn't been configured, hasn't been added to any other views, and in general no pointers have been established between this object and the outside world, it should withstand the archive/unarchive process without any trouble.
If you were trying to do this on the fly to modify an existing object that was already in use, I'd think that would really not be a good idea at all. But this particular scenario doesn't seem *too* bad....
Mike
CALayer object. Additionally, you should never change the delegate of this layer.If you're only wanting to change the background, and that background is usually put in place by UINavigationBar's -drawRect: method, then I'd just add a new subview at index 0, and I'd draw the background with that— normally I'd use a UIImageView with a tiled or stretched background image. The various buttons and suchlike are all views themselves, so you only need to add the subview under the others. I've actually done that before with decent effects on a UIToolbar for the iPad (although for other reasons we ultimately switched to using a completely custom toolbar class).
-Jim
Heh. Mom told me I should learn to reed.
> I'm not sure inserting a layer is any less of a gamble than inserting a view.
Agreed — probably close to being the same thing modulo interacting with responder chain / subview arrays.
> There is a lot of opaque behavior to UINavigationBar / Item. Note that we add UIBarButtonItems, which are NSObjects, to the bar, which implies what actually happens after that is an implementation detail subject to change. Once you start messing with z order you could easily clobber your nav items which you cannot (legally) access or manipulate.
True. I meant give the new layer a zPosition of 1,000 or something to try to keep it atop everything else but, yeah, that could be problematic too.
Keeping the objective in mind perhaps simply adding a new sibling view of the NavigationBar and making sure it stays on top would avoid messing with UINavigationBar internals while keeping the same visual effect. (But you’d need to manage the new view to keep it in sync with where the NavBar is)
- Guy
> If you're only wanting to change the background, and that background is usually put in place by UINavigationBar's -drawRect: method, then I'd just add a new subview at index 0, and I'd draw the background with that— normally I'd use a UIImageView with a tiled or stretched background image. The various buttons and suchlike are all views themselves, so you only need to add the subview under the others. I've actually done that before with decent effects on a UIToolbar for the iPad (although for other reasons we ultimately switched to using a completely custom toolbar class).
Perhaps I went wrong somewhere when I gave this a try - actually, from memory I did this with a layer, not a view - but I had problems with various interface elements moving underneath the background layer when pushing/popping view controllers. For a while I tried using a category to override +layer and providing a custom layer class that disallows adding anything at index 0, thereby keeping the background in the right position, but this felt insane pretty quickly. I ended up swizzling too. Didn't try Guy's zPosition thing though - sounds interesting.
Mike
I want to layer some drawing on top of all or part of the standard UINavigationBar: the normal drawing should happen underneath what I want to draw. But I can't use a subclass, since there's no nib.
I want to layer some drawing on top of all or part of the standard UINavigationBar: the normal drawing should happen underneath what I want to draw. But I can't use a subclass, since there's no nib.
The navigationBar property on UINavigationController is read only. You can't set it. Subclassing UINavigationController to override -navigationBar to return a custom subclass is unreliable because you don't know if UINavigationController accesses the ivar prior to anyone calling the getter you overrode. I'm pretty sure I went down this road in the 2.x days and died a swift death.
On Sep 5, 2010, at 8:13 PM, Michael Ash wrote:
> On Sep 5, 2010, at 9:51 PM, Matt Drance wrote:
>
>> The navigationBar property on UINavigationController is read only. You can't set it. Subclassing UINavigationController to override -navigationBar to return a custom subclass is unreliable because you don't know if UINavigationController accesses the ivar prior to anyone calling the getter you overrode. I'm pretty sure I went down this road in the 2.x days and died a swift death
>
> I didn't realize the thing was publicly accessible. In that case, another alternative to swizzling/archive hackery would be to subclass, being careful not to add any ivars in the subclass, and then use object_setClass() at some conveniently early point. Hacky, but at least you're not modifying *every* instance in your app....
>
> Mike
Ken mentioned this technique offhand. It works but, really and as Mike says, if you add ivars you’ll be in serious pain. I did this kind of thing a few times when wanting to change an NSCell to a subclass after loading a Nib without having to jump through all kinds of hoops. I ended up writing a method on, I believe, NSObject - something like transmuteToClass:. It’d check that the instance size of the target object and the new class were identical before doing anything and it’d flip out and yell at you if they weren’t. I’d paste the code here but I don’t think I’ve got it anymore, it’d be in the Rogue Amoeba repository somewhere.
The reason you want to check the size of the two classes first is, I promise you, someone at some point is going to come along and want to add an ivar to your subclass. Since it’s a subclass that controls a visual effect it’ll be all the more tempting to add some UIImage or UIColor or whatever. Once that happens you won’t crash. Things will probably work a little bit. If the instance size of your UINavigationBar subclass isn’t aligned nicely and there’s space at the end of the allocations for an extra pointer or two you may never even notice — but eventually you’ll end up writing over some other object when you write to your ivar and you’ll turn to drink and go bald before you figure out what’s happening. So be careful.
That said, hey, welcome to Cocoa-Unbound — you can totally mess with the isa pointer of an instance to do all kinds of fun and crazy stuff.
- Guy
> I’d paste the code here but I don’t think I’ve got it anymore, it’d be in the Rogue Amoeba repository somewhere.
It is. It also assumes the old runtime with direct struct access, so not too useful these days. However, it really isn't complicated. It just checks to make sure it's really a subclass, checks that the instance sizes are equal, and then assigns to the isa. Would be trivial to rewrite for the modern runtime API using class_getInstanceSize and object_setClass.
> That said, hey, welcome to Cocoa-Unbound — you can totally mess with the isa pointer of an instance to do all kinds of fun and crazy stuff.
Indeed. I should probably point out now, in case anyone isn't familiar with all the crazy stuff I do on my blog, that if I describe a technique, you should assume it's insane and dangerous unless I say otherwise.
Mike
> On Sep 6, 2010, at 12:03 AM, Guy English wrote:
>
>> I’d paste the code here but I don’t think I’ve got it anymore, it’d be in the Rogue Amoeba repository somewhere.
>
> It is. It also assumes the old runtime with direct struct access, so not too useful these days. However, it really isn't complicated. It just checks to make sure it's really a subclass, checks that the instance sizes are equal, and then assigns to the isa. Would be trivial to rewrite for the modern runtime API using class_getInstanceSize and object_setClass.
A working example of this trick can be found in xmppframework. It's not my code, but I believe it implements basically the technique you're discussing using the modern framework:
http://code.google.com/p/xmppframework/source/browse/trunk/XMPPMessage.m
--
Jon Olson / @jonolson
Ballistic Pigeon, LLC
ballisticpigeon.com