Struggling with iOS Design Patterns? Embrace Modlizer

in #ios7 years ago (edited)

Do you have headaches everytime you open your 100+ files Xcode project? Are you hopeless, empty of life and you’re close to throwing your Mac out the window?

If that’s so, you really gotta relax! This article will come and rescue you ✊

1. First things first

Being an iOS developer for a few solid years now, I faced a plethora of issues with regard to my Xcode’s project architecture. The solution I found is called Modlizer, which is a simple yet effective way to structure source code when an app is created from scratch.

Before digging into it, I’d like to point out there are several iOS design patterns out there and I wish to thank Bohdan Orlov for his fantastic contribution.

… but why don’t I put them into service, right?

They didn’t fulfill my needs and I find them inefficient. Especially the classic type of MVC where you put a bulk of code in one file! Although I acknowledge it is the one recommended by Apple, many, including me, don’t use it and prefer to call it Massive View Controller.

VIPER also caught my attention, however I started using the architecture pattern described here instead and it worked like a charm.

2. Concept

Modlizer takes its name from the logic behind the scenes: make the project modular by separating it into as many independent slices as possible.

Ummm… slices?

That’s correct, folks! Slices 🍊 Does the header image make sense now?

The architecture focuses on the view controller and uses broadly the Objective-C categories and the more recent Swift extensions. They are the ones which give the modularity and classify the logic components in a coherent manner.

Long story short, a slice is simply a particular category or extension which acts as a component of the bigger picture.

3. Anatomy

Done with the talking, let’s get our hands dirty!

3.1 Naming

First things first, naming is important in this business. This is how it is constructed:

Slice’s Name = Prefix + View-Controller + Slice-Description
  • Prefix = the two most representative letters of your project’s name
  • View-Controller = the plain, simple name of your view controller
  • Slice-Description = the actual component (further explanation below)

As an example: APLoginButtonController

3.2 Essentials

There are many things which could be called slices inside a view controller, so here is the list of the essential ones:

  • ViewController = the actual class, obviously a mandatory file
  • Data = data handler, from declaration to interaction and beyond
  • Network = the communication with the server (usually, but not exclusively)

Basically, what I observed during my freelancing career is that a concise, smart distinction between these three slices means higher levels of productivity.

Distinction means productivity!

3.3 Optionals

Apart from the main ones, there are dozens of other slices, because this really depends on the project and your English preferences. Here is a partial enumeration:

  • NavigationBarController = a customizer for the navigation bar; I always keep this file in the top part of the folder, owing to the fact that it’s a cool thought analogy with the graphical representation (the navigation bar is in the 🔝 of the screen)
  • ButtonController = IBActions (quite often)
  • ScrollViewController = the scroll view’s delegate
  • TableViewController = the table view’s data source and delegate (not to be confused with UITableViewController)
  • TextFieldController = the text field’s delegate

The aforementioned are highly common, but they are not compulsory.

It didn’t make sense to include all of my slices I’ve ever coded, as technically you can build one on any component you’d like to. You’re free to create something like an APLoginAudioController if you implement audio recognition in your project.

3.4 Flow

This image aims to demonstrate the simplicity of the system. The slices have the ability to talk to each other (green nodes), as they are all categories/ extensions of the same view controller (the blue, centered node).

There’s the gray-coloured Model, but this is an exception we’ll discuss later on.

As a side note, I think Modlizer could be easily called the Controller-Data-Network design pattern, even though I find the name horrible and not sexy at all 😭

Observation: it is extremely important to understand the difference between slice and controller. A slice is the actual file, the whole representation of that component, while the controller stands as a standard, personally preferred ending for an optional slice.

4. Source Code

Let’s study Modlizer by looking on some actual code. This article embodies only the Swift version of the design pattern.

All the code was extracted from a project I worked on, called AdPacer, which is an outstanding app if you’re a marketing person. We’ll look only on the login page’s source files.

4.1 Hierarchy

First off, the hierarcy, inside the folder (or group, the way Xcode calls it):

It may not be visible in this minimalist example, but the order is the following:

  1. NavigationBarController
  2. ViewController (base class)
  3. Alphabetically arranged optional slices
  4. Data
  5. Network
This is the way I chose to order them, but it isn’t by any means a must. It just seemed more practical to have Data and Network at the bottom, so they are easy to access.

4.2 APLoginViewController

Naturally, this is the place where we subclass UIViewController and override the default methods.

import UIKit

class APLoginViewController: UIViewController {

