iOS 7: Hidden Gems and Workarounds

When iOS 7 was first announced, Apple developers all over the world tried to compile their apps, and spent the next few months fixing whatever was broken, or even rebuilding the app from scratch. As a result, there wasn’t much time to take a closer look at what’s new in iOS 7. Apart from the obvious great small tweaks like NSArray’sfirstObject, which has been retroactively made public all the way back to iOS 4, there are a lot more hidden gems waiting to be discovered.


Smooth Fade Animations

I’m not talking about the new spring animation APIs or UIDynamics, but something more subtle. CALayer gained two new methods: allowsGroupOpacity andallowsEdgeAntialiasing. Now, group opacity isn’t something particularly new. iOS was evaluating the UIViewGroupOpacity key in the Info.plist for quite some time and enabled/disabled this application-wide. For most apps, this was unwanted, as it decreases global performance. In iOS 7, this now is enabled by default for applications that link against SDK 7 - and since certain animations will be slower when this is enabled, it can also be controlled on the layer.

An interesting detail is that _UIBackdropView (which is used as the background view inside UIToolbar or UIPopoverView) can’t animate its blur if allowsGroupOpacity is enabled, so you might want to temporarily disable this when you do an alpha transform. Since this degrades the animation experience, you can fall back to the old way and temporarily enable shouldRasterize during the animation. Don’t forget setting the appropriate rasterizationScale or the view will look pixelerated on retina devices.

The edge antialiasing property can be useful if you want to replicate the animation that Safari does when showing all tabs.

Blocking Animations

A small but very useful addition is [UIView performWithoutAnimation:]. It’s a simple wrapper that checks if animations are currently enabled, disables them, executes the block, and re-enables animations. One caveat is that this will not block CoreAnimation-based animations. So don’t be too eager in replacing all your calls from:

    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    view.frame = CGRectMake(...);
    [CATransaction commit];

to:

    [UIView performWithoutAnimation:^{
        view.frame = CGRectMake(...);
    }];

However, most of the time this will do the job just fine, as long as you don’t deal with CALayers directly.

In iOS 7, I had quite a few code paths (mostly UITableViewCells) that needed additional protection against accidental animations, for example, if a popover is resized and at the same time the displayed table view loads up new cells because of the height change. My usual workaround is wrapping the entire layoutSubviews into the animation-block-method:

- (void)layoutSubviews 
{    // Otherwise the popover animation could leak into our cells on iOS 7 legacy mode.
    [UIView performWithoutAnimation:^{
        [super layoutSubviews];
        _renderView.frame = self.bounds;
    }];
}

Dealing with Long Table Views

UITableView is very efficient and fast, unless you begin usingtableView:heightForRowAtIndexPath:, where it starts calling this for every element in your table, even the non-visible ones - just so that the underlying UIScrollViewcan get the correct contentSize. There were some workarounds before, but nothing great. In iOS 7, Apple finally acknowledged the problem and addedtableView:estimatedHeightForRowAtIndexPath:, which defers most of the cost down to actual scrolling time. If you don’t know the size of a cell at all, simply returnUITableViewAutomaticDimension.

There’s now a similar API for section headers/footers as well.

UISearchDisplayController

Apple’s search controller learned a new trick to simplify moving the search bar into the navigation bar. Enable displaysSearchBarInNavigationBar for that (unless you also use a scope bar - then you’re out of luck.) Now I would love to write that that’s it, but sadly, UISearchDisplayController seems to be terribly broken on iOS 7, especially on iPad. Apple seems to have run out of time, so showing the search results will not hide the actual table view. Before 7, this wasn’t an issue, but since thesearchResultsTableView has a transparent background color, it looks pretty bad. As a workaround, you can either set an opaque background color, or move to some more sophisticated ways of hacking to get what you expect. I’ve had very mixed results with this control, including it not showing the search table view at all when usingdisplaysSearchBarInNavigationBar.

Your results may vary, but I’ve required some severe hacks to getdisplaysSearchBarInNavigationBar working:

- (void)restoreOriginalTableView 
{    if (PSPDFIsUIKitFlatMode() && self.originalTableView) {        self.view = self.originalTableView;
    }
}

- (UITableView *)tableView 
{    return self.originalTableView ?: [super tableView];
}

- (void)searchDisplayController:(UISearchDisplayController *)controller 
  didShowSearchResultsTableView:(UITableView *)tableView 
{    // HACK: iOS 7 requires a cruel workaround to show the search table view.
    if (PSPDFIsUIKitFlatMode()) {        if (!self.originalTableView) self.originalTableView = self.tableView;        self.view = controller.searchResultsTableView;
        controller.searchResultsTableView.contentInset = UIEdgeInsetsZero; // Remove 64 pixel gap
    }
}

