If you are new to Swift, then you may be wondering what a failable initializer is and why you would ever use one. In this episode, I show you how to create a failable initializer and I hope I can convince you of their benefits. I use failable initializers in every project I work on and you may be surprised when I say that you do too.
What Is a Failable Initializer?
As the name suggests, a failable initializer is an initializer that can fail. You recognize a failable initializer by the question mark after the init
keyword. Take a look at the following example. We define a struct with name Video
. It defines two properties, position
of type TimeInterval
and duration
of type TimeInterval
.
import Foundation
struct Video {
// MARK: - Properties
let position: TimeInterval
let duration: TimeInterval
}
A struct automatically receives a memberwise initializer. Let's create a Video
object by invoking its memberwise initializer.
let video = Video(position: 15.0, duration: 60.0)
Even though the Video
struct is as simple as it gets, it introduces a number of problems. Let's define a computed property, progress
, of type Float
. The computed property returns the relative progress by dividing the value of position
by the value of duration
.
import Foundation
struct Video {
// MARK: - Properties
let position: TimeInterval
let duration: TimeInterval
// MARK: -
var progress: Float {
Float(position / duration)
}
}
The value progress
returns is equal to 0.25
.
let video = Video(position: 15.0, duration: 60.0)
video.progress // 0.25
What happens if we create a Video
object with 0.0
as the duration? Dividing a number by zero isn't a good idea. Xcode shows us that the value of progress
is infinity.
The values we passed to the initializer of the Video
struct are invalid. The duration of a video should always be greater than 0.0
and it should be greater than the position of the player.
We could validate the values we pass to the initializer, but that can become tedious if the Video
struct is used in multiple places in the codebase. It is more convenient to put the Video
struct in charge of deciding which values are valid and which values are invalid. If the values passed to the initializer are invalid, initialization should fail. In short, we need a failable initializer. It shouldn't be possible to create a Video
object with invalid values.
How to Implementation a Failable Initializer?
A failable initializer needs to meet two requirements. We add a question mark after the init
keyword and we return nil
if initialization fails. That's it. Let's implement a failable initializer for the Video
struct.
Because we implement an initializer for the Video
struct, Swift won't create a memberwise initializer for the Video
struct. That is a good thing because Video
objects should only be created using the failable initializer we are about to implement.
The initializer defines two parameters, position
of type TimeInterval
and duration
of type TimeInterval
. In the body of the initializer, we assign the values of the parameters to the properties.
import Foundation
struct Video {
// MARK: - Properties
let position: TimeInterval
let duration: TimeInterval
// MARK: -
var progress: Float {
Float(position / duration)
}
// MARK: - Initialization
init(position: TimeInterval, duration: TimeInterval) {
self.position = position
self.duration = duration
}
}
The initializer we implemented is identical to the memberwise initializer Swift creates for the Video
struct if it doesn't define an initializer. We make the initializer failable by appending a question mark to the init
keyword.
import Foundation
struct Video {
// MARK: - Properties
let position: TimeInterval
let duration: TimeInterval
// MARK: -
var progress: Float {
Float(position / duration)
}
// MARK: - Initialization
init?(position: TimeInterval, duration: TimeInterval) {
self.position = position
self.duration = duration
}
}
The initialization of the Video
object fails if the initializer returns nil
. The values that are passed to the initializer need to meet a few requirements. The value of the position
parameter needs to be greater than or equal to 0.0
and its value needs to be less than or equal to the value of the duration
parameter. We use a guard
statement and the closed range operator to validate the value of the position
parameter.
// MARK: - Initialization
init?(position: TimeInterval, duration: TimeInterval) {
guard (0...duration).contains(position) else {
return nil
}
self.position = position
self.duration = duration
}
Even though an initializer doesn't return a value, by returning nil
we communicate that the initialization of the Video
object failed.
We use the same pattern to validate the value of the duration
parameter. Because we already verified that the value of the position
parameter needs to be less than or equal to the value of the duration
parameter, we only need to verify that the value of the duration
parameter is greater than 0.0
.
// MARK: - Initialization
init?(position: TimeInterval, duration: TimeInterval) {
guard (0...duration).contains(position) else {
return nil
}
guard duration > 0.0 else {
return nil
}
self.position = position
self.duration = duration
}
We can combine the guard
statements into a single guard
statement like this.
// MARK: - Initialization
init?(position: TimeInterval, duration: TimeInterval) {
guard
duration > 0.0,
(0...duration).contains(position)
else {
return nil
}
self.position = position
self.duration = duration
}
The downside of a failable initializer is that it creates an optional value of the type it initializes. This means we need to use optional chaining to access the progress of the Video
object.
let video = Video(position: 15.0, duration: 0.0)
video?.progress // nil
Because the values we pass to the initializer are invalid, the video
constant is equal to nil
. The benefit of using a failable initializer is that we can be sure every Video
object is initialized with a valid position and duration.
Benefits of Failable Initializers
It may seem inconvenient that a failable initializer creates an optional value of the type it initializes, but that downside doesn't outweigh the benefits of failable initializers.
Data Integrity
As I mentioned earlier, we could validate the values we use to create the Video
object before we pass them to the initializer. The problem with that solution is that it doesn't scale and is prone to errors. It would result in code duplication and code that is tedious to maintain.
By implementing a failable initializer, we make the Video
struct the gatekeeper. The Video
struct defines which values are valid and which values are invalid. The benefit is that validation is encapsulated in the Video
struct. A single code path is responsible for validating the values we use to create Video
objects and we can guarantee that no Video
objects can be created with an invalid position and/or duration.
Other objects that create or work with Video
objects don't need to worry about these details. This results in clean and readable code and no code duplication.
Testability
By encapsulating these responsibilities in the Video
struct, unit testing becomes trivial. We don't need to write unit tests for every scenario the Video
struct is used in. We only need to write unit tests for the initializer of the Video
struct. Let me show you how easy that is.
I have created a project that includes the Video
struct. We use an XCTestCase
subclass to unit test the Video
struct. The testInitialization()
method unit tests the failable initializer we created earlier. Let's first unit test the happy path. We create a number of Video
objects and use the XCTAssertNotNil(_:)
function to make sure the initializer creates a valid Video
object.
import XCTest
@testable import Videos
final class VideoTests: XCTestCase {
// MARK: - Tests for Initialization
func testInitialization () throws {
XCTAssertNotNil(Video(position: 0.0, duration: 10.0))
XCTAssertNotNil(Video(position: 1.0, duration: 10.0))
XCTAssertNotNil(Video(position: 10.0, duration: 10.0))
}
}
Because we are unit testing a failable initializer, we also need to write unit tests for the unhappy path. The idea is similar. We create a number of Video
objects and use the XCTAssertNil(_:)
function to make sure the initialization fails if the values we pass to the initializer are invalid.
import XCTest
@testable import Videos
final class VideoTests: XCTestCase {
// MARK: - Tests for Initialization
func testInitialization () throws {
XCTAssertNotNil(Video(position: 0.0, duration: 10.0))
XCTAssertNotNil(Video(position: 1.0, duration: 10.0))
XCTAssertNotNil(Video(position: 10.0, duration: 10.0))
XCTAssertNil(Video(position: 0.0, duration: 0.0))
XCTAssertNil(Video(position: 11.0, duration: 10.0))
XCTAssertNil(Video(position: -1.0, duration: 10.0))
XCTAssertNil(Video(position: 0.0, duration: -10.0))
}
}
It is important to verify that the unit tests cover every possible scenario. Xcode's built-in code coverage support can help you with this, but it isn't a silver bullet.
Common Examples of Failable Initializers
Even if failable initializers are new to you, chances are that you have already used failable initializers. There are plenty of types that define one or more failable initializers.
Take a look at this example. We define a constant and assign the string 5
to it. We can convert the string to an integer by passing the string to an initializer Int
defines. That initializer is failable and that isn't surprising. The conversion from a string to an integer only succeeds if the string can be interpreted as an integer.
let number = "5"
Int(number) // 5
The initialization fails if we pass a string to the initializer that cannot be converted to an integer.
let nan = "abcdef"
Int(nan) // nil
Classes, structs, and enums can define failable initializers. Swift automatically generates a failable initializer for enums with raw values. The failable initializer accepts the raw value as its only argument. Take a look at this example. We define an enum, ImageFormat
, that defines three cases jpg
, png
, and svg
. The raw values are of type String
.
enum ImageFormat: String {
// MARK: - Cases
case jpg
case png
case svg
}
We can create an ImageFormat
object by passing a string to the failable initializer Swift automatically generates for the ImageFormat
enum. Notice that the casing of the string we pass to the initializer matters.
ImageFormat(rawValue: "png") // png
ImageFormat(rawValue: "PNG") // nil
What's Next?
Failable initializers have many benefits and I use them frequently in the projects I work on. Keep in mind that initialization can fail for many reasons, not only invalid parameter values. Failable initializers can reduce code duplication, prevent unexpected errors, and improve the testability of a project. The only downside is that a failable initializer creates an optional value of the type it initializes.