When building iOS interfaces, it is common to encapsulate a block of UI elements into a reusable UIView subclass. This article covers the fundamentals of building custom views using both pure code and XIB files, along with the important differences in initialization that arise from each approach.
Dseigning a Custom View in Code
A well-designed custom view hides its internal subview layout from the outside world and exposes only the necessary data model. The typical steps are:
- Override
initWithFrame:to create and add subviews. - Expose a convenience class method for quick instantiation (optional but helpful).
- Override
layoutSubviews(always call super) to position subviews based on the view’s bounds. - Provide a model property; in its setter, update the corresponding subviews.
Code Example
Suppose we need a BookCardView that displays a book cover image and title. The header exposes only the data model:
// BookCardView.h
#import <UIKit/UIKit.h>
@class BookModel;
@interface BookCardView : UIView
@property (nonatomic, strong) BookModel *book;
@end
In the implementation, subviews are kept private in a class extension:
// BookCardView.m
#import "BookCardView.h"
#import "BookModel.h"
@interface BookCardView ()
@property (nonatomic, weak) UIImageView *coverImageView;
@property (nonatomic, weak) UILabel *titleLabel;
@end
@implementation BookCardView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupSubviews];
}
return self;
}
- (void)setupSubviews {
UIImageView *cover = [[UIImageView alloc] init];
self.coverImageView = cover;
[self addSubview:cover];
UILabel *title = [[UILabel alloc] init];
title.textAlignment = NSTextAlignmentCenter;
self.titleLabel = title;
[self addSubview:title];
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = CGRectGetWidth(self.bounds);
CGFloat height = CGRectGetHeight(self.bounds);
CGFloat coverHeight = height * 0.7;
self.coverImageView.frame = CGRectMake(0, 0, width, coverHeight);
self.titleLabel.frame = CGRectMake(0, coverHeight, width, height - coverHeight);
}
- (void)setBook:(BookModel *)book {
_book = book;
self.coverImageView.image = [UIImage imageNamed:book.coverImageName];
self.titleLabel.text = book.title;
}
@end
This pattern keeps the controller or parent view unaware of the internal arrangement of the card.
Using a XIB for Visual Layout
Instead of writing all layout code by hand, many developers use a XIB file to design the view graphically. This can speed up development and simplify complex view hierarchies.
XIB vs. Storyboard
Both XIBs and storyboards are compiled by Interface Builder and translate into actual code at build time. The main differences:
- XIB: lightweight, describes a single UI component or a small screen portion.
- Storyboard: can represent multiple scenes and their segues, often used for entire app flows.
Loading a XIB-Based View
A XIB file is stored in the main bundle. You can load it using one of two common approaches:
// Approach 1 – loadNibNamed:owner:options:
NSArray *views = [[NSBundle mainBundle] loadNibNamed:@"BookCardView"
owner:nil
options:nil];
BookCardView *card = views.firstObject;
// Approach 2 – UINib
UINib *nib = [UINib nibWithNibName:@"BookCardView" bundle:nil];
BookCardView *card = [[nib instantiateWithOwner:nil options:nil] firstObject];
To simplify creation, you can encapsulate the loading logic in a class factory method:
+ (instancetype)bookCardView {
return [[NSBundle mainBundle] loadNibNamed:@"BookCardView"
owner:nil
options:nil].firstObject;
}
Important Setup Details
- Set the custom class of the XIB’s top‑level view to your subclass (e.g.,
BookCardView). - Connect subviews (image view, label) to the corresponding
IBOutletproperties in the implementation file using Interface Builder. - Since the view is unarchived from the XIB, it does not call
initWithFrame:. Instead, it callsinitWithCoder:, and after decoding finishes the system callsawakeFromNib.
Initialization Differences at a Glance
The way a view is created directly affects which initializer is called:
- Pure code (
[[BookCardView alloc] initWithFrame:…]) →initWithFrame:. - Loaded from a XIB or storyboard →
initWithCoder:, followed byawakeFromNibwhen all outlets are connected.
Therefore, when you need to perform one‑time setup (adding suvbiews, default appearance), you should place it in initWithFrame: (for code‑based creation) or in awakeFromNib (for XIB‑based creation). A common pattern is to call a shared setup method from both places so that the view initializes consistently regardless of how it was instantiated.