The basics of Swift are easy to learn, but the language has evolved significantly over the past few years. The more I use the language, the more I learn about it and discover its lesser known features. In this episode, I would like to share a handful of tips and patterns I have picked up over the years. They are easy to implement and have the potential to transform messy code into elegant, readable code.

Switching on a Boolean

In Swift, the condition of an if statement needs to evaluate to true or false and that is why most developers use an if statement if they need to evaluate the value of a boolean.

To make the code I write more readable, I sometimes use a switch statement instead of an if statement to evaluate the value of a boolean. Take a look at this example. We use an if statement to evaluate the value of isActive. This is textbook Swift. Right?

if isActive {
    worker.deactivate()
}  else {
    worker.activate()
}

In some scenarios, it is more convenient or easier on the eyes if you replace the if statement with a switch statement.

switch isActive {
case true:
    worker.deactivate()
case false:
    worker.activate()
}

The if statement is still my default for evaluating the value of a boolean, but I sometimes use a switch statement if I feel it improves the readability of the implementation. Give it a try. You may like it.

Code Hygiene

Object literals have the tendency to clutter a project and I am not only referring to string literals. We use number literals for animations, sizing and positioning views, and many other seemingly trivial use cases. The problem is that those number literals can introduce inconsistencies and code duplication. I would like to share two strategies for managing and reducing number literals in your projects.

The first option may seem odd at first. I use it frequently and it works very well. In this example, we define the size and position of several subviews in code using SnapKit. SnapKit is a lightweight library for working with Auto Layout in code. We define the size and position of the views using number literals. You may notice that several number literals appear more than once. This isn't a major problem, but it is easy to avoid this problem.

private func setupView() {
    // Table View
    tableView.snp.makeConstraints {
        $0.edges.equalToSuperview()
    }

    // Email Text Field
    emailTextField.snp.makeConstraints {
        $0.width.equalTo(200.0)
        $0.height.equalTo(30.0)
    }

    // Password Text Field
    passwordTextField.snp.makeConstraints {
        $0.width.equalTo(200.0)
        $0.height.equalTo(30.0)
    }

    // Title Label
    titleLabel.snp.makeConstraints {
        $0.width.equalTo(200.0)
        $0.leading.equalToSuperview().offset(20.0)
        $0.trailing.equalToSuperview().offset(-20.0)
    }
}

We create a private extension for the BillingViewController class at the bottom of the file and define an enum with name Layout. We create an enum for each view or collection of views we want to size and position. The enum defines one or more static properties of type CGFloat. We use these properties instead of number literals.

private extension BillingViewController {

    enum Layout {

        enum TextField {
            static let width: CGFloat = 200.0
            static let height: CGFloat = 30.0
        }

        enum TitleLabel {
            static let leading: CGFloat = 20.0
            static let trailing: CGFloat = -20.0
        }

    }

}

The benefits are obvious. The implementation of the setupView() method no longer contains number literals and that makes the code you write more readable. Refactoring also becomes painless because you immediately see which values are used in several places. Autocompletion is another benefit you get for free. This technique is simple but surprisingly effective. The only drawback is that you need to write a few more lines of code.

private func setupView() {
    // Table View
    tableView.snp.makeConstraints {
        $0.edges.equalToSuperview()
    }

    // Email Text Field
    emailTextField.snp.makeConstraints {
        $0.width.equalTo(Layout.TextField.width)
        $0.height.equalTo(Layout.TextField.height)
    }

    // Password Text Field
    passwordTextField.snp.makeConstraints {
        $0.width.equalTo(Layout.TextField.width)
        $0.height.equalTo(Layout.TextField.height)
    }

    // Title Label
    titleLabel.snp.makeConstraints {
        $0.leading.equalToSuperview().offset(Layout.TitleLabel.leading)
        $0.trailing.equalToSuperview().offset(Layout.TitleLabel.trailing)
    }
}

The second technique I would like to show you applies to number literals that are used in various, unrelated places of the project. The duration of an animation is a common example. We often use the same duration for most animations in a project and that means we use the same number literal in various, unrelated places of the project. Keeping the values synchronized across the project can becomes tedious and prone to mistakes as the project grows. This is easy to resolve, though.

