Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Extracting UIViews from UIViewControllers in Swift

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 named tableView

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.

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *