Making MVC Great Again!
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 problems in this gist …
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
andUIViewController
- Move all UI and layout code away from view controllers.
- Use extensions to organize view controllers
- 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
swift
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
swift
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
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
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:
swift
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
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 = {
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
- Keep your layout code AWAY from the view controller
- Use
private
to keep your UI code inaccessible from view controllers unless there is a very good reason not to do so. - Create base
UIView
andUIViewController
subclasses and use generics and protocols, to keep that damnviewDidLoad
method clean! - Use convenience initializers to initialize UI elements and set their properties in one line.