Peter Steinberger

Retrofitting containsString: On iOS 7

Daniel Eggert asked me on Twitter what’s the best way to retrofit the new containsString: method on NSString for iOS 7. Apple quietly added this method to Foundation in iOS 8 - it’s a small but great addition and reduces common code ala [path rangeOfString:@"User"].location != NSNotFound to the more convenient and readable [path containsString:@"User"].

Of course you could always add that via a category, and in this case everything would probably work as expected, but we really want a minimal invasive solution that only patches the runtime on iOS 7 (or below) and doesn’t do anything on iOS 8 or any future version where this is implemented.

This code is designed in a way where it won’t even be compiled if you raise the minimum deployment target to iOS 8. Using __attribute__((constructor)) is generally considered bad, but here it’s a minimal invasive addition for a legacy OS and we also want this to be called very early, so it’s the right choice.

A Story About Swizzling “the Right Way™” and Touch Forwarding

Some people think of me as the guy that does crazy things to ObjC and swizzles everything. Not true. In PSPDFKit I’m actually quite conservative, but I do enjoy spending time with the runtime working on things such as Aspects - a library for aspect oriented programming.

After my initial excitement, things have stalled a bit. I shipped Aspects in our PDF framework, and people started complaining that it sometimes freezes the app, basically looping deep within the runtime, when the New Relic SDK was also linked.

Of course I tried to fix this. Contacting New Relic didn’t bring any results at first, even after two paying customers started to report the same issue. After periodically bugging them for over a month I finally got a non-canned response, pointing me to a blog entry about method swizzling.

This basically says that using method_exchangeImplementations is really bad, and that pretty much everybody does swizzling wrong. And they indeed have a point. Regular swizzling messes not only with your brain but also with assumptions that the runtime makes. Suddenly _cmd no longer is what it is supposed to be, and while in most cases it does not matter, there are a few cases where it does very much.

How most people swizzle (including me)

This is the swizzling helper that I’ve used during the last few years:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL PSPDFReplaceMethodWithBlock(Class c, SEL origSEL, SEL newSEL, id block) {
    PSPDFAssert(c && origSEL && newSEL && block);
    if ([c respondsToSelector:newSEL]) return YES; // Selector already implemented, skip

    Method origMethod = class_getInstanceMethod(c, origSEL);

    // Add the new method.
    IMP impl = imp_implementationWithBlock(block);
    if (!class_addMethod(c, newSEL, impl, method_getTypeEncoding(origMethod))) {
        PSPDFLogError(@"Failed to add method: %@ on %@", NSStringFromSelector(newSEL), c);
        return NO;
    }else {
        Method newMethod = class_getInstanceMethod(c, newSEL);

        // If original doesn't implement the method we want to swizzle, create it.
        if (class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(origMethod))) {
            class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
        }else {
            method_exchangeImplementations(origMethod, newMethod);
        }
    }
    return YES;
}

This is a very common approach, with a small twist that it takes a block and uses imp_implementationWithBlock to create an IMP trampoline out of it. Usage is as follows:

1
2
3
4
5
SEL touchesMovedSEL = NSSelectorFromString(@"pspdf_wacomTouchesMoved:withEvent:");
PSPDFWacomSwizzleMethodWithBlock(viewClass, @selector(touchesMoved:withEvent:), touchesMovedSEL, ^(UIView *_self, NSSet *touches, UIEvent *event) {
    [WacomManager.getManager.currentlyTrackedTouches moveTouches:touches knownTouches:[event touchesForView:_self] view:_self];
    ((void ( *)(id, SEL, NSSet *, UIEvent *))objc_msgSend)(_self, touchesMovedSEL, touches, event); // call the original method
});

(Yes, Wacom’s framework for stylus support is horrible. There are way better ways to hook into touch handling, such as subclassing UIApplication’s sendEvent:.)

Note the cast to objc_msgSend. While this (by luck) worked without casting in the earlier days, this will probably crash your arm64 build if you don’t cast this correctly, because the variable argument casting specifications changed. Add #define OBJC_OLD_DISPATCH_PROTOTYPES 0 to your files to make sure this is detected at compile time, or even better, use Xcode 6 and enable error checking on this:

