Waldo sessions now support scripting! – Learn more
App Development

3 Ways to Improve Your Code With Swift Extensions

Juan Reyes
Juan Reyes
3 Ways to Improve Your Code With Swift Extensions
December 6, 2022
6
min read

One of the many beautiful things about working with a modern programming language is the extensive toolset of features it offers, and the quality-of-life improvements that can bring for developers. So whether you're a beginner familiarizing yourself with a more approachable syntax, a professional capitalizing on the malleability and extensibility of the frameworks, or a power user pushing what can be done to the limit, you can always expect to find something for you.

Swift is one such language, and there's no doubt that the community behind it will stop at nothing to make sure it stays at the forefront of innovation. But sometimes, even all that work is not enough. Sometimes you require something very specific to your circumstances and needs. 

Imagine you have a library with features that are very close to what you need and are very easy to implement, but it doesn't cover all the points, or the result is not exactly what you require. 

Thankfully, there's one feature of Swift that we can use to cover the gaps in functionality and adapt code, extending what a library can do. And that feature is called Extensions. 

So today, we'll show you three ways to improve your code with Swift Extensions.

What Are Swift Extensions?

As the name states, extensions, well, extend the functionality of your code. Nothing extraordinary at first glance, but when you think about it, it's a pretty powerful tool. 

To be more specific, extensions extend Swift named types (e.g., classes, enums, structs, and protocol) so you can add functionality. This means you can insert code into an existing system or into third-party code to which you wouldn't otherwise have access. 

Why Do We Use Extensions in Swift?

Swift extensions are a great way to fill gaps in existing functionalities and features that can be hard to modify. As we mentioned before, it's common to find that third-party libraries and packages don't cover all the features we need. But with extensions, you can take care to complete the functionality and save tons of hours of work. What's more, many of the libraries you'll find online are built as extensions of existing native libraries or frameworks themselves.

Additionally, extensions offer a path to declutter and tidy up your code, making it more DRY ("Don't Repeat Yourself") and preventing unexpected behavior and bugs. 

How to Use Swift Extensions

Want an example? Alright, here's a simple class representing an object Car with some properties and methods. 


import UIKit

class Car {
    enum Condition {
        case new
        case excellent
        case good
        case fair
        case poor
        
        func description() -> String {
            switch self.hashValue {
            case 0:
                return "New"
            case 1:
                return "Excellent"
            case 2:
                return "Good"
            case 3:
                return "Fair"
            case 4:
                return "Poor"
            default:
                return "Good"
            }
        }
    }
    
    private let maker: String?
    private let model: String?
    private var color: UIColor?
    private var condition: Condition?
    private var milleage: Int?
    private let builtAt: Date?
    private let price: Float?
    
    init(maker: String, model: String, color: UIColor, condition: Condition, milleage: Int, builtAt: Date?, price: Float?) {
        self.maker = maker
        self.model = model
        self.color = color
        self.condition = condition
        self.milleage = milleage
        self.builtAt = builtAt
        self.price = price
    }
    
    func getMaker() -> String? {
        return maker
    }
    
    func getModel() -> String? {
        return model
    }
    
    func getColor() -> String? {
        return color?.description
    }
    
    func getCondition() -> String? {
        return condition?.description()
    }
    
    func getMilleage() -> String? {
        return "\(String(describing: milleage))"
    }
    
    func getBuiltDate() -> String? {
        return builtAt?.formatted(date: .abbreviated, time: .omitted)
    }
    
    func getPrice() -> Float? {
        return price
    }
}

let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"

var usedCar = Car(maker: "Honda",
                  model: "Civic",
                  color: .red,
                  condition: .good,
                  milleage: 15_000,
                  builtAt: formatter.date(from: "01/01/1999"),
                  price: 2500)

usedCar.getMaker()
usedCar.getModel()
usedCar.getColor()
usedCar.getCondition()
usedCar.getMilleage()
usedCar.getBuiltDate()