- (void)searchDisplayController:(UISearchDisplayController *)controller 
  didHideSearchResultsTableView:(UITableView *)tableView 
{
    [self restoreOriginalTableView];
}

Also, don’t forget calling restoreOriginalTableView in viewWillDisappear, or things will crash badly. Remember that this is only one solution; there might be less radical ways that don’t replace the view itself, but this really should be fixed by Apple. (TODO: RADAR!)

Pagination

UIWebView learned a new trick to automatically paginate websites withpaginationMode. There are a whole bunch of new properties related to this feature:

@property (nonatomic) UIWebPaginationMode paginationMode NS_AVAILABLE_IOS(7_0);@property (nonatomic) UIWebPaginationBreakingMode paginationBreakingMode NS_AVAILABLE_IOS(7_0);@property (nonatomic) CGFloat pageLength NS_AVAILABLE_IOS(7_0);@property (nonatomic) CGFloat gapBetweenPages NS_AVAILABLE_IOS(7_0);@property (nonatomic, readonly) NSUInteger pageCount NS_AVAILABLE_IOS(7_0);

Now while this might not be useful for most websites, it certainly is to build simple ebook readers or display text in a nicer way. For added fun, try setting it toUIWebPaginationModeBottomToTop.

Flying Popovers

Wonder why your popovers are flying around like crazy? There’s a new delegate in theUIPopoverControllerDelegate protocol which allows you to control the madness:

-     (void)popoverController:(UIPopoverController *)popoverController
  willRepositionPopoverToRect:(inout CGRect *)rect 
                       inView:(inout UIView **)view

UIPopoverController will behave if anchored to a UIBarButtonItem, but if you’re showing it with a view and rect, you might have to implement this method and return something sane. This took me quite a long time to figure out - it’s especially required if you dynamically resize your popovers via changing preferredContentSize. Apple now takes those sizing requests more serious and will move the popover around if there’s not enough space left.

Keyboard Support

Apple didn’t only give us a whole new framework for game controllers, it also gave us keyboard-lovers some attention! You’ll find new defines for common keys likeUIKeyInputEscape or UIKeyInputUpArrow that can be intercepted using the all-newUIKeyCommand class. Before iOS 7, unspeakable hacks were necessary to react to keyboard commands. Now, let’s grab a bluetooth keyboard and see what we can do with this new hotness!

Before starting, you need some basic understanding for the responder chain. YourUIApplication inherits from UIResponder, and so does UIView andUIViewController. If you’ve ever had to deal with UIMenuItem and weren’t using my block-based wrapper, you know this already. So events will be sent to the topmost responder and then trickle down level by level until they end at UIApplication. To capture key commands, you need to tell the system what key commands you’re interested in (there’s no catch-all). To do so, override the new keyCommands property:

- (NSArray *)keyCommands 
{    return @[[UIKeyCommand keyCommandWithInput:@"f"
                                 modifierFlags:UIKeyModifierCommand  
                                        action:@selector(searchKeyPressed:)]];
}

