High Performance iOS Apps - Reading Note - Part II

Concurrent Programming

topics:

  • Creating and managing threads
  • The Great Central Dispatch (GCD) abstraction
  • Operations and queues

Threads

Stack Size

The main thread stack size is 1 MB and cannot be changed. Any secondary thread is allocated 512 KB of stack space by default.

Note that the full stack is not immediately created. The actual stack size grows with use

Before a thread starts, the stack size can be changed. The minimum allowed stack size is 16 KB, and the size must be a multiple of 4 KB.

1
2
3
//Example for changing thread stack size
+(NSThread *)createThreadWithTarget:(id)target selector:(SEL)selector object:(id)argument stackSize:(NSUInteger)size {
	if( (size % 4096) != 0) { 
return nil; } NSThread *t = [[NSThread alloc] initWithTarget:target selector:selector object:argument]; t.stackSize = size; return t;
}

GCD

review of GCD fundamentals, check out Ray Wenderlich’s Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial

In the scenarios where your app has multiple long-running tasks to be executed concurrently, it is better to take control of the thread creation. If your code takes longer to complete, you may soon hit the limit of 64, the maximum GCD thread pool size.

Operations and Queues

comparison of the NSThread, NSOperationQueue, and GCD APIs:

GCD

  • Highest abstraction.
  • Two queues are available out of the box: main and global.
  • Can create more queues (using dispatch_queue_create).
  • Can request exclusive access (using dispatch_barrier_sync and dispatch_bar rier_async).
  • Manages underlying threads.
  • Hard limit on 64 threads created.

NSOperationQueue

  • No default queues.
  • App manages the queues it creates.
  • Queues are priority queues.
  • Operations can have different priorities (use the queuePriority property).
  • Operations can be cancelled using the cancel message. Note that cancel is merely a flag. If an operation is under execution, it may continue to execute.
  • Can wait for an operation to complete (use the waitUntilFinished message).

NSThread

  • Lowest-level construct, gives maximum control.
  • App creates and manages threads.
  • App creates and manages thread pools.
  • App starts the threads.
  • Threads can have priority. OS uses this for scheduling their execution.
  • No direct API to wait for a thread to complete. Use a mutex (e.g., NSLock) and custom code.

Locks

Locks are the basic building blocks to enter a critical section. atomic properties and @synchronized blocks are higher-level abstractions available for easy use.

NSLock

NSLock must be unlocked from the same thread where it was locked. NSLock does not allow lock to be called more than once without first calling unlock.

NSRecursiveLock

NSRecursiveLock, as the name indicates, does allow lock to be called more than once before it is unlocked. Each lock call must be matched with an equal number of unlock calls before the lock can be considered released for another thread to acquire.

NSCondition
A thread can wait on a condition that releases the lock. Another thread can sig nal the condition by releasing the same lock and awakening the waiting thread. The standard producer–consumer problem can be solved using NSCondition.

Use Reader–Writer Locks for Concurrent Reads and Writes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface HPCache ()
@property (nonatomic, readonly) NSMutableDictionary *cacheObjects; @property (nonatomic, readonly) dispatch_queue_t queue;

-(id)objectForKey:(id) key;
-(void)setObject:(id)object forKey:(id)key;

@end
@implementation HPCache

-(instancetype)init {
if(self = [super init]) { _cacheObjects = [NSMutableDictionary dictionary]; _queue = dispatch_queue_create(kCacheQueueName, DISPATCH_QUEUE_CONCURRENT); } return self;
}

-(id)objectForKey:(id<NSCopying>)key {
__block id rv = nil;
//Use dispatch_sync (or dispatch_async) for operations that do not modify state. dispatch_sync(self.queue, ^{ rv = [self.cacheObjects objectForKey:key]; });
return rv;
}
-(void)setObject:(id)object forKey:(id<NSCopying>)key {
//Use dispatch_barrier_sync (or dispatch_barrier_async) for operations that may modify state.
dispatch_barrier_async(self.queue, ^{ [self.cacheObjects setObject:object forKey:key]; });
}

Use Immutable Entities

