Link

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:

  • Minor UX issues like the Master view controller showing through when the Detail pane is dragged leftwards needs to be resolved thru more application-centric work.
  • It needs hysterisis and bounceback to feel more realistic.
  • It needs some better API regarding custom presenting / dismissal animations.
  • It probably needs a way to extend support for a second Master view controller shown on the right.
  • The sample app probably needs to be done, in terms that supporting a reference implementation for a common Master–Detail navigation structure might be utterly needed for new people to utilize this class efficiently.

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.

Text

Managing content size of UIViewController + UITableView in popover

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:

Link

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.

Text

Polyfill in Objective-C

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:

    1. All initializers in any framework you link to.
    2. All +load methods in your image.
    3. All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
    4. All initializers in frameworks that link to you.

      In addition:
    • A class’s +load method is called after all of its superclasses’ +load methods.
    • A category +load method is called after the class’s own +load method.
    • In a +load method, 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];

}
Text

Drawing single-line-height text without abandoning CTFramesetterRef and CTFrameRef

Core Text is lightweight and all awesome. But we have this pesky little problem with multilingual layouts — different fonts are created by different vendors and typographers, and they often mismatch in terms of metrics. Some are taller, some shorter; some have huge ascender and descenders, some relatively tiny and tame. Things are quite manageable with just one language, but it’s not the case for me.

We need layouts with even baselines, which runs orthogonal to how the CTFramesetter works. The frame setter calculates typographic bounds, adds up ascender, descender and leading, then use the whole sum as the height of the line. This is straightforward as in it works like movable type, but also painful as it can wreak havoc with baseline alignment.

No, you don’t have to roll your own CTFramesetter implementation. That’s too much pain. Re-implementing something when you can hack and use all the free stuff? No way, that’ll take a whole week.

Creating a well-hacked attributed string

To ensure that your Core Text label draws even line heights, make sure the attributed string you feed into your label is well-hacked:

- (NSAttributedString *) attributedStringForString:(NSString *)aString font:(UIFont *)aFont color:(UIColor *)aColor {

    if (!aString)
        return nil;

    float_t lineHeight = aFont.leading;

    id fontAttr = [NSMakeCollectable(CTFontCreateWithName((CFStringRef)aFont.fontName, aFont.pointSize, NULL)) autorelease];
    id foregroundColorAttr = (id)(aColor ? aColor.CGColor : [UIColor blackColor].CGColor);
    id paragraphStyleAttr = ((^ {

        CTParagraphStyleSetting paragraphStyles[] = (CTParagraphStyleSetting[]){
            (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(float_t), (float_t[]){ 0.01f } },
            (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(float_t), (float_t[]){ lineHeight } },
            (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(float_t), (float_t[]){ lineHeight } },
            (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierLineSpacing, sizeof(float_t), (float_t[]){ 0.0f } },
            (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(float_t), (float_t[]){ 0.0f } },
            (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(float_t), (float_t[]){ 0.0f } }

        };

        CTParagraphStyleRef paragraphStyleRef = CTParagraphStyleCreate(paragraphStyles, sizeof(paragraphStyles) / sizeof(CTParagraphStyleSetting));
        return [NSMakeCollectable(paragraphStyleRef) autorelease];

    })());

    NSAttributedString *returnedString = [[[NSAttributedString alloc] initWithString:aString attributes:[NSDictionary dictionaryWithObjectsAndKeys:
        fontAttr, kCTFontAttributeName,
        foregroundColorAttr, kCTForegroundColorAttributeName,
        paragraphStyleAttr, kCTParagraphStyleAttributeName,
        [NSNumber numberWithInt:kCTUnderlineStyleSingle], kCTUnderlineStyleAttributeName,
    nil]] autorelease];

    return returnedString;

}

Think of the many 0.0f as CSS Reset.

Drawing the attributed string with even baselines

If you’re creating an UILabel subclass, this would probably work. Without drawing adjustments, fonts with slightly different ascender and descender values will twiddle around. Which makes an unholy, unsightly mess.

