Establishing Solid Base Types
AP020-0200 • • Florian Pfisterer
This article is intended to provide a set of solid base types that you can use in almost any Swift project. These types, if used intelligently, make your code more clean & succinct, maintainable and easier to unit-test.
1. The Result Type
In almost any project, there are calls to functions that might fail. In Swift, the common pattern in this case is that
either the function throws an error or it returns nil
.
While this might be convenient in some cases, in others you neither want to throw
and rethrow
errors in
different nested function calls nor do you want to unwrap optionals all the time.
To solve this problem, consider the following generic Result type:
enum Result<T> {
case success(T)
case failure(ErrorType)
}
This type, when used as the result of a function call for example, represents one of the following cases:
- The call succeeded, and here’s the result: an object of type
T
- Somewhere there was an error, and here it is: an
ErrorType
case (you could useNSError
as well)
The Result type basically extends the Swift standard library optional type by providing the error of why there is no
result object (T
) available.
enum Optional<T> {
case some(T)
case none
}
That’s really all there is to Swift optionals, the “?”s and “!”s are just syntactic sugar.
Example Usage
Consider the following function that asynchronously loads some users from a remote database (inside of a
DatabaseClient
, for example):
static func loadUsers(completion: (Result<[User]>) -> Void) {
let queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
dispatch_async(queue, {
do {
// .. heavy network tasks to load the users
let users: [User] = try networkOperationThatMightThrow()
completion(.success(users))
} catch let error {
completion(.failure(error))
}
})
}
When we then use this function, in our ViewController
for example, we neither have to wrap the call in a
do-try-catch
block, nor do we have to unwrap optionals. Our code stays clean, maintainable and easy to understand.
DatabaseClient.loadUsers { result in
switch result {
case .failure(let error):
// present an alert etc.
case .success(let users):
// update the UI and display the users
}
}
One Extension
In some cases it might be useful if you can just quickly check if there’s an object (.success
) or not (.failure
). Therefore we can add the following extension to our Result type:
extension Result
{
var optional: T? {
switch self {
case .success(let object): return object
default: return nil
}
}
}
2. A Model Type
Probably most iOS apps save and retrieve objects from a database
If you use Realm or CoreData as your local database, you will be provided with a superclass for your model types
(Object
in Realm and NSManagedObject
in CoreData). But if you don’t - when making calls to a REST API on a server
for example - you need to create some base type for your model yourself.
Why? Because this prevents repetitions and therefore makes your code more maintainable. Instead of having to write a
save
function for every single of your model types, they can all share the necessary properties and therefore all use
one implementation of a save
function.
How to Implement This Base Type?
The first idea might be to just create a class
named Model, from which all the other types inherit. The Model class
could contain properties like a unique id
and provide functionality like a save
function. The problem with this
approach is that most of the time, it’s better to use value-type structs
instead of reference-type classes
. I won’t
go into the details here, but you can read about value vs. reference types in another article TODO: insert link.
So how do we share properties and functionality between structs
? Inheritance is not an option here.
Using Protocol Extensions
With Swift, we can now write protocol extensions
to add functionality to protocols
. Consider the following first
implementation of our Model type:
protocol Model {
var id: String { get }
var createdAt: NSDate { get }
var updatedAt: NSDate { get set }
}
Now every model type (like customer, poduct, invoice, etc. for example) has to implement these properties. They don’t
share any functionality yet, but we’ll add that now; with the magic of Swift’s protocol extensions
:
extension Model {
func save(completion: Result<Self>) {
// save the object using the `id` property for example
}
}
We could implement other functions like update
, delete
, etc., but the point is that every model type we need to
implement now can use the functionality implemented in the Model protocol
. Hence, if something changes, we only have
to update our code in one place.
A Little Side Note
If certain model types need to use their own implementation of the save
function, you have to include the declarations
for this function right in the protocol
declaration itself, not merely in an extension
. That way, if we call save
on the respective object, it calls the correct implementation.