Best practices:

  • Use immutable entities.
  • Support them with an updater subsystem.
  • Allow observers to receive notifications on data changes.

Have a Central State Updater Service

The model layer was built based on three principles:

  • Immutability
  • Denormalized storage
  • Asynchronous, opt-in consistency

Application Lifecycle

App Delegate

The App Life Cycle

Application Launch

There are four types of application launches possible:

  • First launch
  • Cold start
  • Warm (re)start
  • Launch a er upgrade

First launch

Although each subsystem may be individually performant, their performance may drop when used together. For example, if multi‐ ple components attempt to simultaneously read from the filesys‐ tem, it will result in an overall sluggishness.

How can we optimize the performance if these (possibly mandatory) initializations add up to a long initialization time?

follow these concrete steps to split down the task list for more effective performance:

  • Identify what must be executed before the UI can be shown.

    If the app is being launched for the first time, there is no need to load any user preferences such as theme, refresh interval, cache size, and so on. There will be Application Launch | 153
    no custom values. It is OK to let the initial cache grow wild, as we know that it will not grow beyond a fraction of the final intended limit.
    The crash reporting system should be the first one to be initialized.

  • Order the tasks.

    Ordering is very important—not only because the tasks may have interdependen‐ cies but also because it may save you precious user time.

  • Split the tasks into two categories—tasks that must execute in the main thread only and tasks that can execute in other threads—and execute them accordingly.

    It is possible to further split the tasks that can be executed in a non-main thread into those that can be executed concurrently and those that cannot be.

  • Other tasks can either be executed after the UI is loaded or may be fired asyn‐ chronously.

    Delay initialization of other subsystems, such as loggers and analytics. It may be possible to queue operations (e.g., writing log messages or tracking events) until these subsystems are completely initialized, which may happen late in the app’s life.

Example Code

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
54
55
56
57
58
59
60
61
62
63
@interface HPInstrumentation ()

@property (nonatomic, copy) BOOL initialized;
@property (nonatomic, strong) NSMutableArray *events;
@property (nonatomic, strong) dispatch_queue_t queue;

-(void)markInitialized;
+(void)logEventImpl:(NSString *)name;

@end

static HPInstrumentation *_instance;

@implementation HPInstrumentation

+(HPInstrumentation *)sharedInstance {
return _instance;
}

+(void)setSharedInstance:(HPInstrumentation *)instance {
_instance = instance;
}

+(void)logEvent:(NSString *)name {
[[HPInstrumentation sharedInstance] logEventImpl:name];
}

-(instancetype)initWithAPIKey:(NSString *)apiKey {
if(self = [super init]) {
self.initialized = NO;
self.events = [NSMutableArray array];
self.queue = dispatch_queue_create("com.m10v.queue.analytics", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[Flurry startSession:apiKey];
self.delegate = [[HPInstrumentationUseList alloc] init];
//Code below is for after initialization
dispatch_sync_barrier(self.queue, ^{
for(NSDictionary *name in self.delegate.events) {
[Flurry logEvent:name];
}
self.delegate = [[HPInstrumentationUseSDK alloc] init];
});
});
}
return self;
}

-(void)logEventImpl:(NSString *)name {
dispatch_sync(self.queue, ^{
[self.delegate logEvent:name];
});
}

@end

//HPAppDelegate.m
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *) launchOptions {
HPInstrumentation *analytics = [[HPInstrumentation alloc] initWithAPIKey:@"API_KEY"];
[HPInstrumentation setSharedInstance:analytics];
[HPInstrumentation logEvent:@"App Launched"];
}

Cold Start

it’s important to know the following:

  • Minimum number (min) of entries required to show a usable and meaningful UI
  • Time it takes to load M entries from local cache (let’s call it tl)
  • Time it takes to get latest M entries from remote server (let’s call it tr)
  • Maximum number (max) of entries you will ever keep in memory at any given point in time for speedier access, especially during fast swipes and scrolls

f we cannot load M entries in 3 seconds, the user experience degrades significantly. why 3seconds

Warm Launch

There are two scenarios for warm launch:

  • User taps the icon
  • App receives a deep link

Launch After Upgrade