   /**
    *  The views which let the user type the email and the password.
    */
   @IBOutlet weak var emailTextField: UITextField!
   @IBOutlet weak var passwordTextField: UITextField!

   
   /**
    *  Override 'viewDidLoad'.
    */
   override func viewDidLoad() {
       super.viewDidLoad()
       
       /**
        *  @located in APLoginTextFieldController.swift
        */
       appendTextField()
   }

   /**
    *  Override 'viewWillAppear'.
    */
   override func viewWillAppear(_ animated: Bool) {
       super.viewWillAppear(animated)
   }
   
   
   /**
    *  Override 'didReceiveMemoryWarning'.
    */
   override func didReceiveMemoryWarning() {
       super.didReceiveMemoryWarning()
   }
}

Notes:

  • This is the core view controller of the login page. I won’t go further into explaining IBOutlets and other concepts.
  • If you’re wondering where is the NavigationBarController, well, there’s simply no customization (color, text) done to the navigation bar here.
  • As you might’ve guessed, I’m all about commenting my code. Placing @location comments in the well-known viewDidLoad method enhances the cohesion of the project, so that we’re able to clearly identify in which slice the initial setup of the view controller occurs.
  • Notice the appendTextField! It indicates that we execute some initialization code inside the APLoginTextFieldController.
Bottom Line: the aim is to use the APLoginViewController for subclassing the mother UIViewController and use it only for that. No other delegates or custom methods here, only the core of the core.

4.3 APLoginButtonController

The key factor here is the extension, because it enables us to access the code (apart from the fileprivate properties or methods) from any other slice. Do you start to see the beauty of Modlizer? 🙌

import UIKit

extension APLoginViewController {
   
   /**
    *  Called when the 'Sign In' (middle) button was tapped.
    */
   @IBAction func didTapSignInButton(_ sender: Any) {
       view.endEditing(true)
       
       /**
        *  We proceed to sign up the user only if all the requirements are met.
        */
       if !isDataValid() { /* @located in APLoginData.swift */
           return
       }
       login() /* @located in APLoginNetwork.swift */
   }
   
   /**
    *  Called when the small 'Forgot Password' (to the right of password field) button was tapped.
    */
   func didTapForgotPasswordButton(_ sender: Any) {
       let storyboard = UIStoryboard(name: APConstant.kAPForgotPasswordStoryboard, bundle: nil)
       let forgotPasswordViewController = storyboard.instantiateInitialViewController()!
       navigationController!.show(forgotPasswordViewController, sender: self)
   }
   
   /**
    *  Called when the 'Forgot Password?' (bottom of the page) button was tapped.
    */
   @IBAction func didTapForgotPasswordLabel(_ sender: Any) {
       didTapForgotPasswordButton(sender)
   }
}

Notes:

  • Observe the @location comments, which explictly describe the source of those methods.
  • didTapForgotPasswordButton is defined here and it’s not an IBOutlet. Later, you’ll see that this it’s assigned as a tap gesture recognizer’s selector, but it belongs in this slice due to the fact that, at the end of the day, when you tap something, that’s a button!
Bottom Line: Anything related to buttons should be kept here. Although there isn’t any snippet of code which initializes stuff, it doesn’t mean one can’t do it. For instance, there could be an appendButton method similar to appendTextField.

4.4 APLoginTextFieldController

Highly similar (in terms of slice structure) with the previous one.

import UIKit

extension APLoginViewController: UITextFieldDelegate {
   
   /**
    *  Append the fully configured text fields to the view controller.
    */
   func appendTextField() {
       let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 25, height: 25))
       imageView.image = UIImage(named: "ic_question_mark")
       imageView.isUserInteractionEnabled = true
       imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(APLoginViewController.didTapForgotPasswordButton)))
       passwordTextField.rightViewMode = .always
       passwordTextField.rightView = imageView
   }
   
   /**
    *  The text field delegate.
    */
   func textFieldShouldReturn(_ textField: UITextField) -> Bool {
       if textField == emailTextField {
           passwordTextField.becomeFirstResponder()
       }
       
       if textField == passwordTextField {
           didTapSignInButton(textField)
       }
       return true
   }
   
   /**
    *  Called when the background was tapped.
    */
   @IBAction func didTapBackground(_ sender: Any) {
       view.endEditing(true)
   }
}

Notes:

  • Observe the appendTextField called from the class slice.
  • Look at the #selector(APLoginViewController.didTapForgotPasswordButton), which bridges to the APLoginButtonController.
  • didTapBackground was implemented here because the background is not actually a button and the action is solely connected with the behaviour of the text fields.
Bottom Line: Anything related to text fields should be kept here.