Suppose this was part of a third-party library you're implementing for a feature, but the class was missing a method to calculate the car's amortized value. The best-case scenario would be to implement that feature in your code and plug the values yourself. However, in a worst-case scenario, where the feature is more complex, that gap could compromise the viability of implementing the library and maybe even require you to build the whole thing yourself, pushing the development schedule back and making your manager very unhappy. 

Quote - extensions offer a path to declutter and tidy up your code

Now, let's use the power of extensions to add the missing method as part of the class itself. 


extension Car {
    func amortizedPrice() -> Float {
        var conditionMultiplier = 5
        
        switch condition {
        case .new:
            conditionMultiplier = 5
            break
        case .excellent:
            conditionMultiplier = 4
            break
        case .good:
            conditionMultiplier = 3
            break
        case .fair:
            conditionMultiplier = 2
            break
        case .poor:
            conditionMultiplier = 1
            break
        default:
            conditionMultiplier = 5
            break
        }
        
        var dateMultiplier = 5
        
        let yearsSinceMade = builtAt!.distance(to: Date())
        
        switch yearsSinceMade {
        case 0:
            dateMultiplier = 5
            break
        case 1..<5:
            dateMultiplier = 4
            break
        case 5..<10:
            dateMultiplier = 3
            break
        case 10..<20:
            dateMultiplier = 2
            break
        default:
            dateMultiplier = 1
            break
        }
        
        return price! - (price! * Float(conditionMultiplier + dateMultiplier) / 100)
    }
}

usedCar.amortizedPrice()

Wow, look at that! Brief and simple. 

As you can see, all that we did was declare the extension with the corresponding class and add the extra code. So you can safely assume this code now lives inside the original class. 

3 Ways to Improve Your Code With Swift Extensions

Now, let's talk about how you can improve your code with extensions. 

Using Enhanced Features

The first obvious way is to include third-party libraries that enhance features or make you more productive by simplifying complex tasks or repetitive procedures. 

A good example is something like the SwiftDate library

This library works by extending the String object in Swift to add methods to transform a string into a Swift date in one line. 

As you might have noticed in the previous example, I had to create a date formatter object and define the date string format so it could adequately parse the string into a date object. This situation is all good and well, but it can be repetitive and prone to bugs unless appropriately managed. So there has to be a better way to do it.

And that's why talented developers are creating third-party libraries like SwiftDate to make other developers more empowered and more productive, helping the community. 

Once you have your Swift package set up, adding SwiftDate as a dependency is as easy as adding it to your Package.swift dependencies value. 


dependencies: [
.package(url: "https://github.com/malcommac/SwiftDate.git", from: "5.0.0")
]

Then, you can import it to your code and change the date formatter code logic to the following: 


"01/01/1999".toDate()

And you can now handle dates in a much more straightforward way throughout your app. 

But wait—it doesn't stop there. 

Extensions also allow you to add more than just methods. You can add properties, initializers, subscripts, and more. If you want some examples, you can find them in the official Swift documentation

Implementing Protocol Conformance

The second way extensions can empower your code is by adding conformance to protocols in classes that otherwise wouldn't have it. How? Well, just like how we were adding methods to existing libraries, you can also declare conformance to protocols and provide the methods to conform in the extension itself. 

To illustrate, let's say that I want to make the Car class conform to a protocol necessary to process the price of cars in bulk. 

Once you have the protocol defined, all you have to do is add the conformance to the extension and implement any required methods. 


protocol BulkPricing {
    func amortizeCar() -> Float
}

extension Car: BulkPricing {
    func amortizeCar() -> Float {
        return amortizedPrice()
    }
    
    func amortizedPrice() -> Float {
        var conditionMultiplier = 5
        
        switch condition {
        case .new:
            conditionMultiplier = 5
            break
        case .excellent:
            conditionMultiplier = 4
            break
        case .good:
            conditionMultiplier = 3
            break
        case .fair:
            conditionMultiplier = 2
            break
        case .poor:
            conditionMultiplier = 1
            break
        default:
            conditionMultiplier = 5
            break
        }
        
        var dateMultiplier = 5
        
        let yearsSinceMade = builtAt!.distance(to: Date())
        
        switch yearsSinceMade {
        case 0:
            dateMultiplier = 5
            break
        case 1..<5:
            dateMultiplier = 4
            break
        case 5..<10:
            dateMultiplier = 3
            break
        case 10..<20:
            dateMultiplier = 2
            break
        default:
            dateMultiplier = 1
            break
        }
        
        return price! - (price! * Float(conditionMultiplier + dateMultiplier) / 100)
    }
}