Note that ctFrame is a private property and irCTFrameEnumerateLines is a simple convenience wrapping around functions that operate on a CTFrameRef:

- (void) drawTextInRect:(CGRect)rect {

    if (![self isShowingRichText]) {
        [super drawTextInRect:rect];
        return;
    }

    CTFrameRef usedFrame = self.ctFrame;
    if (!usedFrame)
        return;

    CFRetain(usedFrame);
    CGContextRef context = UIGraphicsGetCurrentContext();   
    CGContextConcatCTM(context, CGAffineTransformMake(
        1, 0, 0, -1, 0, CGRectGetHeight(self.bounds)
    ));

    __block CGFloat usableHeight = CGRectGetHeight(self.bounds);
    CGContextSetTextMatrix(context, CGAffineTransformIdentity); 
    irCTFrameEnumerateLines(usedFrame, ^(CTLineRef aLine, CGPoint lineOrigin, BOOL *stop) {

        usableHeight -= self.font.leading;

        CGContextSetTextPosition(context, lineOrigin.x, usableHeight - self.font.descender);
        CTLineDraw(aLine, context);

    });

    CFRelease(usedFrame);

}

Appendix

gh://schwa/CoreTextToy

Text

IRTextAttributor

IRTextAttributor is a simple class that solves the problem where I’d like to attribute a certain string asynchronously. For example, given the string:

Google.com is here.

I’d like it to eventually look like:

Google.com (“Search the world’s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you’re looking for.”) is here.

The idea is to create an attributor which works synchronously, that has two primary worker blocks.

The first worker block is called the Discovery block, which goes thru all the characters, and reports ranges of substrings to a callback block, describing prospective items of interest.

The second worker block is called the Attribution block, which is also handed a substring and a callback block. The block will do its own work, probably thru an Embed.ly invocation, or probably thru reading from a file, and when it’s done, call its callback with the final attribute value, or nil if it can’t find one.

The attributor holds a mutable attributed string as a property, and every time the property changes, it reevaluates the entire string, skipping work on known substring-attribute pairs. Its delegate object, conforming to <IRTextAttributorDelegate>, will be notified whenever a new attribute is found and inserted to the “master” attributed string.

To use the object, you can create an instance, then take advantage of IRTextAttributorDiscoveryBlockMakeWithRegularExpression(), which takes an NSRegularExpression object. Then, assign an attribution block, which takes a string and a callback:

textAttributor.attributionBlock = ^ (NSString *attributedString, IRTextAttributorAttributionCallback callback) {

    if (!attributedString) {
        callback(nil);
        return;
    }

    NSURL *url = [NSURL URLWithString:attributedString];
    if (!url) {
        callback(nil);
        return;
    }

    [[WARemoteInterface sharedInterface] retrievePreviewForURL:url onSuccess:^(NSDictionary *aPreviewRep) {

        callback(aPreviewRep);

    } onFailure:^(NSError *error) {

        callback(nil);

    }];

};

Then, assuming you have a text view, use some localized event coalescing in -textViewDidChange:

- (void) textViewDidChange:(UITextView *)textView {

    NSString *capturedText = textView.text;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.25f * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void){

        if (![textView.text isEqualToString:capturedText])
            return;

        self.textAttributor.attributedContent = [[[NSMutableAttributedString alloc] initWithString:capturedText] autorelease];

    });

}

The coalescing also prevents issue with the attributor working overzealously on every single keystroke. I tried using NSRunLoop methods which are probably higher level, but did not get them to work at the first time. :p

One thing I learned when writing this class, is that attributed strings don’t only contain attributes to style the text. They can contain almost any Objective-C object, and the fact can be very well-exploited.

Text

Harm-free Class Swizzling with NSKeyedUnarchiver

I came across a problem where we need to subclass UINavigationBar in an UINavigationController. Conventionally, one may subclass the navigation controller, and commence hacks. Fortunately, NSKeyedArchiver saves the day:

