Custom UIViews
A clean way to connect custom UIView classes with their corresponding xib files
A long time ago, when I was just starting with Objective-C and Xcode, a big source of frustration for me was finding a clean way to connect my custom UIView classes with their corresponding xib files. I'm not sure what tutorials are out there now since then but in any case I'll show you how I like to do it. I think it's pretty simple, however if you have a better method, please let me know.
So why do we even need this?
In just about every app, there is usually at least one custom view that needs to be reused in many view controllers. To illustrate this I'm going to show an example with a reusable loading spinner. I'll attach a link to the full source of my demo app at the bottom of this post.
The Code
I'm going to assume you already have some basic Objective-C skills and are already familiar with Xcode. If you have that, you should be able to follow along no problem.
The first step is to create a new single view project. Once you've done that create a new class in your project called LoadingView
. Below are my header and implementation files. Scan through them real quick and I'll go over the important parts below.
//
// LoadingView.h
//
// Created by Reed Dadoune on 4/23/12.
// Copyright (c) 2012 Reed Dadoune. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface LoadingView : UIView
@property (weak, nonatomic) IBOutlet UILabel *loadingLabel;
@property (readonly) BOOL loading;
- (void)centerView;
- (void)show;
- (void)showWithCompletion:(void (^)(BOOL))callback;
- (void)dismiss;
- (void)dismissWithCompletion:(void (^)(BOOL))callback;
+ (LoadingView *)loadingView;
@end
//
// LoadingView.m
//
// Created by Reed Dadoune on 4/23/12.
// Copyright (c) 2012 Reed Dadoune. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "LoadingView.h"
@interface LoadingView()
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *spinner;
@property (nonatomic) NSInteger showCount;
@end
@implementation LoadingView
#pragma mark - Properties
- (BOOL)loading
{
// If show count is greater than 0
// We must be loading
return (self.showCount > 0);
}
- (void)setShowCount:(NSInteger)showCount
{
if (showCount < 0) {
// Prevent showCount from ever
// going below zero
_showCount = 0;
} else {
_showCount = showCount;
}
}
#pragma mark - Public Methods
- (void)centerView
{
// Center in parent
self.center = CGPointMake(CGRectGetWidth (self.superview.frame)/2,
CGRectGetHeight(self.superview.frame)/2);
}
- (void)show
{
[self showWithCompletion:nil];
}
- (void)showWithCompletion:(void (^)(BOOL))callback
{
self.showCount++;
[self.spinner startAnimating];
[UIView animateWithDuration:0.5
animations:^{
self.alpha = 1.0;
}
completion:^(BOOL finished) {
if (callback) {
callback(finished);
}
}];
}
- (void)dismiss
{
[self dismissWithCompletion:nil];
}
- (void)dismissWithCompletion:(void (^)(BOOL))callback
{
self.showCount--;
if (self.loading) {
NSLog(@"Hide Skipped");
return;
}
// The number of "dismiss" method calls
// Should be equal to the number of "shows"
[UIView animateWithDuration:0.5
animations:^{
self.alpha = 0.0;
}
completion:^(BOOL finished) {
if (finished) {
[self.spinner stopAnimating];
}
if (callback) {
callback(finished);
}
}];
}
#pragma mark - Lifecycle
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// We don't want this view to obstruct
// any of the UI elements below it
self.userInteractionEnabled = NO;
// Init the style
self.alpha = 0.0;
self.layer.cornerRadius = 5;
}
return self;
}
#pragma mark - Class Methods
+ (LoadingView *)loadingView
{
// Load xib file whose name matches this class name
NSArray *xib = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:self
options:nil];
// Locate the first view of this class
for (id view in xib) {
if ([view isKindOfClass:[self class]]) {
return view;
}
}
return nil;
}
@end
Next create a simple xib file, that looks something like this:
After you finish that make sure you set the view class to LoadingView
just below where it says "Custom Class", like so:
Also remember to connect the appropriate IBOutlets
from your label to loadingLabel
and from your activity spinner to spinner
.
Explanation
Alright so most of that code is basic stuff that you should understand pretty easily. The important part, is in my class method + (LoadingView *)loadingView;
. This is convenient because now whenever you need to initialize a LoadingView
in one of your UIViewControllers
you can simply do something like this:
LoadingView loadingView = [LoadingView loadingView];
[self.view addSubview:loadingView];
[loadingView centerView];
Whenever you call this class method, initWithCoder
will also be called and you can do whatever setup your custom class needs in there. Also, if your custom class isn't something you can just fire and forget, then it's probably a good idea to lazy load it into a property of your UIViewController
. In my example, keeping the reference is a good idea so that LoadingView
can be shown over and over again whenever the controller begins some new asynchronous task.
Controller Integration
Setup in a controller requires three simple steps
- Include your class at the top
#import "LoadingView.h"
- Define a class property to hold you view
@property (strong, nonatomic) LoadingView *loadingView;
- Lazy load your view into your corresponding proper
- (LoadingView *)loadingView
{
if (!_loadingView) {
_loadingView = [LoadingView loadingView];
[self.view addSubview:_loadingView];
[_loadingView centerView];
}
return _loadingView;
}
Then whenever you need do some asynchronous task and want the user to know you're loading data simply do as follows:
- (void)asynchronousTask
{
// Begin task
[self.loadingView show];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
// Wait 3 seconds then dismiss
[self.loadingView dismiss];
});
}
One extra thing to note about my LoadingView
class is the showCount
. The showCount
is important in that it keeps track of how long LoadingView
should stay open. For example, let's say you fire asynchronousTask
multiple times before the first 3 seconds is up, we don't want it to hide LoadingView
prematurely. So to handle that, we increment showCount
every time it's called and decrement every time it's dismissed. This effectively keeps LoadingView
visible until all asynchronous tasks finish. dismiss
only actually hides the view when showCount
returns to zero.
To see a full working example download and run the following source in Xcode.