Building a Universal App for iOS
How you construct your universal application is going to vary based on the the goals and requirements of the project. Here's one way to leverage auto layout, storyboards and custom UIView subclasses in order to save time and avoid unnecessary duplication.
Updated on 02 Feb 2015
This post previously suggested that you remove constraints at the start of your updateConstraints
implementation. This is incorrect and can at a minimum break animations. The UIView documentation has been updated to clarify that what you have to do is remove any invalidated constraints as soon as you know they're no longer valid and before calling setNeedsUpdateConstraints
.
Storyboard the "Big Picture" in Main Interface Storyboard Files
When you create a new universal application project in Xcode it automatically creates two separate storyboard files for you. One for the iPhone and iPod touch and the other for the iPad. These files are configured under your project's "General" settings and the correct storyboard is automatically loaded when the app is launched on a device. While separate storyboards are a convenient way to separate your user interface for each device type you should always try to avoid putting any shared UI in these files. Any changes to the duplicated contents of these files will need to be made twice. This takes twice the time and as the complexity increases, so does the chance that your storyboards will diverge in ways you didn't intend causing bugs and inconsistency in your application. By building your device-specific UIs from large reusable "chunks" you can use safely use these storyboards to define only the differences between each interface.
Encapsulate Common User Interface Elements Into UIView subclasses
Common elements of your UI should be implemented as custom UIView subclasses. Start out by identifying the largest parts of your user interface that are shared between each device type. You can then extract these elements into reusable components. Keep in mind that these custom views can be composed of multiple subviews and controls (which can also be custom classes). You can define your custom views either in nib files, in code or any combination of the two.
Use Auto Layout to Make Custom Views Resizable
You'll probably end up with some elements that need to be sized differently for each device type. By using auto layout and avoiding fixed dimensions you can build custom views that stretch or compress to fit the available space. Consider defining objcontentCompressionResistance
and objcontentHuggingPriority
on your subviews to avoid specifying exact heights and widths or to tweak the way they resize. If your custom view needs to specify its own size within the final layout you can override intrinsicContentSize
to return the correct dimensions.
Subclass Existing Views and Controls to Encapsulate Configuration
You can create "smarter" views by subclassing existing classes. This helps keep view-specific configuration and logic out of your view controllers. For instance, you might have a UICollectionView subclass that acts as its own datasource and delegate and controls its own behavior and presentation. You can then use a separate delegate protocol that describes how this view communicates with your view controller.
Use Base Classes as Placeholders in Storyboards and Nibs
It's important to remember that defining your own custom views doesn't preclude using them in Interface Builder. By using the base class as a placeholder and specifying your subclass in the Identity inspector you can build your layouts graphically and configure base class properties right in IB. Anything custom to your subclass can be configured elsewhere such as in its own initializer. You might have a collection view that scrolls horizontally on the iPad and vertically on the iPhone. By using a stock collection view as a placeholder for your subclass you can specify its scroll direction in each storyboard while keeping common configuration in the shared subclass.
An Example Custom View Defined in Code
Here's a custom UIView subclass that manages an image view and a label. It uses auto layout to place its subviews and defines its own intrinsic content size based on the size of the image and amount of text it's displaying. While not incredibly useful in itself it demonstrates what I consider best practices for writing custom views.
// SimpleView.h
#import <UIKit/UIKit.h>
@interface SimpleView : UIView
@property (nonatomic) UIImage *image;
@property (nonatomic) NSString *text;
@end
// SimpleView.m
#import "SimpleView.h"
@interface SimpleView ()
// This view manages its own subviews so we define them in a private interface
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UILabel *label;
@end
@implementation SimpleView
+ (BOOL)requiresConstraintBasedLayout
{
return YES;
}
static void initSimpleView(SimpleView *self) {
// Configure default properties of your view and initialize any subviews
self.backgroundColor = [UIColor clearColor];
self.imageView = ({
UIImageView *imageView = [[UIImageView alloc] init];
imageView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:imageView];
imageView;
});
self.label = ({
UILabel *label = [[UILabel alloc] init];
label.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:label];
label;
});
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
initSimpleView(self);
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
initSimpleView(self);
}
return self;
}
- (void)updateConstraints
{
// Set up all your layout constraints.
// If you need to modify your constraints for any reason your can request an update by
// calling setNeedsUpdateConstraints.
NSDictionary *views = NSDictionaryOfVariableBindings(_imageView, _label);
// The 'H' for 'horizontal' is the default and not required but I find it more readable to include it
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_imageView]" options:0 metrics:nil views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_label]|" options:0 metrics:nil views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_imageView]-4-[_label]|" options:0 metrics:nil views:views]];
// You *MUST* call super last
[super updateConstraints];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
// Some controls automatically inherit their superviews tint color. You can use
// this method to add that behavior to your own interactive controls
self.label.textColor = self.superview.tintColor;
}
- (CGSize)intrinsicContentSize
{
// Give your view an intrinsic content size if its size is defined by its subviews
// or other properties.
// Be sure to call invalidateIntrinsicContentSize if anything changes (see setters below).
CGFloat width = fmaxf(self.label.intrinsicContentSize.width, self.imageView.image.size.width);
return CGSizeMake(width, self.label.intrinsicContentSize.height + 4 + self.imageView.image.size.height);
}
- (UIImage *)image
{
return self.imageView.image;
}
- (void)setImage:(UIImage *)image
{
self.imageView.image = image;
[self invalidateIntrinsicContentSize];
}
- (NSString *)text
{
return self.label.text;
}
- (void)setText:(NSString *)text
{
self.label.text = text;
[self invalidateIntrinsicContentSize];
}
@end
Wrap it Up
Break your user interface up into chunks. Implement those shared components as custom views. Lay out those "big picture" pieces in your main interface storyboards. If you're smart about the way you break up your UI you can save time and duplication while also avoiding userInterfaceIdiom
checks in your controllers.