self.window.rootViewController = (( ^ {

    //  Since it is totally unsafe to modify the navigation controller, the best way to swizzle-in a custom navigation bar subclass is to use NSKeyedUnarchiver, providing a custom class for any object whose class is named UINavigationBar.

    UINavigationController *navController = [[[UINavigationController alloc] initWithRootViewController:presentedViewController] autorelease];
    NSData *navControllerData = [NSKeyedArchiver archivedDataWithRootObject:navController];

    NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:navControllerData] autorelease];
    [unarchiver setClass:[WANavigationBar class] forClassName:@"UINavigationBar"];

    //  The root object is keyed “root”. :D
    UINavigationController *swizzledNavController = [unarchiver decodeObjectForKey:@"root"];
    [swizzledNavController setViewControllers:navController.viewControllers];

    return swizzledNavController;

})());

WANavigationBar inherits from UINavigationBar, and overrides -drawRect: with an empty implementation.

Obviously, if you were already setting your navigation controller using Interface Builder (i.e. you can actually see it in Interface Builder), then the best way requiring no swizzling code at all is to find the navigation bar instance, then set its class to your own subclass in the Identity inspector.

Text

Providing default user defaults

NSUserDefaults is fun and practical. Assuming you’ve got an IRDefaults.plist in your app bundle, this registers everything in the dictionary as default user defaults:

void IRRegisterUserDefaults () {

    NSURL *defaultsURL = [[NSBundle mainBundle] URLForResource:@"IRDefaults" withExtension:@"plist"];
    NSData *defaultsData = [NSData dataWithContentsOfMappedFile:[defaultsURL path]];
    NSDictionary *defaultsObject = [NSPropertyListSerialization propertyListFromData:defaultsData mutabilityOption:NSPropertyListImmutable format:nil errorDescription:nil];

    [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsObject];

}
Link

This has been alive for a couple of weeks. Please help by looking at it. The discrete layout manager is an experimental shot at creating easy, extensive, expressive magazine-style layouts for iPad apps.

Text

IRPaginatedView

IRPaginatedView is a special view that makes it extremely easy to create paginated, scrolling views like ones powering the Photos app, or the one you see when viewing PDFs in iBooks. Or when paging thru the Springboard. This is a tiny class which was done a long, long time ago. I’m pretty confident that it’s not too shabby, given that the apps using it are all working fine.

It’s a part of IRFoundations right now.

Demo

Basically — your IRPaginatedViewDelegate needs to know the number of pages in the view, and is also responsible for providing the views. The paginated view responds to bound changes on its own.

A few notes:

  • iOS views are not masked by default, so anything placed in a superview will bleed through its bounds. The scroll view clips by default. Disabling clipping — setting clipsToBounds to NO — lets you see all the views in the scroll view even if it’s currently out of bounds. Since the paginated view assume all the pages it contain are resizable to the size of its frame, this is handy.

  • Who doesn’t love shouldRasterize.

  • No animated update as of now. If you need anything special, consider using CATransition.

I assume that you’ve got a IPSample folder in the app’s Resources full of images, and the URLs are stored as a property. Here’s all the code you’ll need to make a dedicated view controller. Unrelated code have been snipped.

- (void) loadView {

    self.allImageFileURLs = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:@"IPSample"] includingPropertiesForKeys:nil options:0 error:nil];

    self.view = [[[UIView alloc] initWithFrame:(CGRect){ 0, 0, 1024, 1024 }] autorelease];
    self.view.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.0f];

    self.paginatedView = [[[IRPaginatedView alloc] initWithFrame:self.view.bounds] autorelease];
    self.paginatedView.backgroundColor = nil;
    self.paginatedView.opaque = NO;
    self.paginatedView.frame = UIEdgeInsetsInsetRect(self.paginatedView.frame, (UIEdgeInsets){ 64, 64, 64, 64 });
    self.paginatedView.scrollView.clipsToBounds = NO;
    self.paginatedView.clipsToBounds = NO;
    self.paginatedView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    self.paginatedView.delegate = self;
    self.paginatedView.horizontalSpacing = 10.0f;

    [self.view addSubview:self.paginatedView];
    [self.paginatedView reloadViews];

}

- (NSUInteger) numberOfViewsInPaginatedView:(IRPaginatedView *)paginatedView {

    return [self.allImageFileURLs count];

}