The Crash

This works as expected in most cases, but has the issue that the original implementation will be called with a different _cmd than it expects. This can be a problem when _cmd is actually used, such as in the touch forwarding logic. I learned this the hard way after swizzling touchesMoved:withEvent: to inject additional logic. The app crashed with the popular doesNotRecognizeSelector: exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
* thread #1: tid = 0x695bfa, 0x0000000104cee973 libobjc.A.dylib`objc_exception_throw, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000104cee973 libobjc.A.dylib`objc_exception_throw
    frame #1: 0x000000010507e65d CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 205
    frame #2: 0x0000000104fded8d CoreFoundation`___forwarding___ + 973
    frame #3: 0x0000000104fde938 CoreFoundation`__forwarding_prep_0___ + 120
    frame #4: 0x0000000103080ae1 UIKit`forwardTouchMethod + 247
  * frame #5: 0x00000001000f071e PSPDFCatalog`__56-[PSPDFWacomStylusDriver prepareViewForTouchMonitoring:]_block_invoke_2(.block_descriptor=0x0000000115e1d010, _self=0x000000010e94a2a0, touches=0x000000010fefbd40, event=0x000000010e61fd50) + 382 at PSPDFWacomStylusDriver.m:122
    frame #6: 0x0000000102f85bbc UIKit`-[UIWindow _sendTouchesForEvent:] + 372
    frame #7: 0x0000000102f866e4 UIKit`-[UIWindow sendEvent:] + 925
    frame #8: 0x0000000102f5e29a UIKit`-[UIApplication sendEvent:] + 211
    frame #9: 0x0000000102f4baed UIKit`_UIApplicationHandleEventQueue + 9579
    frame #10: 0x0000000104f7cd21 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    ...
    frame #15: 0x0000000102f4de33 UIKit`UIApplicationMain + 1010

Somehow UIKit wants to call pspdf_wacomTouchesMoved:withEvent: on a class that I definitely did not swizzle, and so of course the runtime throws an exception. But how did we end up here? Investigating the stack trace, UIKit’s forwardTouchMethod looks interesting. Let’s see what this actually does.

Touch forwarding in UIKit

The base class for UIView is UIResponder, and it implements all basic touch handling: (Note: I don’t have access to the UIKit sources, so this might not be 100% accurate. The snippets are based on disassembling UIKit and manually converting this back to C.)

1
2
3
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    forwardTouchMethod(self, _cmd, touches, event);
}

