IRSlidingSplitViewController -
The Sliding Split View Controller is a tiny class implementing sliding pane navigation sported by the Facebook, Path, and Sparrow apps on iOS. (Many other apps have them, but these are the ones on my iPhone right now.) The controller is extremely simple — it holds a masterViewController and a detailViewController. By default, the Master view controller is hidden, but you can show it, and animate the process optionally, like the many methods:
- (void) setShowingMasterViewController:(BOOL)showingMasterViewController animated:(BOOL)animate completion:(void(^)(BOOL didFinish))callback;
- (void) setMasterViewController:(UIViewController *)toMasterVC animated:(BOOL)animate completion:(void(^)(BOOL didFinish))callback;
- (void) setDetailViewController:(UIViewController *)toDetailVC animated:(BOOL)animate completion:(void(^)(BOOL didFinish))callback;
The reason behind copious callbacks is that manually tracking every single possible animations is tedious or impossible without hacks — already so with the Map View — and stacking multiple animations usually won’t work well in a serious setup. So, I’ve got a rule of thumb that all the methods which could animate should accept a callback block.
A particular project of mine uses this controller, and I have been getting some interesting feedback regarding its interaction with different iOS controls (the Table View, the Map View, miscellany controls like Buttons and Sliders, and other gesture recognizers in custom views).
Deep down, it uses View Controller Containment introduced in iOS 5, and the whole thing was a blast to build both in terms of development speed and feeling. New code feels good.
Going forward, these issues are on my mind regarding the project:
I’m confident that these problems are either already solved, or trivial (though tedious) to implement. We’ll see where it goes, and please drop me a line if you plan on using it in your next project.
Let’s say you have a method definition looking like this, that you’d like to test:
[self doSomethingWithCompletion:^(BOOL finished){
[weakSelf doSomethingElse];
}];
A naïve way to test it would look like:
@interface NaiveTestCase : SenTestCase
@end
@implementation NaiveTestCase
- (void) testSomething {
[self doSomethingWithCompletion:^(BOOL finished){
STAssertTrue(finished, @"Operation must finish");
[weakSelf doSomethingElse];
}];
}
@end
This might even work, as long as the block is executed immediately and not concurrently, like in the case of -enumerateObjectsUsingBlock. However, if the block is a callback — for example, from API response over the Internet — it’ll usually not return immediately. And before the assertion statemaents are hit, your method ends, so the block is never really tested.
The initiative is to prevent the method from exiting before the callback is hit — prevent the method from exiting, unless explicitly told so — while at the same time not blocking the main thread, so the callbacks made on the main thread would still work.
One way to do this is to create an NSOperation subclass that takes a block, whose parameter is a callback block. Once code in the block is done, it calls the callback block provided by the operation, which mutates the operation’s internal state and make it return appropriate values for -isExecuting and -isFinished:
AsyncOperation *op = [AsyncOperation operationWithBlock: ^ (AsyncOperationCallback callback) {
[something doSomethingWithCompletion: ^ (BOOL didFinish) {
callback();
}];
}];
That’s a simplified version of IRAsyncOperation. (I’m pretty sure there’s other, better stuff ;) Then, put the operation on an implicitly created operation queue, don’t wait until finished:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
[queue addOperations:[NSArray arrayWithObject:op] waitUntilFinished:NO];
Since every single operation queue implicitly creates threads (the maximum number is defined by maxConcurrentOperationCount, so setting that to 1 guarantees serial execution), it moves the code which generates asynchronous callbacks on another thread for free.
Then, prevent the method from exiting early with a nice Run Loop hack:
while (queue.operationCount)
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]];
Some notes on threading (Brent Simmons) on Main Thread Gravity, and why it’s nice to always make the callback happen on the main thread.
Concurrency Programming Guide (Apple), just because.
Asynchronous Unit testing (CocoaBuilder): “OCUnit doesn’t run unit tests within a runloop itself. You should be able to run one yourself from within your [tests.]”
非同期で動作する OCUnit (SenTestingKit) を書いてみた (小野 将司) & akisute/SenAsyncTestCase
Melbourne is not as far as SF.
PaintCode is awesome. That’s what my 2010 self would say to myself. Unfortunately, my 2012 self thinks that it’s not going to work on a multiple-member team where roles and capabilities are diverse, and here’s why.
With PaintCode, you’ll dictate what tools your designers use, and frankly that is boneheaded and totally backwards. A great designer produces standard-format end results using her tools of choice. PaintCode is never going to be as flush and posh as Photoshop and Illustrator, and that along is a big no-no.
Frankly, if Time • Talent ∝ Skill-level, I would rather get a dedicated designer, rather than a multi-hat, given constant bandwidth, throughput and talent. People with multiple talents are rare, and locating them is not a scalable business. (They are ephemeral, latch on them while you can.)
With a bunch of images, you scan a directory in Finder, and you know how every single of them looks like. With code, you must run them, which means it’ll take longer for you to find the piece of code you need, and longer for you to replace or iterate, because it forces a recompile rather than a re-copy.
With drawing code, you can not diff changes visually. With tools like Kaleidoscope (which I have bought a license) you can visually compare revisions in images. With UI code, it’s hard — you get a text diff, and will need to spin up a second thread in your brain drawing the results, out of thin air. It might also bug out, and has a chance of not quite matching what comes out from the real device. ;)
Who cares if your app is 20MB instead of 10MB. Let’s go on a thought experiment:
We end up back at square one. Drawing code may dramatically optimize the size of the app bundle, but size does not matter. Bundle size takes back seat to user experience. If you really need to scale, why not use a PDF.
I feel anxious and happy, simultaneously, for effort that has been put into PaintCode. On one hand it’s a new experiment tredding unknown territory, on the other hand its current incarnation allows good-enough results for simple things, but might never scale pass that pinnacle. Many libraries suffer from this problem, too: they make ordinary things easy to accomplish, but you end up hitting walls after walls when you want to do something different. Go use something else, they will say. What’s going to happen next? Adobe gets a bucket of gold on an Illustrator or Photoshop license, and you’ll bundle images with your code.
Enterprisy, one may quip. But any piece of enterprisy software that sells well, fulfills its mission. Life is short, use it well.
IRDroplet is a Dropbox iOS SDK wrapper and has recently been bumped to use the 1.1 SDK, which uses oAuth. Let’s say your user needs to link her Dropbox account with the app. Here’s what happens within an app using the old SDK:
With the new SDK, this happens:
This is what happens if you use IRDropletLinkAccountViewController:
That’s pretty self-explainatory.
p.s. oAuth works extremely well for Web apps because they run in a browser, and modern browsers support tabbed browsing. On a mobile device with only one screen, and often a screenful of content at one time, oAuth is horribly broken.
There seems no reason to manually set and observe things again. Provlded that you wrap it in a navigation controller, this works:
+ (NSSet *) keyPathsForValuesAffectingContentSizeForViewInPopover {
return [NSSet setWithObjects:
@"tableView.contentInset",
@"tableView.contentSize",
nil];
}
- (CGSize) contentSizeForViewInPopover {
return (CGSize){
320,
self.tableView.contentInset.top + self.tableView.contentSize.height + self.tableView.contentInset.bottom
};
}
Related:
I’d like to talk about the dreaded retain cycles that often happen unexpectedly, using this wrong sample:
__block __typeof__(self) nrSelf = self;
self.onSomething = ^ (BOOL someCondition) {
NSParameterAssert(!someCondition); // Wrong!
[nrSelf doSomethingElse];
};
The block causes self to leak. Reason? Check how NSParameterAssert is expanded in NSException.h and compare it with NSCParameterAssert. You’ll see that all of them ultimately gets expanded to NSAssertionHandler method calls:
NSAssert, NSAssert1, … call -handleFailureInMethod:object:file:lineNumber:description:.NSCAssert, NSCAssert1, … call -handleFailureInFunction:file:lineNumber:description:.When you use NSAssert, ultimately the expanded code contains a reference to self and _cmd, inside whatever method the line was used. This also prohibits use of NSAssert in a block, because it creates a retain cycle. Just use NSCAssert which looks slightly wonky but does not require being used within an Objective-C method, and does not allow the programmer to accidentally create retain cycles.
Many iOS apps are built upon table views that need to be updated on-demand. This article presents a rather simple solution to the problem:
Update the scroll view, continuously, but only when the user is not touching it, so the scrolling and bouncing is not obstructed (and broken) by the updates.
I originally tried a makeshift solution, a couple of years ago, including a UILongPressGestureRecognizer, an ad-hoc dispatch queue, and a couple of lines calling dispatch_suspend() and dispatch_resume(). It was bad.
The new way is simple — do the work thru another layer of indirection:
[self doSomethingWithNetworkDataWithBlock: ^ (id results) {
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^ {
// All the actual fun happens here.
});
}];
For more information, take a look at the Threading Programming Guide. What needs to be told, has been excellently told in the piece.
This is what happens when you scroll a scroll view. Note how the run loop exits then re-enters the default mode. I’ve also updated the UIScrollViewExperiments repository with a sample project, UIScrollView-RunLoopExperiments.
C3PO: Hassle-free Localization -
Just updated my C3PO fork with some added info in its README. The script was written by Jamie.
On the vouching part, check the PBXProject object in your pbxproj file, and you’ll see a knownRegions. That is an exhaustive list of locales your app is expected to serve, and if anything doesn’t pass muster your app is broken.
Oh, and even if you can’t use a framework target without (seriously) hackery, you can use a bundle.
IRDeployment newly documented -
I’ve also updated documentation for IRDeployment. Now the ever-changing part of the CI workflow — the provisioning profile and the developer identity — is hosted as a Gist, and life is good.
IRDeployment is basically a script that builds an iOS project using
xcodebuild, and punts it on TestFlight.
Inventing on Principle (Bret Victor; CUSEC 2012) -
Belated: Magic Ink: Information Software And The Graphical Interface
Eventually, people get better. A matter of time frames.
This snippet saved me a couple of hours wrestling with libXML. If performance is not that important you can use JavaScript:
NSString *tidyString = [self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:
@"(function tidy (string) { var element = document.createElement('DIV'); element.innerHTML = string; return element.innerHTML; })(unescape(\"%@\"));",
[aString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]
]];
This is mostly a snippet, taken from that kitchen sink project, to illustrate the point. The idea is:
Make your baggage small.
Test for method existance and framework version if you want it to be safe, because it’s human behavior to publicize an existing private API. Method might exist, but with an incomplete implementation that works differently.
Use class_addMethod() for this.
Provide your own overridding method so you can steal encoding info from the compiler.
Don’t know if that’s mandatory, but I do believe the __attribute__((constructor)) is called after +load. Here’s from the documentation for NSObject:
The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
On Mac OS X v10.5, the order of initialization is as follows:
- All initializers in any framework you link to.
- All
+loadmethods in your image.- All C++ static initializers and C/C++
__attribute__(constructor)functions in your image.- All initializers in frameworks that link to you.
In addition:
- A class’s
+loadmethod is called after all of its superclasses’+loadmethods.- A category
+loadmethod is called after the class’s own+loadmethod.- In a
+loadmethod, you can therefore safely message other unrelated classes from the same image, but any +load methods on those classes may not have run
Here it is:
static void __attribute__((constructor)) initialize() {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if ([[[UIDevice currentDevice] systemVersion] localizedCompare:@"5.0"] == NSOrderedAscending) {
Class class = [UIImage class];
if (!class_addMethod(class, @selector(initWithCoder:), class_getMethodImplementation(class, @selector(_irInitWithCoder:)), protocol_getMethodDescription(@protocol(NSCoding), @selector(initWithCoder:), YES, YES).types))
NSLog(@"Error swizzling -[UIImage initWithCoder:]. Expect mayhem.");
if (!class_addMethod(class, @selector(encodeWithCoder:), class_getMethodImplementation(class, @selector(_irEncodeWithCoder:)), protocol_getMethodDescription(@protocol(NSCoding), @selector(encodeWithCoder:), YES, YES).types))
NSLog(@"Error swizzling -[UIImage encodeWithCoder:]. Expect mayhem.");
}
[pool drain];
}
I’m curious.