- (void)searchKeyPressed:(UIKeyCommand *)keyCommand 
{    // Respond to the event}

iOS 7: Hidden Gems and Workarounds_第1张图片

Now don’t get too excited; there are some caveats. This only works when the keyboard is visible (if there’s some first responder like UITextView.) For truly global hotkeys, you still need to revert to the above-linked hackery. But apart from that, the routing is very elegant. Don’t try overriding system shortcuts like cmd-V, as those will be mapped to paste: automatically.

There are also some new predefined responder actions like:

- (void)increaseSize:(id)sender NS_AVAILABLE_IOS(7_0);
- (void)decreaseSize:(id)sender NS_AVAILABLE_IOS(7_0);

which are called for cmd+ and cmd- respectively, to increase/decrease content size.

Match the Keyboard Background

Apple finally made UIInputView public, which provides a way to match the keyboard style with using UIInputViewStyleKeyboard. This allows you to write custom keyboards or (toolbar) extensions for the default keyboards that match the default style. This class has existed since the dawn of time, but now we can finally use it without resorting to hacks.

UIInputView will only show a background if it’s the the root view of your inputView orinputAccessoryView - otherwise it will be transparent. Sadly, this doesn’t enable you to implement a split keyboard without fill, but it’s still better than using a simple UIToolbar. I haven’t yet seen a spot where Apple uses this new API - it still seems to use a UIToolbar in Safari.

Know your Radio

While most of the carrier information has been exposed in CTTelephony as early as iOS 4, it was usually special-case and not useful. With iOS 7, Apple added one method here, the most useful of them all: currentRadioAccessTechnology. This allows you to know if the phone is on dog-slow GPRS, on lighting-fast LTE, or on anything in between. Now there’s no method that would give you the connection speed (since the phone can’t really know that), but it’s good enough to fine-tune a download manager to not try downloading six images simultaneously when the user’s on EDGE.

Now there’s absolutely no documentation around currentRadioAccessTechnology, so it took some trial and error to make this work. Once you have the current value, you should register for the CTRadioAccessTechnologyDidChangeNotification instead of polling the property. To actually get iOS to emit those notifications, you need to carry an instance of CTTelephonyNetworkInfo around. Don’t try to create a new instance ofCTTelephonyNetworkInfo inside the notification, or it’ll crash.

In this simple example, I am abusing the fact that capturing telephonyInfo in a block will retain it:

CTTelephonyNetworkInfo *telephonyInfo = [CTTelephonyNetworkInfo new];NSLog(@"Current Radio Access Technology: %@", telephonyInfo.currentRadioAccessTechnology);
[NSNotificationCenter.defaultCenter addObserverForName:CTRadioAccessTechnologyDidChangeNotification 
                                                object:nil 
                                                 queue:nil 
                                            usingBlock:^(NSNotification *note) 
{    NSLog(@"New Radio Access Technology: %@", telephonyInfo.currentRadioAccessTechnology);
}];

The log output can look something like this, when the phone’s moving from Edge to 3G:

iOS7Tests[612:60b] Current Radio Access Technology: CTRadioAccessTechnologyEdge
iOS7Tests[612:1803] New Radio Access Technology: (null)
iOS7Tests[612:1803] New Radio Access Technology: CTRadioAccessTechnologyHSDPA

Apple exported all string symbols so it’s easy to compare and detect the current technology.

Core Foundation, Autorelease and You.

There’s a new helper in Core Foundation that people have been missing and hacking around for years:

CFTypeRef CFAutorelease(CFTypeRef CF_RELEASES_ARGUMENT arg)

It does exactly what you expect it to do, and it’s quite puzzling how long it took Apple to make it public. With ARC, most people solved returning Core Foundation objects via casting them to their NS-equivalent - like returning an NSDictionary, even though it’s a CFDictionaryRef and simply using CFBridgingRelease(). This works well, until you need to return methods where no NS-equivalent is available, like CFBagRef. Then you either use id and lose any type safety, or you rename your method tocreateMethod and have to think about all the memory semantics and using CFRelease afterward. There were also hacks like this one, using a non-ARC-file so you can compile it, but using CFAutorelease() is really the way to go. Also: Don’t write code using Apple’s namespace. All those custom CF-Macros are programmed to break sooner or later.

Image Decompression

When showing an image via UIImage, it might need to be decompressed before it can be displayed (unless the source is a pixel buffer already). This can take significant time for JPG/PNG files and result in stuttering. Before iOS 6, this was usually solved by creating a new bitmap context and drawing the image into it. (See how AFNetworking solves this).

Starting with iOS 7, you can now force decompression directly at image creation time with the new kCGImageSourceShouldCacheImmediately:

+ (UIImage *)decompressedImageWithData:(NSData *)data 
{
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCacheImmediately: @YES});    
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    CFRelease(source);    return image;
}

I was very excited when I first found out about this, but you really shouldn’t be. In my tests, performance actually decreased when I enabled immediate caching. Either this method calls up to the main thread (unlikely) or perceived performance is simply worse because it locks in copyImageBlockSetJPEG, which is also used when showing a non-decrypted image on the main thread. In my app, I load small preview thumbnails from the main thread, and load the large page images from a background thread. Using kCGImageSourceShouldCacheImmediately now blocks the main thread where only a tiny decompression would take place, with a much more expensive operation on the background thread.

iOS 7: Hidden Gems and Workarounds_第2张图片

There’s a lot more to image decompression which isn’t new to iOS 7, likekCGImageSourceShouldCache, which controls the ability where the system can automatically unload decompressed image data. Make sure you’re setting this to YES, otherwise all the extra work could be pointless. Interesting detail: Apple changed thedefault of kCGImageSourceShouldCache from NO to YES with the 64-bit runtime.

Piracy Check

Apple added a way to evaluate the App Store receipt in Lion with the newappStoreReceiptURL method on NSBundle, and finally also ported this to iOS. This allows you to check if your app was legitimately purchased or cracked. There’s another important reason for checking the receipt. It contains the initial purchase date, which can be very useful when moving your app from a paid model to free and in-app purchases. You can use this initial purchase date to determine if your users get the extra content free (because they already paid for it), or if they have to purchase it.