Here it gets interesting. _cmd is used directly in this C function that (at least the name suggests) then forwards our touches up the responder chain. But let’s keep digging, just to make sure. For curiosity’s sake, I translated the whole function, including legacy behavior. (I don’t remember any announcement where Apple changed this in iOS 5. Is this somewhere documented? Hit me up on Twitter if you know more.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void forwardTouchMethod(id self, SEL _cmd, NSSet *touches, UIEvent *event) {
  // The responder chain is used to figure out where to send the next touch
    UIResponder *nextResponder = [self nextResponder];
    if (nextResponder && nextResponder != self) {

      // Not all touches are forwarded - so we filter here.
        NSMutableSet *filteredTouches = [NSMutableSet set];
        [touches enumerateObjectsUsingBlock:^(UITouch *touch, BOOL *stop) {

          // Checks every touch for forwarding requirements.
            if ([touch _wantsForwardingFromResponder:self toNextResponder:nextResponder withEvent:event]) {
                [filteredTouches addObject:touch];
            }else {
              // This is interesting legacy behavior. Before iOS 5, all touches are forwarded (and this is logged)
                if (!_UIApplicationLinkedOnOrAfter(12)) {
                    [filteredTouches addObject:touch];

                    // Log old behavior
                    static BOOL didLog = 0;
                    if (!didLog) {
                        NSLog(@"Pre-iOS 5.0 touch delivery method forwarding relied upon. Forwarding -%@ to %@.", NSStringFromSelector(_cmd), nextResponder);
                    }
                }
            }
        }];

        // here we basically call [nextResponder touchesBegan:filteredTouches event:event];
        [nextResponder performSelector:_cmd withObject:filteredTouches withObject:event];
    }
}

At this point I was a few hours in, digging through Apple’s touch forwarding code. You can use Hopper to read through _wantsForwardingFromResponder:toNextResponder:withEvent:. Most of the code seems to track forwarding phases, checks for exclusiveTouch, different windows and there’s even a dedicated _UITouchForwardingRecipient class involved. There’s quite a lot more logic in UITouch than I would have expected.

Forwarding using _cmd is not restricted to touch handling at all - on the Mac it’s used for mouse[Entered|Exited|Moved]: as well.

A different approach on swizzling

Our naive use of method_exchangeImplementations() broke the _cmd assumption and resulted in a crash. How can we fix this? New Relic suggested using the direct method override. Let’s try that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__block IMP originalIMP = PSPDFReplaceMethodWithBlock(viewClass, @selector(touchesMoved:withEvent:), ^(UIView *_self, NSSet *touches, UIEvent *event) {
    [WacomManager.getManager.currentlyTrackedTouches moveTouches:touches knownTouches:[event touchesForView:_self] view:_self];
     ((void ( *)(id, SEL, NSSet *, UIEvent *))originalIMP)(_self, @selector(touchesMoved:withEvent:), touches, event);
});

static IMP PSPDFReplaceMethodWithBlock(Class c, SEL origSEL, id block) {
    NSCParameterAssert(block);

    // get original method
    Method origMethod = class_getInstanceMethod(c, origSEL);
    NSCParameterAssert(origMethod);

    // convert block to IMP trampoline and replace method implementation
    IMP newIMP = imp_implementationWithBlock(block);

    // Try adding the method if not yet in the current class
    if (!class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) {
        return method_setImplementation(origMethod, newIMP);
    }else {
        return method_getImplementation(origMethod);
    }
}

This solves our problem. We preserve the correct selector (there’s no pspdf_wacomTouchesMoved:withEvent: method anymore) and thus UIKit’s touch forwarding works as expected. The method replacing logic is also simpler.

However, there are downsides to this approach as well. We are now modifying the touchesBegan:withEvent: method of our custom UIView subclass. There is no default implementation yet, so we get the IMP from UIResponder and then manually call this. Imagine if at some later point, somebody else would swizzle touchesBegan:withEvent: on UIView directly using the same technique. Assuming UIView has no custom touch handling code, they would get the IMP from UIResponder and add a new method to UIView. But then our method gets called, which already captured the IMP of UIResponder and completely ignores the fact that we modified UIView as well.

Epilogue

There are solutions to this problem, but they are extremely complex, such as CydiaSubstrate’s MSHookMessageEx, but since this requires a kernel patch (and thus a jailbreak), it’s not something you would use in an App Store app.

If you read trough the whole article and are wondering why I’m not simply subclassing the touch handlers, you are right. This is the usual approach. However we recently added stylus support for a variety of vendors, and this is built via external driver classes, so that we don’t have to “pollute” the framwork with the different approaches. Wacom is the only vendor that requires direct touch forwarding, and every vendor has it’s own way to manage touches and views. Integrating all these into a single class would result in a very hard-to-maintain class, and licensing issues would also prevent us from shipping the framework binaries directly. Furthermore, only some companies use the stylus code, so we designed this to be modular. (e.g. Dropbox just uses PSPDFKit as a Viewer, and thus doesn’t need that part.)

Further Reading:

Hacking With Aspects

I’ve recently spent a few days extracting and polishing the AOP code from PSPDFKit, and the result of this is called Aspects - a delightful, simple library for aspect oriented programming.

Now Aspects is a great new tool in your toolkit. It allows to call code before, instead or after the original implementation, and there’s no need to manually call super, cast objc_msgSend or any of that other stuff you have to should do on swizzling. Use it with reason, it has a few great use cases, some are well-explaind on the GitHub page.

It’s also great for hacking and debugging. While testing the example on an iPad that still runs iOS 6, I found this exception: // *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'On iPad, UIImagePickerController must be presented via UIPopoverController'

Right, Apple fixed that in iOS 7. But I was more curious how this is actually implemented. It’s actually quite tricky to detect if you are inside a popover or not, and sometimes this quite important to know. Has Apple some “secret sauce” they’re using here? I opened Hopper to find out.

That’s roughly their code, converted back from assembly. Interesting that there’s a _UIImagePickerControllerAllowAnySuperview to disable this check. You have to wonder where they are using that… The check is otherwise quote straightforward. The interesting part is here: [_UIPopoverView popoverViewContainingView:self.view].

Let’s look up that as well…

Ha. There’s no secret sauce here. Apple is simply iterating the view hierarchy to find the _UIPopoverView. Fair enough, it’s a simple solution. Sadly there’s no _UIPopoverView for us mere mortals, it’s a private class.

Now, let’s test if this disassembly is actually correct! First, we’ll disable Apple’s check:

That’s all - this makes the controller work perfectly where it threw an exception before. The popover restriction was a pure could be a political one, or there are edge cases we don’t know.

Putting it all together

Now, we want to implant our own check using Aspects. PLLibraryView is again private, so we’ll use a runtime class lookup to hook it globally. I also commented out the property check since this would disable our own checking code.

That’s it!

This code isn’t of much use, but it’s interesting how Apple checks these things internally, and that their popover detection really is just subview querying. And while _UIPopoverView is private, we could easily make this check working without flagging private API by testing for parts of the class name…

Fixing UITextView on iOS 7

UITextView on iOS 7 is a lot more powerful, since Apple switched over from using WebKit to TextKit for rendering. It’s also very much a 1.0, and has some rather terrible bugs. In fact, they go so far that people started writing replacements for the whole scrolling logic.

Of course, people reported these issues in PSPDFKit as well, so I had to find a workaround. I’m using contentInset when the keyboard (iPhone) or another view (iPhone/iPad) goes up, which is pretty much completely ignored by UITextView in iOS 7. This is frustrating mainly because it works perfectly in iOS 6.

At first, my solution was based on a category, but after discovering more and more needed hooks, I moved over to a subclass that automatically forwards all delegate methods. This has the advantage of more shared code, and we might be able to remove all those horrible hacks once iOS 8 comes out. I certainly hope so, and will write a few more radars.

So, what’s fixed in PSPDFTextView?

  • When adding a new line, UITextView will now properly scroll down. Previously, you needed to add at least one character for this to happen.
  • Scrolling to the caret position now considers contentInset. UITextView completely ignored this.
  • Typing will also consider contentInset and will update the scroll position accordingly.
  • Pasted text will scroll to the caret position.

UITextView

PSPDFTextView

To enable these fixes, simply use PSPDFTextView instead of UITextView:

https://github.com/steipete/PSPDFTextView

This is working quite well for my use case, but there surely are edge cases where this won’t be enough (like when using rich text). I also tried using the new textContainerInset but this didn’t work as intended and didn’t solve my scrolling problems.

I have to give credit to countless people who searched for the same solution – this very much was a community-oriented fix. Sadly, this doesn’t seem to be a priority for Apple, since it’s still broken in iOS 7.1b3.

Please fork the repo and send a pull request if you have any ideas on how to simplify the code or find an even better workaround.

Fixing What Apple Doesn’t

It’s one of those days where Apple’s sloppiness on iOS 7 is driving me nuts. Don’t get me wrong; I have a lot of respect in pulling off something as big as iOS 7 in such a short amount of time. It’s just that I see what’s coming in iOS 7.1 and so many annoyances of iOS 7 still aren’t fixed.

No, I’m not talking about the offset arrow, the background – I already made peace with that. But the offset label just looks like crap. (rdar://15748568) And since it’s still there in iOS 7.1b2, let’s fix that.

First off, we need to figure out how the class is called. We already know that it’s inside the printer controller. A small peak with Reveal is quite helpful:

So UIPrinterSearchingView is the culprit. Some more inspection shows that it’s fullscreen and the internal centering code is probably just broken or was somehow hardcoded. Let’s swizzle layoutSubviews and fix that. When looking up the class via the iOS-Runtime-Headers, it seems quite simple, so our surgical procedure should work out fine:

Let’s run this again:

Done! Now obviously this is a bit risky – things could look weird if Apple greatly changes this class in iOS 8, so we should test the betas and keep an eye on this. But the code’s written defensively enough that it should not crash. I’m using some internal helpers from PSPDFKit that should be obvious to rewrite – comment on the gist if you need more info.

Best thing: The code just won’t change anything if Apple ever decides to properly fix this.

How to Inspect the View Hierarchy of Third-Party Apps

I’m generally not a big fan of jailbreaks. Mostly this is because they’re used for piracy and all the hacks result in weird crashes that generally are impossible to reproduce. Still, I was quite excited about the recent iOS 7 jailbreak, since it enables us to attach the debugger to third-party apps and do a little bit of runtime analysis.

Why? Because it’s fun, and it can inspire you to solve things differently. Studying the view hierarchy of complex apps can be rather revealing and it’s interesting to see how others are solving similar problems. This was one thing that took many iterations to get right in PSPDFKit, our iOS PDF framework.

So, how does this work? It’s actually super simple.

  1. Jailbreak your device of choice. I’ve used an iPad 4 here. Make sure it runs iOS 7.0.x. Both arm7(s) and arm64 devices will work now. Don’t jailbreak a device that’s used in production. Otherwise you lose a lot of security features as well. I only jailbreak a clean device and install some apps to inspect.

  2. Open Cydia and install OpenSSH, nano, and Cydia Substrate (previously called MobileSubstrate).

  3. Copy the Reveal library. Find out the device IP address via Settings and execute the following in your terminal (this assumes you have installed Reveal already):
    scp -r /Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/Reveal.framework root@192.168.0.X:/System/Library/Frameworks
    scp /Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/libReveal.dylib root@192.168.0.X:/Library/MobileSubstrate/DynamicLibraries.
    For Spark Inspector, you would use scp "/Applications/Spark Inspector.app/Contents/Resources/Frameworks/SparkInspector.dylib" root@192.168.0.X:/Library/MobileSubstrate/DynamicLibraries
    Note: The default SSH password on iOS is ‘alpine.’

  4. SSH into the device and create following text file with nano /Library/MobileSubstrate/DynamicLibraries/libReveal.plist: { Filter = { Bundles = ( "<App ID>" ); }; }
    Previously, this worked with wildcard IDs, but this approach has problems with the updated Cydia Substrate. So simply add the App ID of the app you want to inspect, and then restart the app.

  5. Respring with killall SpringBoard or simply restart the device.

Done! Start your app of choice and select it in Reveal. (This should also work similary for SparkInspector.) Attaching via LLDB is a bit harder and I won’t go into details here since this could also be used to pirate apps. Google for ‘task_for_pid-allow,’ ‘debugserver,’ and ‘ldid’ if you want to try this.

Fixing UISearchDisplayController on iOS 7

iOS 7 is great, but it’s still very much a 1.0. I’ve spent a lot of time working around iOS 7-specific bugs in PSPDFKit and will share some of my work here.

This is how UISearchDisplayController looks on iOS 7:

Pretty bad, eh? This is how it should look:

Here’s the code to fix it. It doesn’t use private API (although there is some view hierarchy fighting), and it’s fairly clean. It uses my UIKit legacy detector, so this code won’t do any harm on iOS 5/6:

I imagine one could subclass UISearchDisplayController directly and internally forward the delegate to better package this fix, but I only need it in one place so the UIViewController was a good fit.

Note that this uses some awful things like dispatch_async(dispatch_get_main_queue(), but it shouldn’t do any harm even if Apple fixes its controller sometime in the future.

Smart Proxy Delegation

When calling optional delegates, the regular pattern is to check using respondsToSelector:, then actually call the method. This is straightforward and easy to understand:

1
2
3
4
id<PSPDFResizableViewDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(resizableViewDidBeginEditing:)]) {
    [delegate resizableViewDidBeginEditing:self];
}

Now, this used to be three lines and now it’s four lines, because delegate is usually weak, and once you enable the relatively new Clang warning, -Warc-repeated-use-of-weak, you will get a warning when accessing self.delegate more than once. All in all, that’s a lot of boilerplate for a simple selector call.

In the past, I’ve used an approach similar to what Apple does with parsing the delegate when it’s being set and caching the respondsToSelector: calls. But that’s even more boilerplate, cumbersome to update once you change a delegate, and really not worth it unless you call your delegates 100x per second.

What we really want is something like this:

1
[self.delegateProxy resizableViewDidBeginEditing:self];

We can use NSProxy to simply forward the method if it’s implemented. This used to be expensive, but with Leopard Apple came Fast Message Forwarding. This “can be an order of magnitude faster than regular forwarding” and doesn’t require building an NSInvocation-object.

Our custom NSProxy is really quite simple and just a few lines of code. The important part is here:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL)respondsToSelector:(SEL)selector {
    return [self.delegate respondsToSelector:selector];
}
- (id)forwardingTargetForSelector:(SEL)selector {
    id delegate = self.delegate;
    return [delegate respondsToSelector:selector] ? delegate : self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.delegate methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    // ignore
}