Without this feature, you would have to develop a wrapper object to contain the instance and then deal with all the hassle of managing the properties and methods inside this new layer of complexity. Not only is that more work, but it adds space for bugs to be introduced into the code. 

Tidying Up

The last way extensions can take your code to the next level is by allowing you to organize your code and make it more readable.

You could ask yourself, "How is adding a separate chunk of code 'organizing'?" The answer is a simple concept named functionality isolation—also known as single-purpose programming. This means making your code have single units of work that render a result and fulfill a single purpose. 

Here's an example. 

I was working with this class, which fetches and displays the current weather information in LA in a table view. 


//
//  ViewController2.swift
//  TabBarControllerSample
//
//  Created by Juan Mueller on 11/19/22.
//  For more, visit www.ajourneyforwisdom.com

import Foundation
import UIKit

class ViewController2: UIViewController, UITableViewDataSource, UITableViewDelegate {
    // Outlet for tableView
    @IBOutlet weak var tableView: UITableView!
    // Weather data JSON property
    var wdata: [String: Any]?
    
    override func viewDidLoad() {
        // Call the viewdidload super
        super.viewDidLoad()
        // Always register the tableview cell with the corresponding identifier in the storyboard
        // so it can be reused
        tableView?.register(UITableViewCell.self, forCellReuseIdentifier: "dataCell")
        // Set the tableview datasource to self
        tableView?.dataSource = self
        // invoke the requestWeatherData method and handle its completion
        requestWeatherData {
            // Code inside this block will be executed in the main thread
            DispatchQueue.main.async { [self] in
                // Reload the tableview
                tableView?.reloadData()
            }
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Retrieve the registered reusable cell from the tableview
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "dataCell",
                                                                  for: indexPath)
        // Switch between the 4 possible rows to display
        switch indexPath.item {
        case 0:
            // Set the cell text
            cell.textLabel?.text = "Temp: " + (wdata != nil ? "\((wdata!["current_weather"] as! [String : Any])["temperature"] ?? "---")" : "---")
            break
        case 1:
            // Set the cell text
            cell.textLabel?.text = "Elevation: " + (wdata != nil ? "\(wdata!["elevation"] ?? "---")" : "---")
            break
        case 2:
            // Set the cell text
            cell.textLabel?.text = "Wind speed: " + (wdata != nil ? "\((wdata!["current_weather"] as! [String : Any])["windspeed"] ?? "---")" : "---")
            break
        case 3:
            // Set the cell text
            cell.textLabel?.text = "Feels like: " + (wdata != nil ? "\(((wdata!["daily"] as! [String : Any])["apparent_temperature_max"] as! [NSNumber])[0])" : "---")
            break
        default:
            break
        }
        // Return cell
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        // Set the height of cells as fixed
        return 60.0
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Set the number of rows to 4
        return 4
    }
    
    // Method that requests the weather data to meteo and updates the wdata property
    func requestWeatherData(_ completion: @escaping () -> Void) {
        // create the url
        let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=32.22&longitude=-110.93&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset¤t_weather=true&temperature_unit=fahrenheit&timezone=America%2FLos_Angeles")!
            
        // now create the URLRequest object using the url object
        let request = URLRequest(url: url,
                                 cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
                                 timeoutInterval: 30.0)
            
        // create dataTask using the session object to send data to the server
        let task = URLSession.shared.dataTask(with: request, completionHandler: { [self] data, response, error in
                
           guard error == nil else {
               return
           }
                
           guard let data = data else {
               return
           }
                
          do {
             //create json object from data
             if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
                 // Update wdata property
                 wdata = json
                 // Call completion handler
                 completion()
             }
          } catch let error {
            print(error.localizedDescription)
          }
        })
        // Trigger request
        task.resume()
    }
}