4.5 APLoginData

Being a fundamenal slice, it can have two parts:

  1. Class
  2. Extension

Both are optional and it really depends on the project if you implement one or another.

The quick explanation is that a class defined here will act as a model for the data in the view controller (eg: an array of image URLs). The view controller gains access by storing a property of type APLoginData.Regarding the extension, it acts just as the previous ones.

import UIKit

class APLoginData {
   var dummy: String?
}

extension APLoginViewController {
   /**
    *  Checks to see if the entered profile data is valid.
    */
   func isDataValid() -> Bool {
       /**
        *  Email requirements.
        */
       if !emailTextField.text!.isEmail() {
           APAlert.top(NSLocalizedString("Invalid email address", comment: ""))
           return false
       }
       
       /**
        *  Password requirements.
        */
       // We do not allow less than 8 characters and more than 100.
       if passwordTextField.text!.characters.count < 8 || passwordTextField.text!.characters.count > 100 {
           APAlert.top(NSLocalizedString("Password must have between 8 and 100 letters", comment: ""))
           return false
       }
       
       // We do not allow spaces at the beginning and at the end of the string.
       if passwordTextField.text![0] == " " || passwordTextField.text![passwordTextField.text!.characters.count-1] == " " {
           APAlert.top(NSLocalizedString("Password's first and last char cannot be a space", comment: ""))
           return false
       }
       
       return true
   }
}

Notes:

  • isDataValid verifies the requirements of this specific app.
  • If a model is provided, the extension doesn’t need to have any code at all. And viceversa! It’s up to you.
Bottom Line: Anything related to data should be kept here, even if in certain complex situations you can create data subslices. But this is a story for another time.

4.6 APLoginNetwork

Powers the communication with a server, be that an API, a service such as Firebase etc. There’s no class at all to be defined here.

import UIKit
import FirebaseAuth
import MBProgressHUD

extension APLoginViewController {
   
   /**
    *  Simply logins the user using the classic email & password.
    */
   func login() {
       MBProgressHUD.showAdded(to: self.view, animated: true)
       FIRAuth.auth()?.signIn(withEmail: emailTextField.text!,
                              password: passwordTextField.text!,
                              completion: { [unowned self] (user, error) in
                               self.handleResponse(error: error)
       })
   }
   
   /**
    *  Handles the response of the request made above. We're  going to the playground.
    */
   fileprivate func handleResponse(error: Error?) {
       MBProgressHUD.hide(for: self.view, animated: true)
       if error != nil {
           APError.credential(error!)
           return
       }
       
       /**
        *  The user is not successfully logged in until the email is verified.
        */
       if !FIRAuth.auth()!.currentUser!.isEmailVerified {
           APIntent.gotoEmailVerification(sender: self)
           return
       }
       APIntent.gotoPlayground(sender: self)
   }
}

Notes:

  • Both the request and the response are defined in this slice.
  • Notice that the login method was previously called from the APLoginButtonController
Bottom Line: Anything related to networking should be kept here.

5. Rest Of The Project

5.1 General-Purpose Data

Paul, what if I have a general data model? Do I have to rewrite the same model in each view controller?

I strongly urge you not to do that 👊

The solution proposed here is straightforward: create a new folder at the root of your Xcode project and include there any data models you use routinely.

For instance, a social network might need to use an User model in most of the view controllers.

DRY is life!

5.2 Support Files

On many occasions, developers need or prefer to subclass classes from the UIKit. Or one might simply want to write some helper entities.

In these cases, Modlizer recommends to create another folder at the root called support or something alike.

In this way, you get a stable structure which do not interferes with the slices.

6. Conclusion

By introducing Modlizer to the iOS community, I don’t wish to say that it’s better than the other design patterns out there. I simply wanted to make it publicly available, because maybe it will help some of you.

It was solely tested in a freelancing, digital nomad environment and in teams of maximum 3 members, so it might not be bulletproof for larger groups of people working together.

But who knows? It worked superbly in our case, because it provided:

  • Modularity by using the slice ideology
  • Simplicity in each individual file (no more than 300 lines)
  • Reusability at a high level

Overall, fast development speed.

Thank you for reading. Here are some cookies for you!

If you enjoyed this article, would you mind to recommend or share it? It’d be greatly appreciated ✌️

I’m open to any opinions or suggestions!Check out this awesome Github repo for a sample project. Find me on Twitter or LinkedIn if you want to chat.

Sort:  

Congratulations @paulrberg! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

You published your First Post
You got a First Vote

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

By upvoting this notification, you can help all Steemit users. Learn how here!