One sad detail is that if the method is not implemented in the delegate, message forwarding will use the slow path with methodSignatureForSelector: and forwardInvocation:, because forwardingTargetForSelector: interprets both ‘nil’ and ‘self’ as ‘continue with message forwarding.’

Implementing The Delegate Proxy

We want to make sure that implementing this in our classes is simple. Here’s the pattern I use in PSPDFKit:

1
2
3
4
5
6
7
8
9
@interface PSPDFResizableView ()
@property (nonatomic, strong) id <PSPDFResizableViewDelegate> delegateProxy;
@end

PSPDF_DELEGATE_PROXY(id <PSPDFResizableViewDelegate>)

- (void)handleRecognizerStateBegan:(UIGestureRecognizer *)recognizer {
    [self.delegateProxy resizableViewDidBeginEditing:self];
}

You’ll notice a few things. First, we lie about the actual type of delegateProxy. It’s a class of type PSTDelegateProxy, but we declare it as the delegate type so that we don’t have to cast it every time we use it, and so that we also get compiler-checked warnings when there are typos in the selector. Notice that the proxy needs to be strongly retained.

Second, we’re using a macro to simplify things. This macro expands into following:

1
2
3
4
5
6
- (void)setDelegate:(id <PSTDelegate>)delegate {
  self.delegateProxy = delegate ? (id <PSTDelegate>)[[PSPDFDelegateProxy alloc] initWithDelegate:delegate] : nil;
}
- (id <PSTDelegate>)delegate {
  return ((PSPDFDelegateProxy *)self.delegateProxy).delegate;
}

