As we’re finally dropping iOS 7 in PSPDFKit, I’ve lead the project to migrate everything to Apple’s new wonderful UIPresentationController API. Now UIPresentationController is by far not perfect, but it’s a huge step forward coming from UIPopoverController and makes presenting in mixed environments much simpler and cleaner. The beauty: The view controller hierarchy is now correct, where in the older popover controller there were unconnected gaps, and it wasn’t easy to go from the contentViewController to the parent presenter. This required inconvenient redirects when a controller inside a popover wanted to dismiss itself. RIP, UIPopoverController. You won’t be missed.
Detecting if we are in a popover
Sometimes it’s quite useful to know inside the view controller if we are presented as a popover or modally. Of course things like a close button should be managed by the presenter, not the presented object, however there might be other changes (keyboard specifics, moving bar buttons around to have space for the close button, sizing differences). These cannot be determined by the trait classes alone. Initially I was naive and just checked for the existence of .popoverPresentationController - but since the system is adaptive, this object will also be there when we are in a compact trait collection. Turns out, it’s quite hard to correctly detect if we are presented as a popover. Here’s my first attempt:
@implementationUIPresentationController(PSPDFAdditions)// This is more hacky than it should be!-(UIModalPresentationStyle)pspdf_currentPresentationStyle{// We can't use the trait collection from the presentation object, // since that one will always be compact if it's an UIPopoverPresentationController.UIViewController*vc=self.presentedViewController.presentingViewController;if(vc.traitCollection.horizontalSizeClass==UIUserInterfaceSizeClassCompact){UIModalPresentationStylestyle=self.adaptivePresentationStyle;if(style!=UIModalPresentationNone){returnstyle;}}returnself.presentationStyle;}-(BOOL)pspdf_isPopover{returnself.pspdf_currentPresentationStyle==UIModalPresentationPopover;}@end@implementationUIViewController(PSPDFPresentationControllerAdditions)-(UIViewController*)pspdf_outmostParentViewController{UIViewController*parent=self;while(parent.parentViewController){parent=parent.parentViewController;}returnparent;}-(BOOL)pspdf_isPopover{returnself.pspdf_outmostParentViewController.presentationController.pspdf_isPopover;}@end
The evil detail is that UIViewController lazily creates a new _UIFullscreenPresentationController, so simply checking for .presentationController is not always correct, as it doesn’t include the case where we are embedded as a child view controller. Now you might think I’m doing it wrong, but there are some valid cases for this. (We have a container that basically embeds other view controllers completely and just adds a switch control as top bar).
-(BOOL)pspdf_isInPopoverPresentation{return[selfpspdf_isInContextOfPresentationControllerOfClass:UIPopoverPresentationController.class];}-(BOOL)pspdf_isInContextOfPresentationControllerOfClass:(Class)klass{BOOLisInContextOfPresentationController=NO;UIViewController*presentingViewController=self.presentingViewController;while(presentingViewController){// Apple can just access the ivar, we'll have to use KVC to get the same.// This is private API - do not ship this!UIPresentationController*presentationController=[selfvalueForKey:@"_presentationController"];isInContextOfPresentationController=YES;if([presentationControllerisKindOfClass:klass]){break;}if(presentationController.shouldPresentInFullscreen){isInContextOfPresentationController=NO;break;}presentingViewController=presentingViewController.presentingViewController;}returnisInContextOfPresentationController;}
The Problem: Apple uses _presentationController or _originalPresentationController for the detection, but we only have presentationController available - which unfortunately here lazily creates a presentation controller when first being accessed. The above is also incorrect - we have a case where we embed a controller full-screen inside another controller, and this code assumes YES, but doesn’t crawl up the parent/child view controller hierarchy, so we end up with a situation where being in a popover is assumed when it’s clearly not:
When we use the above code, everything works:
Notice how we change the background so the popover uses transparency, while modally we use a clear white. The close button is handled externally by the presentation logic. The solution we use is also not 100% correct - cases where we would present inside the context of a popover would likely be incorrectly flagged as modal presentation - but since we don’t use any of that I didn’t investigate further.
Other solutions
Going back, how did we do things before presentation controllers? In PSPDFKit we set a flag on the view controller, however this was prone to error. What Apple and many others are doing is simply looking at the view hierarchy if there is a _UIPopoverView in the tree . This is a used and proven method, and something Apple uses a lot in UIKit as well, and which has quite some usage and similar implementations inside UIKit. ([UIWebDocumentView inPopover], [_UIPopoverView popoverViewContainingView:], [UITextField _inPopover], …)
Looking at the arrow direction
Someone from Twitter pointed out that there’s even another way to detect if the UIPopoverPresentationController is really used to present as popover, as we can look at the arrowDirection - if that is set to UIPopoverArrowDirectionUnknown it’s likely modal and not a popover.
Are there even better ways? Am I missing something here? I’m not happy with any of the above solutions. Ideally I should be able to ask the controller what his or the nearest ancestor’s adapted current modal presentation is. I’ve filed rdar://22048335 for an API extension - maybe we get this in iOS 10.
With Xcode 61, Apple added support for the NS_DESIGNATED_INITIALIZER flag, and also added this to various framework classes. This is likely a byproduct of Swift, where the initializer call order is much more strongly enforced. This goes as far as there’s a new convenience keyword to mark the separation of convenience vs designated initializers.
With iOS 8.3, Apple made a modification in UITableViewController that by now, every one using this class will have seen. initWithStyle: is now a designated initializer. And while throughout the beta period, this was the only designated one, the GM suddenly added both initWithNibName:bundle: and initWithCoder: to the list as well - making this class quite inconvenient to subclass.
Most of your subclasses will have their own designated initializer since they likely depend on some object at initialization time. If that’s the case, you generally want to prevent users from calling the wrong initializer, even if they are marked designated in the superclass.
A common idiom to do this is to declare them unavailable:
The above code belongs to the header and results in compile-time warnings. Since Objective-C is dynamic, this makes calling init harder, but does not prevent you from shooting yourself in the foot. To be complete, let’s also block this in the implementation:
Combining the two makes it really hard to create objects that are not correctly initialized. So of course I also tried to apply this pattern to UITableViewController… which results in an assert. Here’s the designated initializer chain I’d expect:
However, Apple didn’t really play by the rules in UITableViewController. It calls [super init] inside initWithStyle:. init is overridden in UIViewController to call initWithNibName:bundle: since this is the designated initializer per documentation, even though it’s not annotated with the NS_DESIGNATED_INITIALIZER decoration. This results in following call order:
This is very unfortunate, and I assume it’s not easy to correct since there surely are apps out there who rely on this call order. We work around this by wrapping our implementation into a clang diagnostic block to ignore “-Wobjc-designated-initializers”, but it doesn’t prevent anyone from creating the controller with an invalid state. Maybe Apple fixes this conditionally in iOS 9. rdar://problem/20549233.
What other ways are there do deal with it? Did I miss something here?
1. To be correct, Clang commit r196314 landed in Xcode 5.1, so technically this already had support for designated initializers by using the objc_designated_initializer attribute directly.
Apple’s first GitHub-released open source project is a big thing. There’s much to learn here - I’ve spent some time reading through the source, here are my observations.
Apple added Dynamic Type in iOS 7 to give the user more control about how large text in apps should be. We’re now less than two months away from iOS 9 yet many apps still ignore this, and almost no app properly reacts to changing this setting at runtime. The system sends a UIContentSizeCategoryDidChangeNotification, however there’s no easy way to re-build the UI with a different font. Apple’s way of solving this is subclassing common view classes like UILabel with their ORKLabel, which fetches the new font and then invalidates its intrinsic content size to trigger a new Auto Layout pass. Similar patters are in ORKAnswerTextField/View, ORKTableViewCell and ORKTextButton. This pattern however makes it hard to set custom font sizes. One could extend these classes to accept a font text style like UIFontTextStyleHeadline to make this more flexible. Apple instead uses subclasses like ORKTapCountLabel to customize the font size.
Radar Workarounds
In Apple’s initial release, there are two radars referenced. 19528969 to work around an Auto Layout issue and 19792197 to work around an issue with tinting animated images. Of course there are no detailed entries on OpenRadar but it’s easy to read and at least the workarounds are marked as such. It will be interesting if these radars are a priority on being fixed…
Interface Builder
All views are created in code. Apple uses a Storyboard for the example catalog, but that’s it. Apple uses the standard pattern of overriding viewDidLoad to build UI in combination with Auto Layout and the visual format language, whenever possible.
Creating PDF from HTML
This was particularly interesting, since my main job is working on PSPDFKit - a PDF framework for iOS and Android.. In there we have code that allows converting HTML to PDF via (ab)using UIWebView and the printing subsystem. This is marked as experimental as we were under the impression that it’s not intended usage and more likely works by accident. However Apple’s now using the exact same technique (ORKHTMLPDFWriter) in ResearchKit, so this seems to be an acceptable way of converting HTML documents.
Nullability
It’s really great to see that every class is fully annotated with NS_ASSUME_NONNULL_BEGIN/END. This makes usage much nicer, especially with Swift, but also is great documentation in general. Time to annotate your classes as well!
Swift
Since we’re at Swift… ResearchKit is 100% Objective-C. And I’m sure this was started when Swift was already post 1.0 so time is not the reason. Then again, the example catalog is completely Swift. Objective-C is a great choice for frameworks as you can decide selectively which methods should be public and which ones private - with Swift, this is currently not yet possible.
Update: Access control actually is in Swift since 1.0, so this isn’t the reason they went with Objective-C. Maybe because of the still immature tooling? (and SourceKit crashes)
Internal/Private
There’s no clear pattern when Apple uses _Internal and when _Private for private class extensions, however it’s great to see that they do try to keep the API small and only expose the necessary parts.
Web Views
Large text like the consent review language is displayed by view controllers that embed web views. This is all based on UIWebView - so far no WKWebView is being used here. For regular text, that’s perfectly ok and probably even preferred since it’s a lot simpler to use and doesn’t spin up a separate process. On the other hand, Apple consistently uses UIAlertController - there are no references to the legacy UIAlertView/UIActionSheet APIs anymore.
NSSecureCoding
It’s great to see Apple adopting secure coding everywhere. They’re using a set of macros to make the code less repetitive but overall there’s nothing special about it.
Accessibility
There’s a bunch of interesting details on how Apple approaches accessibility support here. Notable is the ORKAccessibilityStringForVariables macro which allows string concatenation, ignoring empty or nil strings. (sample usage)
Version Checks
ResearchKit contains a few checks for iOS 8.2. Why? Because HealthKit really didn’t work before that release. However instead of checking for the foundation version (fast) or using the new isOperatingSystemAtLeastVersion method on NSProcessInfo, they’re converting the version to float and then compare - the worst way of version checking. I went ahead and wrote a pull request to fix that. We’ll see if that gets merged :)
Tests!
Yes, there are unit tests. They don’t use a Host Application, so they’re all purely model-tests. I’d love to see view/integration tests as well, but it’s a start.
Tinted Animations
If you’re wondering how Apple pulled of these nifty animations and were expecting some advanced path animation code, I have to disappoint - it’s just a set of videos. However, there’s a lot more to it. They are coordinated by ORKVisualConsentTransitionAnimator which is powered by ORKEAGLMoviePlayerView - complete with customshaders. This is a lot of code to tint a video on the fly!
Final Notes
Overall, ResearchKit is very well done. You could critizise some naming inconsistencies, indentation or spacing, but the overall structure is good, and I’m very excited how much better it’ll get once Apple starts merging the onslaught of Pull Requests.. Writing a framework is certainly a challenge - many shortcuts one can do with writing Apps don’t apply. Follow me on Twitter for even more updates. Oh, and if you would love to work on frameworks full-time, we’re hiring.
I’ve had a lot of fun today hunting down a particular regression in iOS 8 that caused rotation when the interface was configured to not autorotate. This is particular fun since this was reported by a PSPDFKit customer and since they’re paying for our product, they also expect a solution. So giving them a “It’s an UIKit regression” answer isn’t good enough. Prepared with IDA and decompiled versions of UIKit iOS 7.1 (where everything works) and UIKit iOS 8.1 (where things are broken) I’ve spend the better part of a day diffing and understanding the root cause.
Here’s my gist with (slightly unordered) thoughts as I went deeper and deeper into the rabbit hole. If you’re curious about UIKit, you’ll find this very interesting to read.
In PSPDFKit we create a few custom windows for various features, like the global progress HUD or the custom text loupe (Dupe rdar://17265615 if you feel like this should be an official API). There’s no easy workaround to not use windows for this features (or rather, this would be a usability regression), so in the end, I’ve came up with a not-extremely terrible workaround that works on iOS 8 and doesn’t do any damage on iOS 7:
This solution “hides” the rootViewController to basically disable any automatic rotation while the window is hidden, which perfectly solves our issue. I have to admit that I quite enjoy digging through closed source code and trying to understand the pieces bit by bit.
A while ago, I’ve stumbled on a string called UIPopoverControllerPaintsTargetRect in some UIKit disassembly - definitely worth investigating! Now that I finally own IDA, I did some research. Turns out there’s a hidden preferences file under /Library/Preferences/com.apple.UIKit that UIKit queries for these settings.
I’ve used Aspects to swizzle NSUserDefaults and enable this key when queried. This actually works, but only under iOS 7, since iOS 8 uses the newer UIPopoverPresentationController and that one doesn’t fully support target rect drawing (or it’s compiled out in our release version of UIKit.)
Digging deeper, I found a bunch of very interesting and useful flags for debugging, which print extensive logging for touches, gestures, animations and more. I’ve listed the most interesting flags in the gist at the end of this article.
The process was easy for UIPopoverControllerPaintsTargetRect but quite a bit harder for most other flags, as these are protected by a check to CPIsInternalDevice() which lives in the private AppSupport.framework. All it does is query libMobileGestalt for a few settings; checking if "InternalBuild" is true or if alternatively "Oji6HRoPi7rH7HPdWVakuw" is set to YES.
I’ve tried to use dlsym to get MGSetAnswer() and set the values manually, however this doesn’t work - it seems that only a few values are modifiable here. So instead, I’ve used Facebook’s fishhook to redirect all calls from MGGetBoolAnswer and manually return YES if it’s queried for “InternalBuild”. Granted, we could also hook CPIsInternalDevice instead; both will work.
Want to try for yourself? Check out my UIKitDebugging repository and add all files to your repository. Remember, that’s just for debugging and to satisfy your curiosity, don’t ship any of that.
Here’s some interesting output. The touch and gesture logging seems very useful.
There are a few other interesting flags like the infamous UISimulatedApplicationResizeGestureEnabled, I’ve only listed the most interesting ones in the gist…
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.
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:
1234567891011121314151617181920212223
BOOLPSPDFReplaceMethodWithBlock(Classc,SELorigSEL,SELnewSEL,idblock){PSPDFAssert(c&&origSEL&&newSEL&&block);if([crespondsToSelector:newSEL])returnYES;// Selector already implemented, skipMethodorigMethod=class_getInstanceMethod(c,origSEL);// Add the new method.IMPimpl=imp_implementationWithBlock(block);if(!class_addMethod(c,newSEL,impl,method_getTypeEncoding(origMethod))){PSPDFLogError(@"Failed to add method: %@ on %@",NSStringFromSelector(newSEL),c);returnNO;}else{MethodnewMethod=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);}}returnYES;}
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:
12345
SELtouchesMovedSEL=NSSelectorFromString(@"pspdf_wacomTouchesMoved:withEvent:");PSPDFWacomSwizzleMethodWithBlock(viewClass,@selector(touchesMoved:withEvent:),touchesMovedSEL,^(UIView*_self,NSSet*touches,UIEvent*event){[WacomManager.getManager.currentlyTrackedTouchesmoveTouches:touchesknownTouches:[eventtouchesForView:_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.
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.)
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.)
staticvoidforwardTouchMethod(idself,SEL_cmd,NSSet*touches,UIEvent*event){// The responder chain is used to figure out where to send the next touchUIResponder*nextResponder=[selfnextResponder];if(nextResponder&&nextResponder!=self){// Not all touches are forwarded - so we filter here.NSMutableSet*filteredTouches=[NSMutableSetset];[touchesenumerateObjectsUsingBlock:^(UITouch*touch,BOOL*stop){// Checks every touch for forwarding requirements.if([touch_wantsForwardingFromResponder:selftoNextResponder:nextResponderwithEvent:event]){[filteredTouchesaddObject:touch];}else{// This is interesting legacy behavior. Before iOS 5, all touches are forwarded (and this is logged)if(!_UIApplicationLinkedOnOrAfter(12)){[filteredTouchesaddObject:touch];// Log old behaviorstaticBOOLdidLog=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];[nextResponderperformSelector:_cmdwithObject:filteredToucheswithObject: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:
12345678910111213141516171819202122
__blockIMPoriginalIMP=PSPDFReplaceMethodWithBlock(viewClass,@selector(touchesMoved:withEvent:),^(UIView*_self,NSSet*touches,UIEvent*event){[WacomManager.getManager.currentlyTrackedTouchesmoveTouches:touchesknownTouches:[eventtouchesForView:_self]view:_self];((void(*)(id,SEL,NSSet*,UIEvent*))originalIMP)(_self,@selector(touchesMoved:withEvent:),touches,event);});staticIMPPSPDFReplaceMethodWithBlock(Classc,SELorigSEL,idblock){NSCParameterAssert(block);// get original methodMethodorigMethod=class_getInstanceMethod(c,origSEL);NSCParameterAssert(origMethod);// convert block to IMP trampoline and replace method implementationIMPnewIMP=imp_implementationWithBlock(block);// Try adding the method if not yet in the current classif(!class_addMethod(c,origSEL,newIMP,method_getTypeEncoding(origMethod))){returnmethod_setImplementation(origMethod,newIMP);}else{returnmethod_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.)
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…
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.
To enable these fixes, simply use PSPDFTextView instead of UITextView:
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.
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.
Can’t stand Apple’s missing attention to detail in iOS 7. I’m just going to hack and patch this myself. pic.twitter.com/Nnd176rPlz
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:
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.