The following best practices can be applied to inform the user about what is about to happen to the local data.

  • When the local cache can be used, inform the user about it. Do not inform the user if there is no migration required because then the use of the local cache is implicit.
  • If the data must be migrated and the process may take several minutes, give an option to postpone it.
  • When the local cache must be discarded because it is easier and faster to retrieve data from the server, inform the user about it.
    A case in hand is when migrating several records, say in a mail app, to the upda‐ ted schema in the newer version of the app may be more complex than retrieving them from the server.

Push Notifications

Remote Notifications

  • If the app is active, it receives the notification via the didReceiveRemoteNotifica
    tion callback.
    No other callbacks are invoked. No UI is presented to the user to avoid any dis‐
    traction.
  • If the app is backgrounded or stopped, only the silent push notification callbacks are triggered.
    Based on the notification settings, non-silent push notifications may appear in the notification center or as alerts and/or update the app icon’s badge counter.
  • When the user opens a notification by interacting with it using the notification center or an alert, one of the following may happen:

    1. If the app was backgrounded, the notification callback method is invoked.
    2. If the app was stopped, the notification object (NSDictionary) is available in the launchOptions parameter to the method application:didFinishLaun chingWithOptions:.

application:didReceiveRemoteNotification:fetch CompletionHandler:, if implemented, can be called even if the app is in the background and can even start the app if it is not already running. Such notifications are known as silent push notifications.

It is also important to note that the latter method may be called twice:

  • First when the notification is received and the payload contains the key content-available with value 1
  • Second when the user interacts with the notification, either in the notification center or an alert

use application state to resolve this problem

1
2
3
4
5
6
7
8
9
10
11
12
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo 
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
//process remote notification - app running
if(application.applicationState == UIApplicationStateInactive) {
//user tapped the notification in notification center or the alert
[self processRemoteNotification:userInfo];
} else if(application.applicationState == UIApplicationStateBackground) {
//App in background, no user interaction - just fetch data
}else{
//app is already active - show in-app update
}
}

Local Notifications

Unlike remote notifications, local notifications will not show any UI when the app is in use.

Use local notifications in conjunction with silent push notifications to make the app responsive and usable faster on the next launch.

Background Fetch

There are three basic steps to enable background fetch:

  1. Enable the capability in your project settings.
  2. Set the refresh interval, preferably in application:didFinishLaunchingWithOptions. Use the -UIApplication setMinimumBackgroundFetchInterval: method to request that the refresh be done at the specified frequency.
  3. Implement the application:performFetchWithCompletionHandler: app delegate method. If the task is not complete within 30 seconds, the OS will schedule the method execution less frequently.
    On a practical note, the typical time that the app may get is much smaller, generally in the range of 2–4 seconds. The Apple developer website lists 30 seconds as the upper-bound limit.

Summary

At times, actual performance may be less important than the perceived performance. Using silent notifications and background fetches to warm up the app to keep it ready for the next use is a smart way to achieve a positive perception of your app.

Now that we’ve covered optimization techniques for various scenarios, including first launch, cold start, warm start, and launch after upgrade, you should be able to mini‐ mize the startup time and make your app usable as quickly as possible.


User Interface

There are external factors that you cannot control

  • Network

    Poor network conditions increases the time taken to synchronize.

  • Hardware

    Better hardware makes for better performance

  • Storage

    The app may run on devices with varying storage ranging from 16 GB to 128 GB, which limits the offline cache that your app can store.

View Controller

