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.
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.
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.
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.
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.
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.