Building Reusable iOS Components in Swift

June 29, 2018
Written by
Kayeli Dennis
Contributor
Opinions expressed by Twilio contributors are their own

ei1MoM9cqEUQoseUZ1fpzblpTtpjK57YFUArLmVFPgOis3ZmqCYMqpubojrk5DTPo8DwWsIXc-xV9Hn5-mJga7eQhlhdoQ9q1kVmoAB-FaQXnYrbHdfPvXrucIzeVR0OvGNQF8TI

Keeping it DRY (Don’t Repeat Yourself) is every software developer’s priority in seeing a product through to completion. It not only helps make debugging easier, but also provisions easy testing and writing less code whilst achieving more. More than once, I’ve found myself building reusable components to reduce the repetitiveness in my code. For instance, building an iOS application will involve building multiple table view controllers. This means one will end up writing the same pieces of code in a couple of places that feed the table view controllers with data. Why not just build a wrapper and reuse it throughout the app as a plugin?

Hopefully by the end of this article I’ll convince you to start building reusable components in Swift for iOS application development. To demonstrate reusable components, we’ll build a dummy app that simulates a company providing employees with meals and then soliciting a feedback form from the employees.

Prerequisites

For the tutorial today, you will need:

  • A Mac running the latest point release of macOS Sierra or later.
  • The latest version of Xcode 9.0 and above.

NB: At the time of writing this tutorial, the latest Xcode version was Xcode 9.0 using Swift 4.0.

Flow of the Application

The general flow of the app will be:

  1. Present a user with the meal items for the day (lunchtime!).
  2. When a user taps a meal item, take them to the ratings view.
  3. On the ratings page, the user can rate all the meals.
  4. The user can submit a rating.

If we were to build an administrative portal, more functional requirements would definitely be needed. For now though, focus on the consumers of the application and come up with reusable components that can achieve more with less code.

Getting Started with Reusable Components

First, we’ll set up the model. Open Xcode and start a new project. Then create a new Swift file, name it Meal and paste in the code below:

import UIKit
struct Meal {
    var name: String
    var photo: UIImage?
    var rating: Int?
}

Value types are safer to use than reference types (classes), so we make a struct.
Our meal struct houses the name, rating and optionally the image of the meal.

We’ll create our meal list view by employing a table view and its binding table view controller.

Find the main storyboard. Delete the default view controller scene. From the object library, pick a table view controller and drag it into the canvas/storyboard.

Highlight the table view controller scene and make it the initial view controller from the attributes inspector.

We can group the table view to make it more appealing:

 
Let’s create a controller for our table view.

Create a Cocoa Touch file and subclass it to a UITableViewController, naming it MealsTableViewController. Now, clear all enclosing functions in the file except for the viewDidLoad() function. The file should look like:

import UIKit

class MealsTableTableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Uncomment the following line to preserve selection between presentations
        // self.clearsSelectionOnViewWillAppear = false

        // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
        // self.navigationItem.rightBarButtonItem = self.editButtonItem
    }
}

A Reusable List View Through Composing Types

Since we are using a reusable dynamic cell, we ought to provide it with the number of rows it’s going to render and the contents of each cell. The first technique we’re going to employ is composing of types.

When building applications, we end up using multiple dynamic table views and consequently have to repeat code for our table view data source. We can make a generic class that provisions a datasource for all the table views we are building. We would only have to provide it with data and use it as a plugin.

Create a new swift file and name it ListViewDatasource. Type – or paste – the following lines of code:

import UIKit

class ListViewDatasource: NSObject, UITableViewDataSource {
    var list = [String]()

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return list.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = list[indexPath.row]
        return cell
    }
}

Above, we have bundled the logic of manufacturing the number of rows and the cells at each indexPath in the ListViewDataSource.

The beauty of this approach is we can reuse this functionality whenever we want to render a list of items. If you are displaying more than just text in your cells it can be a little complex, but it all whittles down to the same logic.

Note that NSObject is the root class of most Objective-C class hierarchies, from which subclasses inherit a basic interface to the runtime system.

In our MealsTableViewController update the lines of code to match:

class MealsTableViewController: UITableViewController {

    // MARK: Instance Properties
    var meals = [Meal]()

