Image for post
Image for post

Making MVC Great Again!

Omar Albeik
Apr 7, 2018 · 4 min read

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?

  • 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

Subclassing 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:

class ViewController<V: View>: UIViewController {

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

All together in the login example

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

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

Where to go from here?

  • See the example Xcode project on Github

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

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