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 😈
While Apple promotes Storyboard as the standard way to develop user interfaces for its ecosystem, Storyboards have major problems like:
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 😭
UIView
and UIViewController
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() {}
}
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
}
}
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)")
}
}
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)
}
}
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() { ... }
}
private
to keep your UI code inaccessible from view controllers unless there is a very good reason not to do so.UIView
and UIViewController
subclasses and use generics and protocols, to keep that damn viewDidLoad
method clean!Use the power of protocols and generic types to avoid extension conflicts
aka. Making MVC Great Again (Part 2)