- (UIViewController *) viewControllerForSubviewAtIndex:(NSUInteger)index inPaginatedView:(IRPaginatedView *)paginatedView {

    return nil;

}

- (UIView *) viewForPaginatedView:(IRPaginatedView *)aPaginatedView atIndex:(NSUInteger)index {

    UIImageView *returnedView = [[[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:[[self.allImageFileURLs objectAtIndex:index] path]]] autorelease];
    returnedView.frame = aPaginatedView.bounds;
    returnedView.contentMode = UIViewContentModeScaleAspectFit;
    returnedView.backgroundColor = nil;
    returnedView.opaque = NO;

    returnedView.layer.shadowRadius = 4.0f;
    returnedView.layer.shadowOffset = (CGSize){ 0, 2 };
    returnedView.layer.shadowOpacity = 0.35f;
    returnedView.layer.shouldRasterize = YES;

    return returnedView;

}
Text

Creating a stepping (discrete) UISlider

There are cases where you want the continuous UISlider to be discrete, only stopping on a few possible values.

The way involving no private API methods is to abuse either the fact that a) Objective-C blocks have scopes, or in another implementable way the power of object composing, which I now like a lot more better; and b) setting the value of the slider can be animated.

(By block scoping I meant that the slider can nil its own delegate and reinstate it after some operation finished. That, if composition can’t be easily done and integrated.)

Let’s say you have a custom UIView. Put an UISlider in it. Then say things along the line of this. Notice that a lot of stuff is omitted, and there are unrelated things, but it should illustrate the idea.

- (NSUInteger) estimatedPageNumberForPosition:(CGFloat)aPosition {

    if (!self.numberOfPages)
        return 0;

    CGFloat roughEstimation = ((self.numberOfPages - 1) * aPosition);

    if (roughEstimation == 0)
        return (NSUInteger)floorf(roughEstimation);
    else if (roughEstimation == (self.numberOfPages - 1))
        return (NSUInteger)ceilf(roughEstimation);
    else
        return (NSUInteger)roundf(roughEstimation);

}


- (void) sliderTouchDidEnd:(UISlider *)aSlider {

    [self willChangeValueForKey:@"currentPage"];
    currentPage = [self estimatedPageNumberForPosition:aSlider.value];
    [self didChangeValueForKey:@"currentPage"];

    [UIView animateWithDuration:0.125f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionAllowUserInteraction animations: ^ {
        self.pageIndicatorLabel.alpha = 0.0f;
    } completion:nil];

    NSUInteger capturedCurrentPage = self.currentPage;
    dispatch_async(dispatch_get_current_queue(), ^ {

        CGFloat inferredSliderSnappingValue = [self positionForPageNumber:capturedCurrentPage];
        [self.delegate paginationSlider:self didMoveToPage:capturedCurrentPage];

        if (self.snapsToPages)
            [aSlider setValue:inferredSliderSnappingValue animated:YES];

    });

}

Reasons that the final part in -sliderTouchDidEnd: is bounced: a) it’s not good to work on this during touch end, generally; and b) the user might as well rapidly start touching the slider again, animation should only take place when appropriate. (I am not totally sure about either but it worked out pretty well.)

If you must snap something during animation, it is not a good idea to snap the slider itself. Actually it is absolutely a very bad idea because you’ll mutilate the actual underlying slider value. In that case, you can make the thumb itself transparent by setting the slider’s thumb image to an 1 by 1, transparent UIImage and overlay your own thumb, then manage its position on your own.

(Philosophically, I like composition. Nothing better than not having to write any finger handling code in a totally custom UI control.)

Text

Continuous smooth scrolling using UIScrollView

I was so dumb not to realize this earlier.

[UIView animateWithDuration:0.3f delay:0 options:(

    UIViewAnimationOptionAllowUserInteraction | 
    UIViewAnimationOptionBeginFromCurrentState | 
    UIViewAnimationOptionCurveEaseInOut

) animations: ^ {

    [self.scrollView scrollRectToVisible:visibilityEnsuredRect animated:NO];

} completion:nil];