On several recent iOS projects at Pivotal Labs, we’ve extracted the view
property of a UIViewController
and made it a subclass of UIView
. The main advantage of this pattern is that it removes from the view controller all of the layout code that would normally clutter it up. We’d like to implement this pattern in Swift as well.
In Objective-C
Here’s how this view-extraction pattern might look in Objective-C (we’re laying out tableView
here by setting its frame; of course, you can use autolayout if you prefer):
// MyView
@interface MyView: UIView
@property (nonatomic, strong) UITableView *tableView;
- (void)configure;
@end
@implementation MyView
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] init];
[self addSubview:_tableView];
}
return _tableView;
}
- (void)configure {
self.tableView.frame = self.bounds;
}
@end
// MyViewController
@interface MyViewController: UIViewController <UITableViewDataSource>
@property (nonatomic, retain) MyView *view;
@end
@implementation
- (void)loadView {
self.view = [MyView alloc] initWithFrame:[[UIScreen mainScreen] bounds];
[self.view configure];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.tableView.dataSource = self;
}
// table view data source implementation, etc.
@end
Notice that in MyViewController
’s interface, we’re re-declaring view
to be an instance of MyView
. Swift doesn’t have the concept of a separate interface and implementation, so we’ll have to figure out some other way of overriding view
to be of type MyView
rather than UIView
.
Translating to Swift, First Attempt: Overriding view
The most direct translation of this view-extraction pattern from Objective-C into Swift might look like this:
// MyView
class MyView: UIView {
@lazy var tableView: UITableView = {
let tableView: UITableView = UITableView()
self.addSubview(tableView)
return tableView
}
func configure() {
tableView.frame = self.bounds
}
}
// MyViewController
class MyViewController: UIViewController, UITableViewDataSource {
override var view: MyView! // <-- compilation error
override func loadView() {
view = MyView(frame: UIScreen.mainScreen().bounds)
view.configure()
}
override func viewDidLoad() {
super.viewDidLoad()
view.tableView.dataSource = self
}
// table view data source implementation, etc.
}
This fails to compile, sadly; our attempt to cast view
to a MyView
fails with the following error:
Cannot override mutable property ‘view’ of type ‘UIView!’ with covariant type ‘MyView!’
So we can’t directly override view
. In our second attempt, we’ll try to sneak our cast by the compiler.
Translating to Swift, Second Attempt: Casting view
This time, instead of overriding view
and declaring it to be of type MyView
, let’s try casting it when it gets instantiated. We don’t get a compile error in loadView()
, which is encouraging:
class MyViewController: UIViewController, UITableViewDataSource {
override func loadView() {
view = MyView(frame: UIScreen.mainScreen().bounds) as MyView
}
override func viewDidLoad() {
super.viewDidLoad()
view.tableView.dataSource = self // <-- compilation error
}
// table view data source implementation, etc.
}
Unfortunately, this fails to compile at the line where we try to access the view’s tableView
property. The error reads:
UIView
does not have a member namedtableView
Well, that’s true; indeed UIView
does not have a member named tableView
. But so what? Isn’t self.view
an instance of MyView
? Doesn’t MyView
have a member named tableView
? Yes to the second, but no to the first. The Swift book says:
“You must always state both the name and the type of the property you are overriding, to enable the compiler to check that your override matches a superclass property with the same name and type.”
In other words, despite attempting to cast self.view
as an instance of MyView
, it is still stubbornly a plain old UIView
. We’re unable to directly cast MyViewController
’s view
property to MyView
.
Solution: Computed Properties
Instead, we’ll use a computed property to substitute self.view
with an instance of MyView
:
var myView: MyView! { return self.view as MyView }
Now our view controller code looks like this:
class MYViewController: UIViewController, UITableViewDataSource {
var myView: MyView! { return self.view as MyView }
override func loadView() {
view = MyView(frame: UIScreen.mainScreen().bounds)
myView.configure()
}
override func viewDidLoad() {
super.viewDidLoad()
myView.tableView.dataSource = self;
}
// table view data source implementation, etc.
}
This compiles and works perfectly. It has the slight downside that we have to refer to MyViewController
’s view as myView
rather than as view
, but that’s a pretty small concession.
Improving MyView
While we’re at it, we can take advantage of Swift’s var
and let
keywords to improve MyView
:
class MyView: UIView {
let tableView: UITableView!
init(frame: CGRect) {
super.init(frame: frame)
tableView = UITableView()
}
func configure() {
tableView.frame = self.bounds
self.addSubview(tableView)
}
}
By using let
instead of var
, we improve the efficiency of MyView
. Semantically, making tableView
a constant makes sense, since once it is initialized, it won’t ever be reset. Constants can’t be lazy-loaded, which is why we omit the @lazy
modifier.
In fact, we don’t even need to override init()
, since we can initialize tableView
in the same line in which we declare it:
class MyView: UIView {
let tableView = UITableView()
func configure() {
tableView.frame = self.bounds
self.addSubview(tableView)
}
}
This is so obviously better it hardly requires comment.
Organizing UIViewController
’s code
As an aside, we usually use #pragma mark -
to organize our code, especially for areas like protocol implementations. Swift doesn’t have compiler directives like #pragma
, but its easy extensions offer a clean alternative:
class MyViewController: UIViewController {
var myView: MyView! { return self.view as MyView }
override func loadView() {
view = MyView(frame: UIScreen.mainScreen().bounds)
myView.configure()
}
override func viewDidLoad() {
super.viewDidLoad()
myView.tableView.dataSource = self;
}
}
extension MyViewController: UITableViewDataSource {
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
// return number of cells
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
// return a cell
}
}
Much cleaner! In fact, when seen like this, it doesn’t seem like a big leap to extract the table view’s data source into its own class entirely. Between extracting the view
property and using extensions to organize the code, we can get a much smaller and more manageable UIViewController
, which is almost always a big win.
About the Author