func hidePaymentView() {
    UIView.animate(withDuration: 0.5) {
        // Hide Payment View
        self.paymentView.alpha = 0.0
    }
}

The first solution is simple. We create an extension for the TimeInterval struct and define a static, constant property, animationDuration, of type TimeInterval. We assign a value to the animationDuration property and access the value through the TimeInterval struct. You can optionally use an enum to namespace the static, constant property to avoid naming collisions.

extension TimeInterval {

    static let animationDuration: TimeInterval = 0.5

}

As with the previous solution, code completion kicks in and you don't run the risk of making a typo.

func hidePaymentView() {
    UIView.animate(withDuration: .animationDuration) {
        // Hide Payment View
        self.paymentView.alpha = 0.0
    }
}

Another more advanced option is the use of an enum. We create an enum with name AnimationDuration and define several cases, slow, normal, and fast. The raw value of the enum is TimeInterval.

enum AnimationDuration: TimeInterval {

    // MARK: - Cases

    case slow = 1.5
    case normal = 0.5
    case fast = 0.25

}

The code you write becomes more readable and typos are a thing of the past.

func hidePaymentView() {
    UIView.animate(withDuration: AnimationDuration.normal.rawValue) {
        // Hide Payment View
        self.paymentView.alpha = 0.0
    }
}

Enums and Properties

Enums can't have stored properties, but they can have computed properties. This is a feature you should take advantage of whenever possible. Let's say we have an enum, Environment, that defines three cases, development, staging, and production.

enum Environment {

    // MARK: - Cases

    case development
    case staging
    case production

}

You could use the enum as is, but that isn't what I recommend. The enum should expose an API that makes it easy to work with the Environment enum. We start simple by defining three computed properties of type Bool, isDevelopment, isStaging, and isProduction.

enum Environment {

    // MARK: - Cases

    case development
    case staging
    case production

    // MARK: - Properties

    var isDevelopment: Bool { return self == .development }
    var isStaging: Bool { return self == .staging }
    var isProduction: Bool { return self == .production }

}

I usually define these computed properties when I need them. They make your code more readable and more concise. This is just a start, though. We can define another computed property, baseUrl, of type URL, that returns the base URL of the backend the application communicates with. This avoids code duplication and it makes sense to make the Environment enum the owner of this information.

enum Environment {

    // MARK: - Cases

    case development
    case staging
    case production

    // MARK: - Properties

    var isDevelopment: Bool { return self == .development }
    var isStaging: Bool { return self == .staging }
    var isProduction: Bool { return self == .production }

    // MARK: -

    var baseUrl: URL {
        switch self {
        case .development:
            return URL(string: "http://0.0.0.0:3000")!
        case .staging:
            return URL(string: "https://staging.cocoacasts.com")!
        case .production:
            return URL(string: "https://cocoacasts.com")!
        }
    }

}

The use of computed properties makes even more sense when you're working with an enum with associated values. The DownloadState enum defines three cases, notDownloaded, downloading, and downloaded. The downloading case has an associated value of type Float. It represents the progress of the download operation.

enum DownloadState {

    // MARK: - Cases

    case notDownloaded
    case downloading(Float)
    case downloaded

}

It isn't elegant to compare enums using an if statement if the enum has cases with associated values. Without computed properties, your code can become messy very quickly. Computed properties resolve this problem elegantly. The solution requires a few additional lines of code since we are dealing with an enum with associated values.

enum DownloadState {

    // MARK: - Cases

    case notDownloaded
    case downloading(Float)
    case downloaded

    // MARK: - Properties

    var isNotDownloaded: Bool {
        switch self {
        case .notDownloaded:
            return true
        default:
            return false
        }
    }

    var isDownloading: Bool {
        switch self {
        case .downloading:
            return true
        default:
            return false
        }
    }

    var isDownloaded: Bool {
        switch self {
        case .downloaded:
            return true
        default:
            return false
        }
    }

}

Enums and computed properties are a powerful combination and I recommend using them whenever it makes sense.

What's Next?

Swift continues to evolve and I try to improve the code I write every single day. It is satisfying to clean up a complex piece of code, but it also improves the readability of the code you write and the maintainability of the project.