Making MVC Great Again!

SHARE

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 {
        return emailTextField.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 init 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
  • If you have any questions or suggestions, don’t hesitate to reach out to me on Twitter.
SoftwareSwiftUIKitiOS

You made it to the end. You're Awesome!

Here is something more to read

Protocol Oriented Extensions

Use the power of protocols and generic types to avoid extension conflicts

Keyboard Events Handling using Protocols in Swift

aka. Making MVC Great Again (Part 2)

This is a fully integrated open-source project that uses NextJS, Redux, and Django to build. Grab your copy from Github

Copyright © 2019 Omar Albeik. All rights reserved.