Making MVC Great Again!

Use generics, protocols, and extensions to get rid of massive view controllers

A copy of this article is also available here

Model-View-Controller is a very common software structure for creating applications in the Apple ecosystem.

Although MVC is a straightforward concept, developers most often miss use it and turn it into MVC; standing for “Massive View Controller” structure.

In this post, we will see how we can make MVC great again by using some simple techniques like generics, protocols, extensions, convenience initializers, and more, so let’s start!

Our app is a very simple app with one screen: login screen where users will be able to enter their email and password and the app will print them in the console.

First, we will start by getting rid of the source of all evil, Storyboards 😈

What is wrong with Storyboards?

While Apple promotes Storyboard as the standard way to develop user interfaces for its ecosystem, Storyboards have major problems like:

  • Slow performance and compile time.
  • They are not git friendly, they make working in a team harder due to their XML nature.
  • Storyboards fail at runtime, not at compile time which makes them a huge source of unknown bugs and problems.
  • And more …

Personally, I find myself facing scalability issues every time I use storyboards or xib files in a project.
however, when I tried to write my UI code programmatically, view controllers got even bigger and harder to maintain which defeated the entire purpose of fighting Massive View Controllers 😭

The solution

1. Subclassing UIView and UIViewController
2. Move all UI and layout code away from view controllers.
3. Use extensions to organize view controllers
4. Use convenience initializers to initialize and set common UI elements in one line.

Subclassing UIView

We’ll create a new base class called View that we will subclass and use from now on, instead of UIView

class View: UIView {
    override init(frame: CGRect) {
super.init(frame: frame)
setViews()
layoutViews()
}
    required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setViews()
layoutViews()
}
    /// Set your view and its subviews here.
func setViews() {
backgroundColor = .white
}
    /// Layout your subviews here.
func layoutViews() {}
}

Subclassing UIViewController:

As we did with View we’ll create another new base class called ViewController that we will subclass and use from now on, instead of UIViewController

class ViewController<V: View>: UIViewController {

override func loadView() {
view = V()
}
    var customView: V {
return view as! V
}
}

All together in the login example

LoginView.swift

protocol LoginViewDelegate: class {
func loginView(_ view: LoginView, didTapLoginButton button: UIButton)
}

class LoginView: View {

weak var delegate: LoginViewDelegate?
var emailAddress: String {
let text = emailTextField.text

        return text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}

var password: String {
return passwordTextField.text ?? ""
}

private lazy var emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email Address"
textField.keyboardType = .emailAddress
return textField
}()

private lazy var passwordTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Password"
textField.isSecureTextEntry = true
return textField
}()

private lazy var loginButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Login", for: .normal)
return button
}()
    override func setViews() {
super.setViews()
        addSubview(emailTextField)
addSubview(passwordTextField)
addSubview(loginButton)
loginButton.addTarget(self, action: #selector(didTapLoginButton(_:)), for: .touchUpInside)
}
    override func layoutViews() {
// layout your subviews here, consider using SnapKit,
it is amazing!
}

}

// MARK: - Actions
private extension LoginView {
    @objc func didTapLoginButton(_ button: UIButton) {
delegate?.loginView(self, didTapLoginButton: button)
}

}

LoginViewController.swift

class LoginViewController: ViewController<LoginView> {
    override func viewDidLoad() {
super.viewDidLoad()
customView.delegate = self
}

}

// MARK: - LoginViewDelegate
extension LoginViewController: LoginViewDelegate {
    func loginView(_ view: LoginView, didTapLoginButton button: UIButton) {
print("Email Address: \(customView.emailAddress)")
print("Password: \(customView.password)")
}

}

Bonus

Use convenience initializers to stop writing the same code again and again:

extension UITextField {
    convenience init(
placeholder: String,
keyboardType: UIKeyboardType = .default,
isSecureTextEntry: Bool = false) {

self.init()

self.placeholder = placeholder
self.keyboardType = keyboardType
self.isSecureTextEntry = isSecureTextEntry
}

}

extension UIButton {
    convenience init(
type: UIButtonType = .system,
title: String?,
image: UIImage?) {

self.init(type: type)
        self.setTitle(title, for: .normal)
self.setImage(image, for: .normal)
}

}

The new LoginView

protocol LoginViewDelegate: class {
func loginView(_ view: LoginView, didTapLoginButton button: UIButton)
}
class LoginView: View {
    weak var delegate: LoginViewDelegate?
    var emailAddress: String {
return emailTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}
    var password: String {
return passwordTextField.text ?? ""
}
    private lazy var emailTextField: UITextField = {
return UITextField(placeholder: "Email Address", keyboardType: .emailAddress)
}()
    private lazy var passwordTextField: UITextField = {
return UITextField(placeholder: "Password", isSecureTextEntry: true)
}()
    private lazy var loginButton: UIButton = {
return UIButton(title: "Login", image: nil)
}()
    override func setViews() {
super.setViews()
addSubview(emailTextField)
addSubview(passwordTextField)
addSubview(loginButton)
loginButton.addTarget(self, action: #selector(didTapLoginButton(_:)), for: .touchUpInside)
}
    override func layoutViews() { ... }

}

Conclusion

1. Keep your layout code AWAY from the view controller
2. Use private to keep your UI code inaccessible from view controllers unless there is a very good reason not to do so.
3. Create base UIView and UIViewController subclasses and use generics and protocols, to keep that damn viewDidLoad method clean!
4. Use convenience initializers to initialize UI elements and set their properties in one line.

Where to go from here?

  • Continue to part 2 where we will build on top of this to handle keyboard events.
  • See the example Xcode project on Github