We keep the weak reference of the delegate directly in the PSPDFDelegateProxy; no need to save it twice. This macro only works if you name your delegate delegate, but that should be the common case, and you could expand the macro to cover different cases. We do keep a strong reference of our proxy-object around, but this won’t hurt. Other solutions work with weak-saving NSProxy, but that’s not needed and also buggy on iOS 5.

Handling Defaults

Now we’ve already covered most of the cases. But there’s one pattern that we still need to take care of, which is optional methods that change a default return value if implemented:

1
2
3
4
5
_acceptedStartPoint = YES;
id<PSPDFSelectionViewDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(selectionView:shouldStartSelectionAtPoint:)]) {
    _acceptedStartPoint = [delegate selectionView:self shouldStartSelectionAtPoint:location];
}

If the proxy can’t find a delegate, it will return nil (id), NO (BOOL), 0 (int), or 0.f (float). Sometimes we want a different default. But again, we can perfectly solve this with some more NSProxy trickery:

1
_acceptedStartPoint = [[(id)self.delegateProxy YESDefault] selectionView:self shouldStartSelectionAtPoint:location];

And here’s the relevant code for the subclass that is being returned after calling YESDefault:

1
2
3
4
5
6
7
- (void)forwardInvocation:(NSInvocation *)invocation {
    // If method is a BOOL, return YES.
    if (strncmp(invocation.methodSignature.methodReturnType, @encode(BOOL), 1) == 0) {
        BOOL retValue = YES;
        [invocation setReturnValue:&retValue];
    }
}

We have to to go down to NSInvocation, which is a bit slower, but again, you won’t notice, except your delegate is called many times per second, which is quite unusual.

All code (including test cases) is on GitHub.

I’ve already implemented this almost everywhere in my iOS PDF Framework and was able to delete a lot of boilerplate code. I’m @steipete on Twitter. Looking forward to your feedback.

Adding Keyboard Shortcuts to UIAlertView

I’m not even home from the more-than-excellent NSConference in Leicester, but had to hack on something super awesome that Evan Doll of Flipboard presented earlier today. They added keyboard support to UIAlertView and UIActionSheet (amongst other things) – simply to make debugging in the Simulator faster by accepting Esc and Enter keys – something that Apple should have done anyway. There’s not much value in shipping this in release builds, except better support for bluetooth keyboards. And since this hack uses private API AND accesses a struct with a memory layout that could change, I don’t recommend shipping it. If you do, make sure that you whitelist iOS versions and block this method by default on unknown future versions of iOS. I’m using it in PSPDFKit only when compiled in DEBUG mode for the Simulator.

The actual hack is mostly based on this blog post about intercepting the keyboard on iOS, and it’s not pretty. I had to modify some constants to make it work on iOS 5/6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#define GSEVENT_TYPE 2
#define GSEVENT_FLAGS 12
#define GSEVENTKEY_KEYCODE 15
#define GSEVENT_TYPE_KEYUP 10
// http://nacho4d-nacho4d.blogspot.co.uk/2012/01/catching-keyboard-events-in-ios.html
__attribute__((constructor)) static void PSPDFKitAddKeyboardSupportForUIAlertView(void) {
    @autoreleasepool {
        // Hook into sendEvent: to get keyboard events.
        SEL sendEventSEL = NSSelectorFromString(@"pspdf_sendEvent:");
        IMP sendEventIMP = imp_implementationWithBlock(^(id _self, UIEvent *event) {
            objc_msgSend(_self, sendEventSEL, event); // call original implementation.

            SEL gsEventSEL = NSSelectorFromString([NSString stringWithFormat:@"%@%@Event", @"_", @"gs"]);
            if ([event respondsToSelector:gsEventSEL]) {
                // Key events come in form of UIInternalEvents.
                // They contain a GSEvent object which contains a GSEventRecord among other things.
                int *eventMem = (int *)[event performSelector:gsEventSEL];
                if (eventMem) {
                    if (eventMem[GSEVENT_TYPE] == GSEVENT_TYPE_KEYUP) {
                        UniChar *keycode = (UniChar *)&(eventMem[GSEVENTKEY_KEYCODE]);
                        int eventFlags = eventMem[GSEVENT_FLAGS];
                        //NSLog(@"Pressed %d", *keycode);
                        [[NSNotificationCenter defaultCenter] postNotificationName:@"PSPDFKeyboardEventNotification" object:nil userInfo: @{@"keycode" : @(*keycode), @"eventFlags" : @(eventFlags)}];
                    }
                }
            }
        });
        PSPDFReplaceMethod(UIApplication.class, @selector(sendEvent:), sendEventSEL, sendEventIMP);

        // Add keyboard handler for UIAlertView.
        SEL didMoveToWindowSEL = NSSelectorFromString(@"pspdf_didMoveToWindow");
        IMP didMoveToWindowIMP = imp_implementationWithBlock(^(UIAlertView *_self, UIEvent *event) {
            objc_msgSend(_self, didMoveToWindowSEL, event); // call original implementation.

            static char kPSPDFKeyboardEventToken;
            if (_self.window) {
                id observerToken = [[NSNotificationCenter defaultCenter] addObserverForName:@"PSPDFKeyboardEventNotification" object:nil queue:nil usingBlock:^(NSNotification *notification) {
                    NSUInteger keyCode = [notification.userInfo[@"keycode"] integerValue];
                    if (keyCode == 41) /* ESC */ {
                        [_self dismissWithClickedButtonIndex:_self.cancelButtonIndex animated:YES];
                    }else if (keyCode == 40) /* ENTER */ {
                        [_self dismissWithClickedButtonIndex:_self.numberOfButtons-1 animated:YES];
                    }
                }];
                objc_setAssociatedObject(_self, &kPSPDFKeyboardEventToken, observerToken, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            }else {
                id observerToken = objc_getAssociatedObject(_self, &kPSPDFKeyboardEventToken);
                if (observerToken) [[NSNotificationCenter defaultCenter] removeObserver:observerToken];
            }
        });
        PSPDFReplaceMethod(UIAlertView.class, @selector(didMoveToWindow), didMoveToWindowSEL, didMoveToWindowIMP);
    }
}

You will also need some swizzling helpers. Here’s what I use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// http://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html
static void PSPDFSwizzleMethod(Class c, SEL orig, SEL new) {
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if (class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    }else {
        method_exchangeImplementations(origMethod, newMethod);
    }
}

void PSPDFReplaceMethod(Class c, SEL orig, SEL newSel, IMP impl) {
    Method method = class_getInstanceMethod(c, orig);
    if (!class_addMethod(c, newSel, impl, method_getTypeEncoding(method))) {
        PSPDFLogError(@"Failed to add method: %@ on %@", NSStringFromSelector(newSel), c);
    }else PSPDFSwizzleMethod(c, orig, newSel);
}

How to Center Content Within UIScrollView

There seems to be some confusion around the net when it comes to the best way to center a view within an UIScrollView.

And it’s actually not that easy to get right. In PSPDFKit version 1, I used the Apple-recommended way (see Apple’s photo example) of subclassing layoutSubviews. This was “good enough” until I added smart zoom in version 2 of my PDF framework. (That is, text blocks are detected and zoomed onto with a double tap, just like you’re used to in the mobile version of Safari). I noticed that the zoomToRect method did not work properly; the final rect was offset, and it was quite obvious why, since the layoutSubview-method moved around the frame of the view that was zoomed in on. And don’t try to compensate this moving – it seems relatively impossible to get right.

The next generation (PSPDFKit 2.2 and upward) used override setContentOffset: to always center the content. This works, but has the drawback that you can’t pan the view around when you’re zooming out more – it’s always fixed centered. This solution also has a very nasty hidden bug, where the UIScrollView can be “locked up” and doesn’t accept any touches anymore until you perform a pinch in/out. I’ve had Apple DTS against this and even the person helping me only came up with useless workarounds (like not using gesture recognizers… right.) It’s hard to reproduce this bug here without open sourcing half of my framework, so please just trust me on that one.

I’ve asked around on Twitter (thanks, everyone!) and most recommendations were about overriding layoutSubviews. Some suggested stuffing the view into another view and doing the centering manually (which I tried) but this has the drawback that you can scroll into the empty space when zoomed in (or you adapt the view and get the same problem as you had in layoutSubviews).

The solution that works best by far is by setting contentInset. It allows you to pan around the view when zoomed out and doesn’t expose the “lock” bug that overriding setContentOffset: had. It also works fine with using zoomToRect. There are a few gotchas when using edgeInsets as well – for example, I have a mechanism that preserves the current view state (page, zoom, contentOffset) and restores that later on. If you set contentOffset to 0,0 this will decenter your image (in contrast to methods one and two). But you can trigger re-centering with following trick:

1
2
3
        // Trigger a new centering (center will change the content offset)
        scrollView.contentInset = UIEdgeInsetsZero;
        [scrollView ensureContentIsCentered];

I’ve made a small GitHub project that shows off all three centering techniques and zooms to a small rect on double tap (to show off the zoomToRect problem). Hopefully this helps someone save a few hours!