The receipt also lets you check if the app was purchased via the volume purchase program and if that license wasn’t revoked, there’s a property namedSKReceiptPropertyIsVolumePurchase indicating that.

You need to take special care when calling appStoreReceiptURL, since it exists as private API on iOS 6, but will call doesNotRecognizeSelector: when called from user code. Check the running (foundation) version before calling. During development, there won’t be a file at the URL returned from this method. You will need to use StoreKit’s SKReceiptRefreshRequest, also new in iOS 7, to download the certificate. Use a test user who made at least one purchase, or else it won’t work:

// Refresh the ReceiptSKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] init];
[request setDelegate:self];
[request start];

Verifying the receipt requires a lot of code. You need to use OpenSSL and embed theApple Root Certificate, and you should understand some basics about certificates,PCKS containers, and ASN.1. There’s some sample code out there, but you shouldn’t make it too easy for someone with less honorable intents - don’t just copy the existing validation methods, at least modify them or write your own. You don’t want a generic patcher app to undo your hard work in seconds.

You should definitely read Apple’s guide on Validating Mac App Store Receipts - a lot of this applies to iOS as well. Apple also details the changes with its new “Grand Unified Receipt” in Session 308 “Using Receipts to Protect Your Digital Sales” @ WWDC 2013.

Comic Sans MS

Admit it. You’ve missed Comic Sans MS. With iOS 7, you can finally get it back. Now, downloadable fonts have been added to iOS 6, but back then the list of fonts was pretty small and not really interesting. Apple added some more fonts in iOS 7, including “famous” ones like PT Sans or Comic Sans MS. ThekCTFontDownloadableAttribute was not declared in iOS 6, so this wasn’t really usable until iOS 7, but Apple retroactively declared this property to be available on iOS 6 upward.

iOS 7: Hidden Gems and Workarounds_第3张图片

The list of fonts is dynamic and might change in the future. Apple lists some of the available fonts in Tech Note HT5484, but the document is outdated and doesn’t yet reflect the changes on iOS 7.

Here’s how you get an array of fonts that can be downloaded withCTFontDescriptorRef:

CFDictionary *descriptorOptions = @{(id)kCTFontDownloadableAttribute : @YES};
CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)descriptorOptions);
CFArrayRef fontDescriptors = CTFontDescriptorCreateMatchingFontDescriptors(descriptor, NULL);

The system won’t check if the font is already on disk and will return the same list. Additionally, this method might do a network call and thus block. You don’t want to call this from the main thread.

To download the font, use this block-based API:

bool CTFontDescriptorMatchFontDescriptorsWithProgressHandler(
         CFArrayRef                          descriptors,
         CFSetRef                            mandatoryAttributes,
         CTFontDescriptorProgressHandler     progressBlock)

This method handles the network call and calls your progressBlock with progress information until the download either succeeds or fails. Refer to Apple’sDownloadFont Example to see how this can be used.

There are a few gotchas here. This font will only be available during the current app run, and has to be loaded again into memory on the next run. Since fonts are saved in a shared place, you can’t rely on them being available. They most likely will be, but it’s not guaranteed and the system might clean this folder, or your app is being copied to a new device where the font doesn’t yet exist, and you might run without a working network. On the Mac or in the Simulator you can obtain the kCTFontURLAttribute to get the absolute path of the font and speed up loading time, but this won’t work on iOS, since the folder is outside of your app - you need to callCTFontDescriptorMatchFontDescriptorsWithProgressHandler again.

You can also subscribe to the newkCTFontManagerRegisteredFontsChangedNotification to be notified whenever new fonts are loaded into the font registry. You can find out more in Session 223 “Using Fonts with TextKit” @ WWDC 2013.

Can’t Get Enough?

Don’t worry - there’s a lot more new in iOS 7! Over at NSHipster you’ll learn about speech synthesis, base64, the all-new NSURLComponentsNSProgress, bar codes, reading lists, and, of course, CIDetectorEyeBlink. And there’s a lot more waiting we couldn’t cover - study Apple’s iOS 7 API Diffs, the What’s new in iOS guide, and theFoundation Release Notes (those are for OS X - but since this is shared code, a lot applies to iOS as well). Many of the new methods don’t even have documentation yet, so it’s up to you to play around and blog about it!



More articles in issue #5

  • Privacy policy



转载于:https://my.oschina.net/u/554222/blog/222937

你可能感兴趣的:(iOS 7: Hidden Gems and Workarounds)