MV(F)C: Model-View-(Fat)Controller

The Model-View-Controller (MVC) pattern is arguably the most common design pattern found in iOS. Though there are three distinct components to the it, the most abused is the Controller. Time and time again I see (I’ve been guilty of this in the past) UIViewController’s that have massive amounts of view, business and networking logic stuffed into themselves.

If you find yourself, as I did, that you are making requestss to network resources, transforming objects and manipulating views outside of animation, for example…then it is time to break that sucker up into more manageable pieces.

How I Do It

It is quite possible to optimize too early and there is never a crystal ball to see all the future features that your app will have, but you do know there will be changes. For me it is easiest to accomadate those changes if I don’t have my code so tightly coupled together.

Because I isolate logic, I’m able to see what functionality is being duplicated and thus removed the duplicty into simpler and reusable code, which for me, means more POP.


Displaying a List of Users

For simplicity sake I want to display a list of users in a UITableView. In the list I need to show the users profile image, first and last name. That type of functional request doesn’t get much easier.

Below is my class setup:

BaseTableView: UITableView

let DefaultMinimumCellHeight: CGFloat = 44.0
let DefaultSectionHeaderHeight: CGFloat = 30.0
let DefaultEstimatedCellHeight: CGFloat = 55.0

class TableView: UITableView {

// MARK: Initializers

override init(frame: CGRect, style: UITableViewStyle) {

super.init(frame: frame, style: style)

self.estimatedRowHeight = DefaultEstimatedCellHeight
self.sectionHeaderHeight = DefaultSectionHeaderHeight
self.cellLayoutMarginsFollowReadableWidth = false

self.translatesAutoresizingMaskIntoConstraints = false
self.tableFooterView = UIView(frame: CGRectZero)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

convenience init() {
self.init(frame: CGRectZero, style: .Plain)
}
}

BaseTableViewCell: UITableViewCell

class TableViewCell: UITableViewCell, ReuseableView {

// MARK: Initializers

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {

super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .Gray
self.backgroundColor = .black
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: Overrides

override func awakeFromNib() {
super.awakeFromNib()
}

override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}

UserTableViewCell

private let MarginOffset: CGFloat = 10.0

class UserTableViewCell: TableViewCell {

// MARK: Public (properties)

var user: UserProfile? {

didSet {

if let newUser: UserProfile = user {
self.textLabel?.text = newUser.displayName
}
}
}

lazy var selectionImageView: UIImageView = {

let imageView: UIImageView = UIImageView(frame: CGRectZero)

imageView.image = self.userUnselectedImage
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.tintColor = .grey

return imageView
}()

// MARK: Private (properties)

private var didSetupConstraints: Bool = false

// MARK: Initializers

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {

super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: Overrides

override func updateConstraints() {

if !self.didSetupConstraints {

self.addConstraintsForSubviews()
self.didSetupConstraints = true
}

super.updateConstraints()
}

// MARK: Private (methods)

private func setup() -> Void {

self.textLabel?.font = self.displayLabelFont
self.contentView.addSubview(self.selectionImageView)
self.setNeedsUpdateConstraints()
}

private func addConstraintsForSubviews() -> Void {

self.selectionImageView.rightAnchor.constraintEqualToAnchor(self.contentView.rightAnchor, constant: -MarginOffset).active = true
self.selectionImageView.centerYAnchor.constraintEqualToAnchor(self.contentView.centerYAnchor).active = true
self.selectionImageView.widthAnchor.constraintEqualToConstant(self.selectionImageSize.width).active = true
self.selectionImageView.heightAnchor.constraintEqualToConstant(self.selectionImageSize.height).active = true
}
}

UsersTableView

class UsersTableView: TableView {

// MARK: Initializers

override init(frame: CGRect, style: UITableViewStyle) {

super.init(frame: frame, style: style)
self.registerClass(UserTableViewCell.self, forCellReuseIdentifier: UserTableViewCell.reuseIdentifier)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

convenience init() {
self.init(frame: CGRectZero, style: .Plain)
}
}

UsersController

class UserController {

func fetch(callback: FetchUsersCallback) -> Void {

let networkController: NetworkController = NetworkController("/path/to/endpoint")
networkController.fetchAll({result in

switch result {

case .Success(let returnedProfiles as [NSDictionary]):

var userProfiles: [UserProfile] = []

returnedProfiles.forEach{

if let profile: UserProfile = try? UserProfile(json: $0) {
userProfiles.append(profile)
}
}

callback(result: .Success(userProfiles))
break
case let .Failure(.ItemNotFound(message)):
callback(result: .Failure(.UsersNotFound(message: message)))
break
default: break
}
})
}
}

UsersViewController

private let DefaultOffset: CGFloat = 10.0

class UsersViewController: ViewController, StandardViewController {

typealias Type = UsersViewController

// MARK: Private (properties)

private var viewModel: UsersViewModel!

lazy private var usersTableView: UsersTableView = {
return UsersTableView()
}()

// MARK: Initializers

required init() {

func setup() -> Void {
self.viewModel = UsersViewModel(tableView: self.usersTableView)
}

super.init()
setup()
}

required init!(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: View Life Cycle

override func loadView() {
super.loadView()
}

override func viewDidLoad() {

super.viewDidLoad()
self.setupViews()
}

override func viewWillAppear(animated: Bool) {

super.viewWillAppear(animated)
self.viewModel.fetch()
}

// MARK: Private (methods)

private func setupViews() -> Void {

let subViews: Array<UIView> = [self.usersTableView]
subViews.forEach{ self.view.addSubview($0) }

let margins = self.view.layoutMarginsGuide

/// Users Table

self.usersTableView.leftAnchor.constraintEqualToAnchor(self.view.leftAnchor).active = true
self.usersTableView.rightAnchor.constraintEqualToAnchor(self.view.rightAnchor).active = true
self.usersTableView.topAnchor.constraintEqualToAnchor(margins.topAnchor, constant: DefaultMarginOffset).active = true
self.usersTableView.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor).active = true
}
}

UsersViewModel

final class UsersViewModel {

// MARK: Private (properties)

private let datasource: UsersDatasource = UsersDatasource()

// MARK: Initializers

init() {}

convenience init(tableView: UITableView) {

self.init()
self.datasource.tableView = tableView
}

// MARK: Public (methods)

func fetch() -> Void {

let userController: UserController = UserController()

userController.fetch({result in

switch result {

case .Success(let users):
self.datasource.users = users
break
default: break
}
})
}

User

struct User {

// MARK: Public (properties)

let uid: String

let displayName: String

let imgRef: String

var role: String?

var follower: Array<UserProfile> = []

var following: Array<UserProfile> = []

var teams: Array<Tag> = []

var conversationRefs: Array<String> = []

var identifier: String

var notifications: Array<NotificationFeedItem> = []
}

UsersDatasource

final class UsersDatasource: NSObject {

// MARK: Public (properties)

var users: Array<User> = [User]() {

didSet {
self.tableView?.reloadData()
}
}

weak var tableView: UITableView? {

didSet {

tableView?.delegate = self
tableView?.dataSource = self
}
}

// MARK: Initializers

override init() {
super.init()
}
}

extension UsersDatasource: UITableViewDelegate, UITableViewDataSource {

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.users.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let cell: UserTableViewCell = tableView.dequeueReusableCellWithIdentifier(UserTableViewCell.reuseIdentifier, forIndexPath: indexPath) as! UserTableViewCell

let user: UserProfile = self.users[indexPath.row]
cell.user = user

return cell
}
}