Getting your framework version in run-time

Xcode 13 introduced a new behavior that breaks framework versions: When your clients upload their app, Apple changes the CFBundleShortVersionString of all frameworks to match that of the app.
(See this StackOverflow thread for example)

One way to avoid that is to hard-code your framework version. But if you you want to keep using the MARKETING_VERSION build setting, you can use the following hack.

Create a version variable for your framework, for example:

public class MyFramwork: NSObject {
    @objc static var version: String = ""
}

Then include this Objective-C file in your framework:

#import <MyFramwork/MyFramwork-swift.h>

#define PROCESSOR_STRING(x) PRE_PROCESSOR_STRING_LITERAL(x)
#define PRE_PROCESSOR_STRING_LITERAL(x) @#x

@interface MyFramwork(version)
@property (class) NSString *version; // to access internal property
@end;

@interface MyFramworkLoader: NSObject
@end

@implementation MyFramworkLoader

#ifdef MARKETING_VERSION
+ (void)load {
    dispatch_async(dispatch_get_main_queue(), ^{
        MyFramwork.version = PROCESSOR_STRING(MARKETING_VERSION);
    });
}
#endif

@end

This will assign the MARKETING_VERSION value that you built you framework with, instead of an Apple-manipulated CFBundleShortVersionString value.

Paging Facebook Graph Results

Facebook iOS SDK provides `FBSDKGraphRequest` to get friends, posts, etc. But the results are paged: you only get the first 25 friends (or 5 posts). To get the rest you need to send a new request, not provided by `FBSDKCoreKit`:

[graphRequest startWithCompletionHandler:^(FBSDKGraphRequestConnection *connection, id result, NSError *error) {
	// first handle error and result...
	// then get the next page of results:
	NSString *nextPage = json[@"paging"][@"next"];
	if (nextPage) {
		[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:nextPage] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
			// parse data, handle results, and get the next page recursively.
		}] resume];
	}
}];

To simplify this, I wrote a simple extension to `FBSDKGraphRequest` that handles paging: FBSDKGraphRequest+Paging. Feel free to use it.

Usage:

FBSDKGraphRequest *friendsRequest = [[FBSDKGraphRequest alloc] initWithGraphPath:@"/me/friends" parameters:nil];
[friendsRequest startPagingWithCompletionHandler:^(id result, NSError *error) {
    // check for error...
    // use parsed result:
    NSArray *friends = result[@"data"];
}];

layoutSubviews Considered Reentrant

I had the weirdest bug the other day: rotating the iPhone worked fine in iOS 7, but not in iOS 8. Actually it was weirder: Some UIView instances adjusted themselves perfectly after the rotation, but some had the wrong size (even though they all had the exact same UIView subclass).

After some digging I discovered that layoutSubview was called several times during rotation, and each time the view had a different frame. Moreover, these calls were not happening sequentially! Instead, layoutSubviews was called with frame1, and before returning it was being called with frame2, and before returning form that it was called with frame3 — all for the same UIView.

There wasn’t a lot of code in my layoutSubviews, nothing that takes long. But it was apparently long enough to cause a bug in some instances.

My solution was to schedule a layout update for a split second later:

- (void)layoutSubviews
{
    [super layoutSubviews];
    [self performSelector:@selector(adjustLayout) withObject:nil afterDelay:0.01]; // avoid re-entering with different frame
}

- (void)adjustLayout
{
    if (CGRectEqualToRect(self.bounds, self.oldBounds)) return;
    // layout code here
    self.oldBounds = self.bounds;
}

Workaround for bug in [NSMutableIndexSet shiftIndexesStartingAtIndex:by:]

The Bug

Shifting an NSMutableIndexSet by a negative number will drop an index in some cases.

Example Code:

NSMutableIndexSet *set = [NSMutableIndexSet indexSetWithIndex:0];
[set addIndex:2];
[set shiftIndexesStartingAtIndex:1 by:-1];
NSLog(@"%@", set);

The set should contain 0-1 but instead contains only 1.

The Reason

NSIndexSet is a series of NSRange-s. If the shift method removes empty space between ranges, than they should become a single unified range. For example, if a set contains the range 1-2 and the range 5-6, and we do

[set shiftIndexesStartingAtIndex:3 by:-2];

then we should get a set with a single range 1-4.

However, the implementation of shiftIndexesStartingAtIndex:by: fails to unify ranges, and also assumes that separate ranges have at least one empty space between them. And so we get a set containing the ranges 1-1 and 3-4.

The Workaround

Luckily, the methods addIndex: and addIndexesInRange: do correctly unify ranges. And so the workaround is to first call one of these methods, and only then shift:

[set addIndexesInRange:NSMakeRange(3, 2)];
[set shiftIndexesStartingAtIndex:3 by:-2];

Simple UIProgressHUD replacement

The private API UIProgressHUD shows an activity indicator (spinner) over a shaded round rect. Although you can use fancy and flexible replacements like MBProgressHUD, there is a simpler way if you don’t need all the extra functionality: Take a regular UIActivityIndicatorView and add a partly transparent UIView behind it.

The simplest is to subclass of UIActivityIndicatorView so you can use it like any activity indicator.

The interface file:

//  ActivityHUD.h
#import 

@interface ActivityHUD : UIActivityIndicatorView
- (ActivityHUD *)initInView:(UIView *)view;
@end

And the implementation file:

//  ActivityHUD.m
#import "ActivityHUD.h"
#import 

@implementation ActivityHUD

- (ActivityHUD *)initInView:(UIView *)view
{
    self = [super initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
    if (self) {
        // add background view
        CGFloat border = self.bounds.size.height / 2;
        CGRect hudFrame = CGRectInset(self.bounds, -border, -border);
        UIView *hud = [[UIView alloc] initWithFrame:hudFrame];
        hud.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.67];
        hud.layer.cornerRadius = border;
        [self addSubview:hud];
        [self sendSubviewToBack:hud];

        // center in parent view
        [view addSubview:self];
        self.center = [self convertPoint:view.center fromView:view];
    }
    return self;
}

@end

To use, init the ActivityHUD in a view, and later call startAnimating to show it, and stopAnimating to hide.

The Three Underscores Idiom

#define ___
// defines three underscores as nothing

This is something I learned on my first programming job (eons ago…) and found useful but underused.

Useful for what? To solve the persistent tension between the robustness of single-exit point and the complexity of using extra state variables and state checking code.

Let me show you what I mean.

Continue reading

Dynamic UIActionSheet

Some actions may sometimes have more than one possible target, and other times only one.

For example: Calling a contact. If the contact has just one phone number, you openURL with it and you’re done. But if she has several numbers, you want to open a UIActionSheet to let the user choose which number to call.

Or picking an image. If you’re running on a camera-less iPod, you just show a UIImagePickerController to pick from the photo library. But with a camera, and maybe an existing image to delete, you’ll want to show an action sheet to choose the actual action that your “Photo” button performs.

So you need to do two things:

  1. Decide whether to show an action sheet at all or just do the one possible action.
  2. If you need an action sheet, fill it dynamically with the possible targets for action.

The code to do that doesn’t have to be complicated. It can actually be as simple as this:

@interface MyViewController() :  {
	NSArray *possibleTargets; // action sheet buttons go here
}
@end

@implementation MyViewControllerMyViewController

- (IBAction)selectTargetForAction:(id)sender
{
    possibleTargets = SomeWayToFindPossibleTargets();

    if ([possibleTargets count] == 1) {
        // just call the one number, or show the one image picker
        [self doAction:[possibleTargets lastObject]];
    }
    else if ([possibleTargets count] > 1) {
        // show a dynamically filled action sheet
        UIActionSheet *targetsSheet =
            [[UIActionSheet alloc] initWithTitle:__(@"Select")
                delegate:self
                cancelButtonTitle:nil // since we want it last
                destructiveButtonTitle:nil
                otherButtonTitles:nil]; // since we can't hard-code them
        for (NSString *target in possibleTargets) {
            [targetsSheet addButtonWithTitle:__(target.title)];
        }
        targetsSheet.cancelButtonIndex = [targetsSheet addButtonWithTitle:__(@"Cancel")];
        [targetsSheet showInView:self.view];
    }
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex != actionSheet.cancelButtonIndex) {
        [self doAction:[possibleTargets objectAtIndex:buttonIndex]];
    }
}

