Five tips to make your Swift code more maintainable

As your project grows in size, it becomes increasingly important to make your existing code as maintainable as possible. With a few basic tips, you can ensure that your code is easily maintained from the ground up.

Abstract methods are key#

It’s no good having lots of methods that do roughly the same thing. This goes against the DRY (Don’t Repeat Yourself) principle. Take a look at these methods:

// These three methods...
func createStoreNavigationController() -> UINavigationController {
    let storeVC = storyboard.instantiateViewController(withIdentifier: "storeView")
    storeVC.title = "Store"
    return UINavigationController(rootViewController: storeVC)
}

func createOrdersNavigationController() -> UINavigationController {
    let ordersVC = storyboard.instantiateViewController(withIdentifier: "ordersView")
    ordersVC.title = "Orders"
    return UINavigationController(rootViewController: ordersVC)
}

func createSettingsNavigationController() -> UINavigationController {
    let settingsVC = storyboard.instantiateViewController(withIdentifier: "settingsView")
    settingsVC.title = "Settings"
    return UINavigationController(rootViewController: settingsVC)
}

// ...are currently called like this
createStoreNavigationController()
createOrdersNavigationController()
createSettingsNavigationController()

As you can see, they are all doing the same three things:

  • Instantiating a UIViewController from the storyboard
  • Setting the title for the View Controller
  • Embedding the View Controller inside a UINavigationController

It’s easy enough to apply some abstraction to this method, so that it can be reused:

// Our new, abstracted method...
func createNavigationController(for vcName: String) -> UINavigationController {
    let rootVC = storyboard.instantiateViewController(withIdentifier: "\(vcName.lowercased())View")
    rootVC.title = vcName
    return UINavigationController(rootViewController: rootVC)
}

// ...can be called like this
createNavigationController(for: "Stores")
createNavigationController(for: "Orders")
createNavigationController(for: "Settings)

By doing this, we’ve cut down our code significantly. Now all of the logic is in one place, it is easier to make modifications to should we need to change it in the future.

Create type aliases for repetitive completion handlers#

If you, like me, make plenty of network calls, then you’ll be very familiar with Completion Handlers and how they work. For example, you might be making API calls that perform CRUD (Create-Read-Update-Delete) operations on various different tables in your backend. As a result, you may find that you are using the same completion handler type across many different methods.

Take a look at these methods here:

class OrdersAPI: API {
    func createOrder(order: Order, _ completion: @escaping (Result<String, Error>) -> Void) {
        // some network call here...
    }
}

class ProductsAPI: API {
    func createProduct(product: Product, _ completion: @escaping (Result<String, Error>) -> Void) {
        // some network call here...
    }
}

You can see that we have the same completion handler type in both of these methods. If successful, we pass through an ID of type String, and if it fails, we simply pass through an Error, which our UI can manipulate to present an error back to the user.

To make maintenance on these API methods easier in the future, we can simply create a typealias for this completion handler type like so:

// In the API superclass, we can create the typealias...
class API {
    typealias CreateCompletionCallback = (Result<String, Error>) -> Void
}

// ...and then in our subclasses, we can use it like so:
class OrdersAPI: API {
    func createOrder(order: Order, _ completion: @escaping CreateCompletionCallback) {
        // some network call here...
    }
}

class ProductsAPI: API {
    func createProduct(product: Product, _ completion: @escaping CreateCompletionCallback) {
        // some network call here...
    }
}

Use enums instead of Strings and Ints#

There’s two use cases I want to describe where enums are particularly useful. The first is for UserDefaults, where you would specify a String to set or get a value. However, as your app grows you could end up with Strings everywhere, which may change over time. To make maintenance in the future much easier and to keep all of your keys in one place, you can create an enum like the example below:

// Store all your keys here...
enum UDKeys: String {
    case highScore = "highScore"
    case preferredColour = "preferredColour"
    case firstName = "firstName"
}

// And then using them is as simple as this:
UserDefaults.standard.set(newHighScore, forKey: UDKeys.highScore.rawValue)
UserDefaults.standard.integer(forKey: UDKeys.highScore.rawValue)

The other use case is for UICollectionView and UITableView sections. Enums can also be used to represent integers, making it compatible with the IndexPath's section property. Here’s an example of how it can be implemented:

// Define your section names here, in the same order as in your UITableView
// or UICollectionView. "About" is section 0, "Account" is section 1, and so on...
enum SettingsSections: Int {
    case about, account, rating, social
}

// And then using them inside delegate methods is straightforward
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // ensure that we have defined an enum case for the section that was selected
    guard let settingsSections = SettingsSections(rawValue: indexPath.section) else { fatalError() }

    switch settingsSections {
    case .about:
        // do about stuff
    case .account:
        // do account stuff
    case .rating:
        // do rating stuff
    case .social:
        // do social stuff
    }
}

Create extensions#

One of the cool things about Swift is that you can extend existing classes to add additional functionality. One that I use a lot is for when I retrieve objects from a database that each have a timestamp, which I then want to convert into a Date object. Instead of creating and configuring a DateFormatter object every time I want to do this conversion, I can put this all into a method and extend the String class:

extension String {

    func dateFromTimestamp() -> Date? {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
        dateFormatter.locale = Locale(identifier: "en_GB")
        dateFormatter.timeZone = .current

        return dateFormatter.date(from: self)
    }
}

Using this method anywhere I need it is then just a one-liner:

let date = "2020-04-19T11:42:04.966Z".dateFromTimestamp()

Modifying a UI element in the same way several times? Time to subclass it!#

For a consistent user interface, you will be wanting to ensure that your UIButton, UILabel and any other views have the same look and feel. At first, you may be tempted to apply any colour changes or fonts inside one of your UIViewController's lifecycle methods. However, with a larger app you will find that you are repeating code over and over again, and so it makes sense to slim this down a bit.

The most common way to do this is to subclass from these views, and then apply any changes to the view during initialisation:

class TintedButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.configure()
    }

    // this method is for if you use the element in a storyboard
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.configure()
    }

    convenience init(title: String) {
        self.init(frame: .zero)
        self.configure()
        setTitle(title, for: .normal)
    }

    private func configure() {
        backgroundColor = .orange
        layer.cornerRadius = 10
        setTitleColor(.black, for: .normal)
        titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
        translatesAutoresizingMaskIntoConstraints = false
    }
}

This new class is now usable when creating your views in the interface builder, as well as programmatically:

let addButton = TintedButton(title: "Add To Basket")

If you’re using SwiftUI instead of UIKit, you can create a new View, and add any repeated View Modifiers in there.

Conclusion#

Ensuring that your code is maintainable is key to an easier ride later in your development lifecycle. Remember the DRY principle - “Don’t Repeat Yourself” - I won’t say it again. 🤣

That concludes this tutorial. If you have any questions, feel free to reach out to me via Twitter, and keep an eye open for my upcoming tutorials.