basic best practices to follow when structuring view controllers:

  • Keep the view controller lightweight. In an MVC app, the controller is only glue. This is not where the entire business logic lies. It does not even belong to the model. The business logic belongs to the service or business logic component. Keep it there.
    The view controller should bind the service component to the views by means of what can be termed action delegates or service providers, which should preferably be injected into the controller
    • Do not write animation logic in the view controller. This may be in an independ‐ ent animation class that accepts views to apply animations on. View controllers attach the animations to the views or transitions.
      Special-purpose views may own their own animations. For example, a custom spinner controller will have its own animation.
    • Use data source and delegate protocols to separate the code pertaining to data retrieval, update, and other business logic. View controllers are restricted to pick‐ ing correct views and connecting them to the feeders.
    • View controllers respond to events from the views—for example, button click or table cell selection—and connect them back into data sinks.
    • View controllers respond to UI-related events from the OS—for example, orien‐ tation changes or low-memory warnings. This may trigger relayout of the views.
    • Do not write custom init code. Why? Well, what if your view controller is repur‐ posed to an XIB or storyboard? The init method will never be called.
    • Do not handcraft the UI in the view controller using code. Do not implement all UI, view creation, and view layout logic in the view controller. Use nibs or story‐ boards.
      Handcrafted code does not last long, especially as the app grows and designs change. It is faster to redesign using Interface Builder than to hand code pixel coordinates.
      In addition, the app may run on devices with different sizes and form factors. It is difficult to scale custom code to work with all form factors, handle rotation dur‐ ing orientation changes, and keep pace with new design paradigms that evolve every couple of years or so
      Also, when you have the design separated out in independent nibs and story‐ boards, you will have the flexibility to run A/B testing where it is easy to choose between different layouts.
    • Prefer creating a base view controller with common setup and have other view controllers inherit from this.
      This technique is not always possible, because there may be a need to inherit from different view controllers at different parts of the app. For example, you should use UITableViewController for the contacts list and UIViewController for the user profile.
      However, if you have multiple places where you need to show content in a UIWeb View, a base view controller will work well. If you need to display the privacy pol‐ icy URL or terms and conditions page, you do not need to subclass. However, if you need to show an image or video that a user shares (in a messaging app), you can create subclasses that can define custom chrome or control overlays.
    • Use categories for creating reusable code across view controllers. In case a parent view controller does not suffice (for example, because you need different types of view controllers in your app), create categories and add your custom methods or properties there.
      That way, you are not restricted to using a predefined base class and still get the benefit of reusability.

View Load

loadView

  • Do set the view property to the root of the view hierarchy.
  • Ensure that the views are now shared with any other view controllers.
  • Do not call [super loadView].

viewDidLoad

  • Configuring data sources to populate data.
  • Binding data to views.

    This is a debatable item. Depending on the use case, you may bind data once and have a refresh button, or bind each time viewWillAppear is called. The upside of doing the latter is that the UI always has the latest data. The downside is that if the data does not update frequently (e.g., in a news app), the user may see an unnecessary refresh each time (say, when the UITableView rebinds).

  • Binding view event handlers, data source delegates, and other callbacks.
  • Registering observers on data.

    Depending on where you bind data to the views, observers on data may also change.

  • Monitoring for notifications from the notification center.
  • Initializing animations.

View Hierarchy

View construction and rendering involves the following steps:

  1. Construct subviews.
  2. Compute and apply constraints.
  3. Perform steps 1 and 2 recursively for the subviews.
  4. Render recursively.

Our focus now will be not only to get as much as possible of the main thread execu‐ tion done within 16 ms, but also to minimize the number of frames dropped (or bulk drop of frames, to be more explicit).

View Visibility

Some best practices for effectively using these lifecycle events are given next

  • Do not override loadView. Enough said.
  • Use viewDidLoad as the last checkpoint to see if the data from data sources is available. If so, update the UI elements.
  • Use viewWillAppear: to update the UI elements—but only if you really want to always show the latest details.
    For example, in a messaging app, if the user returns to the message list in the chat session after watching a shared video, you would want to refresh it with the latest messages.
    However, in a news app, you may not want to immediately refresh the list with all the new articles, lest the user lose the context. In the latter case, the table view view controller will generally listen to the events from the data source and prefer to make subtle and infrequent updates to the list of new articles.
  • Use viewDidAppear: to start animations. If you have streaming content such as video, play that. Subscribe to application events to detect if the animation/video or any other processing that continuously updates the video should continue or not.
    It is not advisable to update the UI here with the latest data. If you do so, the final effect will be that the user transitions into an old UI followed by an update after the transition animation is complete, which may not be a great experience.
    Having said that, there may still be use cases that force you to perform UI updates in viewDidAppear:. If the user experience is acceptable, go ahead with it.
  • Use viewWillDisappear: to pause or stop animations. Again, do nothing more.
  • Use viewDidDisappear: to tear down any complex data structures that you may
    have held in memory.
    This is also a good time to unregister for any notifications from the data sources that the view controller may be bound to and also with the notification center for the app events that will have been connected to animations, data sources, or other UI updates.