@end

Notes:

  1. The @interface code above is part of the .m file, as described in my earlier post How to Keep Your Protocols Private.
  2. If you’re puzzled by the meaning of __(@"String") then see my earlier post on How To Make NSLocalizedString Fun To Use.
  3. The objects in possibleTargets can hold a selector (SEL) of the actual method to be executed, or they can hold just relevant data for the action (like a phone number to call).

Reordering a UITableView

I like the simple and straightforward interface of the Reminders.app: edit reminders inline, and add a reminder by tapping and typing in the next empty line. So I wrote a UITableViewController subclass that does just that, but adds Edit mode with reordering.

Coding reordering requires implementing methods from both of the two entangled protocols UITableViewDataSource and UITableViewDelegate.

In UITableViewDataSource we have the methods:
tableView:commitEditingStyle:forRowAtIndexPath: (for insert and delete)
tableView:canMoveRowAtIndexPath: (for reorder)
tableView:moveRowAtIndexPath:toIndexPath: (for reorder)

And in UITableViewDelegate:
tableView:editingStyleForRowAtIndexPath: (for insert and delete)
tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: (for reorder)

The UITableViewDelegate methods are not strictly necessary in the simplest case where all rows are deletable, no row is insertable, and all rows are reorderable. In fact, in this simplest case you don’t need canMoveRowAtIndexPath either, only this:

- (void)tableView:(UITableView *)tableView
    moveRowAtIndexPath:(NSIndexPath *)fromIndexPath
    toIndexPath:(NSIndexPath *)toIndexPath
{
    // Update the model with the change.
    // The view updates are already handled by UIKit.
}

But of course, if the last row (or the row past last) is an insert row, and is not reorderable, than you need to implement the other methods as well. You can get the code for that (along with the inline editable UITextField-s) at GitHub: http://github.com/yonat/EditableList.

How to Keep Your Protocols Private

Don’t you just hate when a small change in the innards of your view controller forces you to change its header file just to conform to a delegate protocol? For example, adding emailing functionality requires you to implement the MFMailComposeViewControllerDelegate protocol and @import <MessageUI/MessageUI.h>. Talk about breaking encapsulation…

Thankfully, you can do that in your .m implementation file instead. (Even though Apple sample code doesn’t.) All you need to do is use the empty category:

// MyViewController.m
#import "MyViewController.h"
#import 

@interface MyViewController ()
     // privately conform to protocol
@property (nonatomic, strong) UIView *somePrivateSubview;
@end

@implementation MyViewController
// synthesize and methods implementations
@end

The empty category MyViewController () allows you to define private ivars, properties, methods, and even protocols – all in the privacy of your .m file, transparent to your clients.

BadgeLabel – Simple UILabel-based Badge

I know, I know – not another badge class! But the thing is, the other badges laying around the net all seem overly complicated and too inflexible. So yes, I wrote another badge class. Luckily, it was really easy because I used the built-in capabilities of iOS, and it turned out very flexible and powerful, because, well, I used the built-in capabilities of iOS. No manual CoreGraphics drawing code, just automatic CALayer-s magic.

The basic badge is a UILabel whose underlying CALayer has a backgroundColor and cornerRadius:

#import  // don't forget!
// ...
UILabel *badge = [[UILabel alloc] init];
badge.layer.backgroundColor = [UIColor blueColor].CGColor;
badge.layer.cornerRadius = badge.bounds.size.height / 2;

Basically, that’s it. There are some adjustments needed to make sure the label leaves enough space around the text for the rounded corners, but that’s easily done with short overrides of textRectForBounds and drawTextInRect.

The nice thing is how easy it is to make app-icon style badges, with border, shadow and gloss:

  • For border simply set the layer’s borderColor and borderWidth.
  • For shadow set the layer’s shadowOpacity, shadowColor and shadowOffset.
  • For gloss add a CAGradientLayer sublayer.

It’s that simple. And even better: animatable!

You can get the code, along with a Mail.app style BadgeTableViewCell and a demo app, at GitHub: http://github.com/yonat/BadgeLabel