    // MARK: Private Properties
    private let dataSource = ListViewDatasource()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = dataSource
        setUpMeals()
        render()
    }

    // MARK: Private Functions
    private func render() {
        let mealNames = meals.map { return $0.name }
        dataSource.list = mealNames
        tableView.reloadData()
    }

    private func setUpMeals () {
        let mealItemone = Meal(name: "Roasted Chicken", photo: nil, rating: nil)
        let mealItemtwo = Meal(name: "Fish", photo: nil, rating: nil)
        let mealItemthree = Meal(name: "Fries", photo: nil, rating: nil)
        meals.append(mealItemone)
        meals.append(mealItemtwo)
        meals.append(mealItemthree)
    }
}

We have a meals container that will hold the meals. We have also declared our data source as the ListViewDatasourceand made an instance of the same.

From the viewDidLoad function we also communicate to our table view. It now depends on the ListViewDatasource as its data source for the content that needs to be displayed.

Our setUpMeals function supplies the meals.

Finally, we render the meals. Remember in our ListViewDatasource we declared a list of strings which will supply the number of rows and the content of each cell. We get that list from our meal items by mapping the meal objects to a list of meal names. Once we have the meal names, we then channel them to our generic datasource through the list property. After that, we just reload the table view to reflect the content. At this point, we should be able to build the project without any problems.

This sums up our first approach to making reusable components, composing types.

Reusable Rating Component as a Nib File

Next up, we are are going to build the rating component. We’ll bundle it as a nib file and reuse it when rating the various meals in the ratings view. When a user clicks on a meal item on the meal items table view, they will be taken to the meals ratings view. From there, they’ll be able to rate the meals.

With this design, all ratings can take place in the ratings view at the same time:

Drag a view controller from the object library onto the main storyboard. Drag a button and a label and place it as below:


In this case, our button will let us return to the main meal items view. Find a suitable back arrow image and add it to the application assets. We can now configure our button with the image:

For now, use Meal Name as the placeholder text in the label and later populate it with the meal item name. These operations will leave you with this:

In the above scene we can change the background color. For example, here’s a nice color:

Next, we will build our ratings component with the next and previous button actions. Create a new nib file (File > New > View) and name it RatingsView. The layout of the nib view will match the following:
        


The stars are five buttons stacked in a horizontal stack view, configured with an image instead of plain text. Additionally, add a previous and next button to the screen.

Reusable Rating View

Create a Swift class named RatingView subclassing the UIView. Besides the view outlets, update your file to look like this:

import UIKit

class RatingView: UIView {

    // MARK: Properties
    var rating = 0 {
        didSet{
            updateState()
        }
    }

    // MARK: Private Properties
    private var ratingButtons:[UIButton] = []

    // MARK: Outlets
    @IBOutlet weak var first: UIButton!
    @IBOutlet weak var second: UIButton!
    @IBOutlet weak var third: UIButton!
    @IBOutlet weak var fourth: UIButton!
    @IBOutlet weak var fifth: UIButton!

    override func awakeFromNib() {
        super.awakeFromNib()
        ratingButtons = [first, second, third, fourth, fifth]
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    // MARK: Private Functions
    private func updateState(){
        for (index, button) in ratingButtons.enumerated(){
            button.isSelected = index < rating
        }
    }

    private func setUpButtons(){
        for button in self.ratingButtons {
            button.setImage( imageLiteral(resourceName: "filledStar"), for: .selected)
            button.setImage( imageLiteral(resourceName: "emptyStar"), for: .normal)
            button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        }
        updateState()
    }

    @objc func buttonTapped(button: UIButton){
        guard let index = self.ratingButtons.index(of: button) else { return }
        let selectedRating = index + 1
        if selectedRating == rating {
            rating = 0
        } else {
            rating = selectedRating
        }
    }
}

In the above code snippet, our rating property holds a number that will represent a submitted rating. We have used a key-value observer for listening for a new rating so we can update the state.

The updateState() method consumes the rating value to toggle the states of the rating buttons. The setUpButtons() is our binder for the various rating event states. Finally, the button tapped function will update the rating property for a given meal.

Now that we have our rating component as a nib file, we need to load it in the ratings view controller so we can reuse it when rating the meals.

Create a new Cocoa Touch file subclassing it to a UIViewController, and name it RatingsViewController. This is the controlling class for our ratings view.

You can bind the ratings view with this class:

Create an outlet for the label we had on the ratings view and name it mealName.

import UIKit

class RatingsViewController: UIViewController {
	    //MARK: Outlets
    @IBOutlet weak var mealName: UILabel!

}

Our end goal is to have the user switch between meals and rating. To do this, we will load three copies of our nib file and use them to switch between meals. We’ll animate them accordingly depending on the number of meals on the MealsTableViewController.

To this end, add the following lines of code:

 public var mealItems: [Meal]?

    // MARK: Private Properties
    private var currenratingView: RatingView!
    private var nextratingView: RatingView!
    private var previousratingView: RatingView!
    private var mealIndex = 0 {
        didSet{}
    }

    private var currentLeftConstraint: NSLayoutConstraint!
    private var previousLeftConstraint: NSLayoutConstraint!
    private var nextLeftConstraint: NSLayoutConstraint!

    private let currentLeftOffset: CGFloat = 20
    private let nextLeftOffset: CGFloat = 344
    private let previousLeftOffset: CGFloat = -316

    private lazy var ratingNib = UINib(nibName: "ratingsView", bundle: nil)

The currentratingView, nextratingView, and previousratingView properties will be the containers that hold the ratings view for the various meals as we switch, depending on the number of meals.

One thing we forgot about is the communication between the rating component and the ratings view. Modify the RatingView.swift file to look like the following:

import UIKit

protocol RatingViewDelegate {
    func nextButtonTapped()
    func previousButtonTapped()
}

class RatingView: UIView {

    var delegate: RatingViewDelegate?

    // MARK: Properties
    var rating = 0 {
        didSet{
            updateState()
        }
    }

    // MARK: Private Properties
    private var ratingButtons:[UIButton] = []

    // MARK: Outlets
    @IBOutlet weak var first: UIButton!
    @IBOutlet weak var second: UIButton!
    @IBOutlet weak var third: UIButton!
    @IBOutlet weak var fourth: UIButton!
    @IBOutlet weak var fifth: UIButton!