View

basic rules to follow

  • Minimize work done in the main thread. Any extra code to be executed means higher chances of dropping a frame. Too many frames dropped will introduce jitter.
  • Avoid fat nibs or storyboards. Storyboards are great, but the entire XML must be loaded (I/O) and parsed (XML processing) before it can really be used. Minimize the number of units that go into storyboards.
    If needed, create multiple storyboards or nib files. This will ensure that all the screens are not loaded in one go during app launch, but are loaded as needed. This not only helps the app start-up time but also keeps the overall memory requirement lower. Example in Twitter
  • Avoid multiple layers of nesting in the view hierarchy. Try to keep it as flat as possible. Nesting is a necessary evil, but still an evil.
    Each time a view is added anywhere in the hierarchy, its ancestor tree receives a setNeedsLayout: with a value of YES that causes layoutSubviews: to be invoked when the event queue is processed. This is an expensive call because the view has to recompute the positions of the subviews using the constraints, and it happens for each level in the ancestor tree.
  • Lazy-load the views and reuse them wherever possible. The more views you have, the longer it takes to not only load but also render them, which impacts both memory and CPU usage.
    If needed, create your own view cache. This may be over and above the cell-reuse support already provided in UITableView and UICollectionView. These con‐ tainers will let the view be dealloced when not in the viewport. If the view con‐ struction is complex and takes time, implementing a custom view cache is advisable.
    What if you use UIScrollView? Definitely lazy-load. Load only the views required for scroll position 0, and then mimic UITableView behavior by building your own view cache. Use delegate’s scrollViewDidScroll: in conjunction with the contentOffset (scroll position) property to know which views are to be rendered.
    As a general practice, render elements up to the screen height beyond the view‐ port to avoid any jitter during scroll since they need to be rendered in quick succession as the scrolling starts.
    And keep this at the back of your mind: UITableView inherits from UIScroll View, which means if UITableView can do smart view caching, so can your cus‐ tom code.
  • Prefer custom drawing for complex UIs. It results in one view to be drawn instead of multiple subviews, and avoids costly layoutSubviews and drawRect: calls.
    In addition, you avoid the cost of using general-purpose, feature-rich compo‐ nents by using optimized views for optimized direct drawing.

UILabel

steps to render UILable

  1. Using the font family, font style, and text to be rendered, compute the number of pixels it requires. This is an extremely costly process and it should be done as sparingly as possible.4
  2. Check against the width of the available frame to render.
  3. Check against the numberOfLines to compute the number of lines to show.
  4. Was sizeToFit called? If so, compute the height.
  5. If sizeToFit was not called, check if content can be shown for the given height of the frame.
  6. If the frame is not sufficient, use lineBreakMode to determine the wrap or truncation location.
  7. Take care of other configuration options (e.g., if it is plain text or attributed, shadows, alignment, autoshrink, etc.).
  8. Finally, use the font, style, and color to render the nal text to be shown.

If the width is a fraction, everything will work great except that the rendering requires anti-aliasing, which is an expensive operation.

UIButton

There are four ways to render a button:

  • Default rendering with custom text
  • Button with full-sized assets
  • Resizable assets
  • Using CALayer and Bézier paths for custom drawing

presents the pros and cons of working with each option for rendering a button.

“Designing for iOS: Taming UIButton”.

UIImageView

