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

How I Do It

Displaying a List of Users

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)
}
}
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)
}
}
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
}
}
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)
}
}
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
}
})
}
}
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
}
}
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
}
})
}
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> = []
}
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
}
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store