    override func awakeFromNib() {
        super.awakeFromNib()
        ratingButtons = [first, second, third, fourth, fifth]
        setUpButtons()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    //MARK: Actions
    @IBAction func previousButtonTapped(_ sender: UIButton) {
        delegate?.previousButtonTapped()
    }

    @IBAction func nextButtonTapped(_ sender: UIButton) {
        delegate?.nextButtonTapped()
    }

    // MARK: Private Functions
    private func updateState(){
        for (index, button) in ratingButtons.enumerated(){
            button.isSelected = index < rating
        }
    }

    private func setUpButtons(){
        for button in self.ratingButtons {
            button.setImage( imageLiteral(resourceName: "filledStar"), for: .selected)
            button.setImage( imageLiteral(resourceName: "emptyStar"), for: .normal)
            button.addTarget(self, action: #selector(buttonTapped(button:)), for: .touchUpInside)
        }
        updateState()
    }

    @objc func buttonTapped(button: UIButton){
        guard let index = self.ratingButtons.index(of: button) else { return }
        let selectedRating = index + 1
        if selectedRating == rating {
            rating = 0
        } else {
            rating = selectedRating
        }
    }
}

Note that we introduced a delegate for communicating with the parent view; RatingViewController.

Next up, we are going to load our nib files and assign them to our currentratingView, nextratingView and previousratingView. This activity can be housed within the viewDidLoad View Controller lifecycle method. We will also point the RatingViewController as the delegate to the rating view nib files. Our updated viewDidLoad will now resemble the code snippet below.

override  func viewDidLoad(){
        guard let current = loadRatingView(),
            let next = loadRatingView(),
            let previous = loadRatingView() else { return }
        super.viewDidLoad()
        currentratingView = current
        nextratingView = next
        previousratingView = previous
        currentratingView.delegate = self
        nextratingView.delegate = self
        previousratingView.delegate = self
}

This operation will necessitate the need to put in place constraints for the views on the RatingViewController. Therefore, inside the RatingViewController we can build two helper functions that will assist with the positioning. We can add the code snippet below:

    private func activateTopConstraint(`for` ratingView: UIView){
        ratingView.translatesAutoresizingMaskIntoConstraints = false
        let constant: CGFloat = 203
        NSLayoutConstraint(item: ratingView,
                           attribute: .top,
                           relatedBy: .equal,
                           toItem: self.view,
                           attribute: .top,
                           multiplier: 1.0,
                           constant: constant).isActive = true
    }

    private func activateLeftConstraint(`for` cardView: UIView,
                                        leftOffset: CGFloat) -> NSLayoutConstraint {

        let constraint = NSLayoutConstraint(item: cardView,
                                            attribute: .left,
                                            relatedBy: .equal,
                                            toItem: self.view,
                                            attribute: .left,
                                            multiplier: 1,
                                            constant: leftOffset)

        constraint.isActive = true

        return constraint
    }

The two methods above, activateTopConstraint and activateLeftConstraint, will also be handy in helping us animate the rating views during the rating process.

We can employ these methods in our viewDidLoad function to position the rating cards:

        // Top Constraints for our rating views
        activateTopConstraint(for: currentratingView)
        activateTopConstraint(for: nextratingView)
        activateTopConstraint(for: previousratingView)

        // Left Constraints for our rating Views
        currentLeftConstraint = activateLeftConstraint(for: currentratingView, leftOffset: currentLeftOffset)
        nextLeftConstraint = activateLeftConstraint(for: nextratingView, leftOffset: nextLeftOffset)
        previousLeftConstraint = activateLeftConstraint(for: previousratingView, leftOffset: previousLeftOffset)

Now this code provisions for us to create a bindView method that will consume the the mealItems list and update the mealName label outlet.

    private func bindView(){
        guard let meals = mealItems else { return }
        let currentMeal = meals[mealIndex]
        self.mealName.text = currentMeal.name
    }

To employ the above method, we can add it at the end of the viewDidLoad function.

The last step is to update our delegate methods for the rating cards whenever a user taps on the next meal item in the meal items list during the rating process. We can also employ constraint animations for the same to achieve a slide show effect.

extension RatingsViewController: RatingViewDelegate {
    func previousButtonTapped() {
        print("Yes previous tapped!")
    }

    func nextButtonTapped() {
        guard let mealItems = mealItems else { return }
        guard self.mealIndex < mealItems.count - 1 else { return }

        previousratingView.isHidden = true
        previousLeftConstraint.constant = nextLeftOffset + 336

        self.view.layoutIfNeeded()

        previousratingView.isHidden = false

        UIView.animate(withDuration: 0.5,
                       delay: 0.0,
                       options: .curveEaseInOut,
                       animations: {
                        self.currentLeftConstraint.constant = self.previousLeftOffset
                        self.nextLeftConstraint.constant = self.currentLeftOffset
                        self.previousLeftConstraint.constant = self.nextLeftOffset

                        self.view.layoutIfNeeded()
        },
                       completion: { _ in
                        let tempView = self.currentratingView

                        self.currentratingView = self.nextratingView
                        self.nextratingView = self.previousratingView
                        self.previousratingView = tempView

                        self.currentLeftConstraint.isActive = false
                        self.nextLeftConstraint.isActive = false
                        self.previousLeftConstraint.isActive = false

                        self.setUpAnimationConstraints()

                        self.mealIndex += 1

        } )
    }

    func setUpAnimationConstraints() {
        currentLeftConstraint = activateLeftConstraint(for: currentratingView, leftOffset: currentLeftOffset)
        nextLeftConstraint = activateLeftConstraint(for: nextratingView, leftOffset: nextLeftOffset)
        previousLeftConstraint = activateLeftConstraint(for: previousratingView, leftOffset: previousLeftOffset)
    }
}

We loaded three copies of the rating nib files, then pointed them to the ratings view controller as the parent view.

We have the delegate methods, nextButtonTapped() and previousButtonTapped(), that will switch the rating views depending on the number of meal items. At this point, you should be able to build and run – and test – the app.

Click on one of the meals and it should segue you to the ratings view. Click on next after rating a meal and you will notice the nice slideshow effect.

Reusable Components

We have just built a reusable plugin as a nib file and reused it as needed in a food rating application. You’ve gained insight into how to bundle a reusable component using a nib file, and then you reused the component.

These aren’t the only ways to build reusable components though. There are a few more techniques, including Swift Generics and delegate patterns, that come in handy when writing reusable components. We’ll save those ones for next time.

The source code for this project can be found in this repository on Github.

That’s all for now. I’m Kayeli, an iOS Engineer currently working at Homie. Reach me on Instagram @nedozkayeli.