When implementing iOS 7 support for a client’s app I got a result that might stump even seasoned Cocoa programmers.
On iOS 7 views generally go behind translucent bars. To still get your views aligned correctly – when creating them in code – you have to get the responsible view controller’s top and bottom layout guides.
Those get set sometime before viewWillLayoutSubviews and I found it useful to add a viewInset property to the view controller’s base view. Setting this would setNeedsLayout and then you can rearrange the subviews according to the new insets in layoutSubviews.
Though this is not the riddle I want to talk about. Consider the following code – imagine if you will – by an engineer who is trying to code it such that you can still build it with the iOS 6 SDK.
- (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; CGFloat topInset = 0; if ([self respondsToSelector:NSSelectorFromString(@"topLayoutGuide")]) { id topLayoutGuide = [self valueForKeyPath:@"topLayoutGuide"]; topInset = (CGFloat)[topLayoutGuide length]; } // do something with topInset } |
You can create a Single View iOS app and paste the above code into the ViewController.m file to see the effect yourself.
Ok, now the question: running on iOS 7, what will the value be in topInset? And why?
97 Pixels?!
The answer is 97. And this is – of course not the amount we were looking for. Since we don’t have any top tool or nav bars the correct answer is 20 occupied by the now-transparent status bar.
Cédric Luthi sheds some light on this:
Since topLayoutGuide is typed as id, the return type of the length message is guessed to be NSUInteger and the message is sent with the wrong ABI calling convention.
Try using your topLayoutGuide object actual class instead of casting the result to CGFloat.
id<UILayoutSupport> topLayoutGuide = [self valueForKeyPath:@"topLayoutGuide"]; topInset = [topLayoutGuide length]; |
Calling it like this we get the correct value. Having a protocol as part of the variable definition is key since this allows the compiler to know that the return value type to be expected is CGFloat. Without this the compiler guesses which of the multiple length methods you might be referring to, and in this case it guesses wrong.
There is a compiler warning that you can enable, which is disabled by default because historically Apple’s code was full of such weak typing hazards.
Building with this setting enabled we get an interesting warning:
Indeed! NSString’s length is being used instead of the length property defined in the UILayoutSupport protocol.
If we enforce the user of the correct length method via the protocol we solve the problem. The above is only compilable with iOS 7 SDK (where UILayoutSupport is defined for the first time), but this protocol is very simple:
/* UILayoutSupport protocol is implemented by layout guide objects returned by UIViewController properties topLayoutGuide and bottomLayoutGuide. These guide objects may be used as layout items in the NSLayoutConstraint factory methods. */ @protocol UILayoutSupport <NSObject> @property(nonatomic,readonly) CGFloat length; // As a courtesy when not using auto layout, this value is safe to refer to in -viewDidLayoutSubviews, or in -layoutSubviews after calling super @end |
BTW it is not entirely accurate that the value is only safe to refer to in -viewDidLayoutSubviews. If you had to wait until that time you had to always do two layout passes, one without the guides set and one with the insets. But it turns out that you can already retrieve the correct value in -viewWillLayoutSubviews. This way you can set up the insets before the first layoutSubviews will occur.
If we don’t want to compile against iOS 7 SDK, we can create our own protocol with the same property just so that the compiler will do the right thing with the stack parameters.
// above the @implementation @protocol MyLayoutSupport <NSObject> @property(nonatomic,readonly) CGFloat length; @end |
And there I was assuming that you can send any kind of message to an id-typed objects. Well, yes, you can and if the return values are also id then you have no problem like this. But if you expect a scalar (int, float) return value then you are asking for trouble, messaging ids.
Matt Gallagher wrote an article two years ago explaining this limitation of Objective-C’s weak typing in greater detail.
UPDATE: Damien DeVille, Software engineer from London UK, wrote a response to this article explaining this phenomenon a bit more in depth with some ARM assembly.
Conclusion
It is generally good practice to make sure that you coax ids into strong object types as much as feasibly. The new instancetype type helps with telling the compiler that this is a specific Class, not just any id. This avoids potential “unrecognized selector” crashes at runtime.
It might be a good idea to enable a couple of additional Xcode warnings, especially if you are looking to clean up an existing project or starting a new one. Strict Selector Matching might be a candidate for you.
When it comes to scalar return values there you definitely must go the strongly typed route. Either you can cast the id to a concrete object type, or if you cannot then you can at least define a protocol to tell the compiler what return type to assume.
Categories: Q&A