Most engineers will have no problem understanding what it does and working on it. But that doesn't mean you can't improve the readability. 

This code can be segmented with extensions so that the conformance to the table view protocols and delegates resides in its section, separating responsibilities and preventing a feature from being tampered with unnecessarily. 


//
//  ViewController2.swift
//  TabBarControllerSample
//
//  Created by Juan Mueller on 11/19/22.
//  For more, visit www.ajourneyforwisdom.com

import Foundation
import UIKit

class ViewController2: UIViewController  {
    // Outlet for tableView
    @IBOutlet weak var tableView: UITableView!
    // Weather data JSON property
    var wdata: [String: Any]?
    
    override func viewDidLoad() {
        // Call the viewdidload super
        super.viewDidLoad()
        // Always register the tableview cell with the corresponding identifier in the storyboard
        // so it can be reused
        tableView?.register(UITableViewCell.self, forCellReuseIdentifier: "dataCell")
        // Set the tableview datasource to self
        tableView?.dataSource = self
        // invoke the requestWeatherData method and handle its completion
        requestWeatherData {
            // Code inside this block will be executed in the main thread
            DispatchQueue.main.async { [self] in
                // Reload the tableview
                tableView?.reloadData()
            }
        }
    }
    
    // Method that requests the weather data to meteo and updates the wdata property
    func requestWeatherData(_ completion: @escaping () -> Void) {
        // create the url
        let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=32.22&longitude=-110.93&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset¤t_weather=true&temperature_unit=fahrenheit&timezone=America%2FLos_Angeles")!
            
        // now create the URLRequest object using the url object
        let request = URLRequest(url: url,
                                 cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
                                 timeoutInterval: 30.0)
            
        // create dataTask using the session object to send data to the server
        let task = URLSession.shared.dataTask(with: request, completionHandler: { [self] data, response, error in
                
           guard error == nil else {
               return
           }
                
           guard let data = data else {
               return
           }
                
          do {
             //create json object from data
             if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
                 // Update wdata property
                 wdata = json
                 // Call completion handler
                 completion()
             }
          } catch let error {
            print(error.localizedDescription)
          }
        })
        // Trigger request
        task.resume()
    }
}

extension ViewController2: UITableViewDataSource, UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Retrieve the registered reusable cell from the tableview
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "dataCell",
                                                                  for: indexPath)
        // Switch between the 4 possible rows to display
        switch indexPath.item {
        case 0:
            // Set the cell text
            cell.textLabel?.text = "Temp: " + (wdata != nil ? "\((wdata!["current_weather"] as! [String : Any])["temperature"] ?? "---")" : "---")
            break
        case 1:
            // Set the cell text
            cell.textLabel?.text = "Elevation: " + (wdata != nil ? "\(wdata!["elevation"] ?? "---")" : "---")
            break
        case 2:
            // Set the cell text
            cell.textLabel?.text = "Wind speed: " + (wdata != nil ? "\((wdata!["current_weather"] as! [String : Any])["windspeed"] ?? "---")" : "---")
            break
        case 3:
            // Set the cell text
            cell.textLabel?.text = "Feels like: " + (wdata != nil ? "\(((wdata!["daily"] as! [String : Any])["apparent_temperature_max"] as! [NSNumber])[0])" : "---")
            break
        default:
            break
        }
        // Return cell
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        // Set the height of cells as fixed
        return 60.0
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Set the number of rows to 4
        return 4
    }
}

Now, all the code related to table view behavior is in the extension, making it much more manageable. 

You can find the complete code here

In Summary

This is by no means the full extent of the versatility and power extensions offer. I could talk for hours about the many subtle and groundbreaking ways extensions have made my life easier and removed a significant stress load. 

And if you want to remove even more stress from your life and simplify your work further, I recommend you check out Waldo's extensive toolset for UI testing. It requires no coding and is very approachable, even for non-developers.

Related readings:

Automated E2E tests for your mobile app

Waldo provides the best-in-class runtime for all your mobile testing needs.
Get true E2E testing in minutes, not months.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.