best practices to maximize performance when working with UIImage and UIImageView:

  • For known images, use the imageNamed: method to load the images. It ensures that the content is loaded in memory only once and repurposed across multiple UIImage objects.
  • Use an asset catalog for loading bundled images with the imageNamed: method. This is especially useful if the app has a bunch of icons, each one of which may be small in size. Feel free to create multiple catalogs of related images (i.e., images that are generally used together).
    When iOS loads from the disk, there is an optimal buffer size that can be used to load multiple images in a single read. Also, opening multiple I/O streams has overheads as compared to opening one stream and reading multiple images from there. It is generally faster to read one combo file that is 32 KB in size than 16 files that are each 2 KB in size.
    However, if you want to load a large image that will be used only once, or at best very sparingly, consider using imageWithContentsOfFile: instead of using an asset catalog and the imageNamed: method, because the asset catalog caches the images (which is not needed in this case).
    In an app that I worked on, the team saw a reduction of about 300 ms in initial load time by choosing to bundle the images for the initial two screens in one asset catalog.
  • For other images, use a high-performing image cache library. AFNetworking and SDWebImage are great libraries to use.
    When working with an in-memory image cache, be sure to configure memory usage parameters correctly. Do not hardcode. Make it adaptive—a percentage of available RAM is a good way to configure it.
  • Load the image of the same size as the UIImageView to be rendered. You get the maximum performance when the dimensions of the image parsed and of the UIImageView are the same—resizing images is an expensive operation, and it is even costlier if the image view is contained in a UIScrollView.
    If the image is downloaded from the network, try to download the image that matches the view size. If that option is not available, preprocess the image to resize it appropriately.
  • If there is a need to apply effects like blur or shades, create a copy of the image contents, apply the effects, and use the final bitmap to create the final UIImage. That way, the effects are only applied once and the original image can be used for other displays if needed.
  • Whatever technique you use to load the images, execute it off the main thread, preferably in a dedicated queue.
    Specifically, decompress JPG/PNG images off the main thread.
  • Last but not the least, determine whether you really need the images. If you were to show a rating bar, you might be better off with a custom view with direct drawing than using multiple images with transparency and overlays.

UITableView

best practices to keep in mind when working with UITableView:

  • In the dataSource method tableView:cellForRowAtIndexPath:, use table View:dequeueReusableCellWithIdentifier: or tableView:dequeueReusable CellWithIdentifier:forIndexPath: to reuse cells instead of creating new cells each time.
    Cell creation has a performance cost. The cost grows multifold if several cells have to be created in a short span of time—for example, when the user scrolls the table view. Also, as cells go out of scope, they are dealloced, resulting in a double whammy. Reusing cells means the only overhead will be to render the cells.

  • Avoid dynamic-height cells as much as possible. Fixed, predetermined heights means less computation. With dynamically configured content, not only must the height be computed when required, but the cell contents have to flow and lay‐ out must be performed each time the view is rendered. That can be a big perfor‐ mance hit.

  • If you really need dynamic-height cells, define a rule to mark the cells dirty. If a cell is dirty, compute the height and cache it. Continue to return the cached height in the tableView:heightForRowAtIndexPath: callback of the delegate until the cell is not dirty.
    If the model to render is immutable, a simple rule that can be used is to check if the model currently being rendered is the same as the one at the corresponding indexPath. If so, the same values will be rendered and hence it requires no fur‐ ther processing. If not, recompute the values and attach the new object (model) to the cell.
  • When reusing the cells using custom views, avoid laying them out each time by calling layoutIfNeeded it is requested.
    Even if a cell is fixed-height, it is possible that the individual elements within the cell may still be configurable to be of varying height—for example, UILabel sup‐ ports multiline content and UIImageView can work with varying-sized images.
    Avoid that. Fix the size of each element. This ensures minimal time needed to render the cells.
  • Avoid nonopaque cell subviews. Whenever you create a UITableViewCell, try to only have opaque elements in it. Translucent or transparent elements (views with alpha less than 1.0) may look great but have a performance hit.
    For aesthetic reasons, you may still want to have alpha set to a value less than 1.0. If that is case, be aware of the costs.
  • Consider using shell interfaces when fast-scrolling (see Figure 6-7). When the user fast-scrolls the table view, it is possible that even with all your optimizations, the view reuse and rendering will still take well over 16 ms and there will be occa‐ sional frame drops that may result in a jittery experience.
    A good option in these situations is to use a shell interface, which can be defined as a predefined interface whose only purpose is to indicate to the end user that there is some data to be shown. As the scrolling velocity drops down below a threshold, flush in the final view and populate it with the data.
    You can get the velocity using the panGestureRecognizer property associated with the table view
1
2
3
4
-(void)scrollViewDidScroll:(UIScrollView *)scrollView { 
CGPoint velocity = [tableView.panGestureRecognizer velocityInView:self.view]; self.velocity = velocity; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if(fabs(self.velocity.y) > 2000) { //return shell cell }else{ //return real cell }
}
  • Avoid gradients, image scaling, and any offscreen drawing. These effects are a drain on the CPU as well as the graphics processing unit (GPU).

UIWebView

best practices to keep in mind while working with UIWebView

  • UIWebView can be bulky and sluggish. Reuse web view objects as much as possi‐ ble. UIWebView is also known to leak memory. So, one instance per app should be good enough.
    Whenever you want to present a new URL to the user, reset the content to empty HTML. This ensures that the web view does not show previous content to the end user. Use loadHTMLString:baseURL: followed by loadRequest: to accomplish this.
  • Attach a custom UIWebViewDelegate. Implement the webView:shouldStartLoad WithRequest:navigationType: method. Watch out for the URL scheme. If it is anything other than http or https, beware: your app knows how to handle it or warn the user that the site is trying to let her out of the app.
    This is a great option not only to ensure that the user does not abruptly land in another app, but also for safeguarding against malicious content, especially if you happen to show content from an unknown URL—say, in a mail or messaging app.
  • You can create an app-to-JavaScript bridge by using the stringByEvaluatingJavaScriptFromString: method to execute JavaScript in the currently loaded web page. To call a method into a native app, use custom URL schemes and refer to the preceding bullet point on how to handle them.
  • Implement the method webView:didFailLoadWithError:of the delegate to keep a close track of any and all errors that may occur.
  • Implement the webView:didFailLoadWithError: method to handle specific errors. The NSError object has meaningful insights if the domain equals NSURLErrorDomain.
1
2
3
4
5
6
7
-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
if([NSURLErrorDomain isEqualToString:error.domain]) {
switch(error.code) { case NSURLErrorBadURL:
//handle bad URL break; case NSURLErrorTimedOut: //handle timeout break;
//... etc. }
}
}
  • UIWebView will not notify of any HTTP protocol errors like a 404 or 500 response. You will have to make a double call, first using a custom NSURLConnection call and then by the web view. Provide a dele‐ gate to the NSURLConnection and implement connection:didReceiveResponse: to get response details.
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
@interface HPWebViewController() <UIWebViewDelegate, NSURLConnectionDataDelegate>
@property (nonatomic, assign) BOOL shouldValidate;
@end

@implementation HPWebViewController
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType) navigationType {
if(self.shouldValidate) {
[NSURLConnection connectionWithRequest:request delegate:self];
return NO;
}
return YES;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSInteger status = [(NSHTTPURLResponse *)response statusCode];
if(status >= 400) {
//Bingo! An error.
//Show alert or hide web view - don't show bad page.
}else{
self.shouldValidate = NO;
[self.webView loadRequest:connection.originalRequest];
}
[connection cancel];
}
@end
  • The container where you embed the UIWebView should provide the following:

    1. Navigation buttons (back and forward)
    2. Reload button
    3. Cancel button to cancel the page currently being loaded
    4. UILabel to show the title of the page
    5. Close button to move out from the web view, unless that is the only UI that you have in your app—for example, in a hybrid app

Custom Views

When Should I Set layer.shouldRasterize to YES?

As you may notice, there is a staggering performance difference of a factor of 2x–20x. The initial load is blazingly 50x faster when using direct draw.

And that is when the code for direct draw is not even optimized.
Thus, from a performance perspective, direct drawing should offer better performance at times—orders of magnitude better than composite views.

However, from a maintenance perspective, the code can be difficult to maintain and evolve. Once you have stabilized the app, there will be a definitive case to move away from a composite UI to direct drawing.

Auto Layout

In general, directly setting the frames is around 1,000x faster than using Auto Layout.
Auto Layout Performance on iOS // source code for the test

Size Classes

Size classes require Auto Layout. If you choose not to use Auto Layout for performance reasons, you will not be able to use size classes.