Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

A complete guide to the Swift defer statement

4 min read 1127

Swift Logo

The Swift defer statement is useful for cases where we need something done — no matter what — before exiting the scope. For example, defer can be handy when cleanup actions are performed multiple times, like closing a file or locking a lock, before exiting the scope. Simply put, the Swift defer statement provides good housekeeping.

The defer keyword was introduced in the Swift language back in 2016, but it can be difficult to find good examples as it seems to be used sparingly in projects. The basic snippet provided in the Swift documentation isn’t very helpful either.

In an effort to provide more clarity on this topic, this article will examine Swift’s defer statement and syntax. We’ll also look at several real-world use cases:

Syntax

When we use the defer keyword, the statements we provide inside defer are executed at the end of a scope, like in a method. They are executed every time before exiting a scope, even if an error is thrown. Note that the defer statement only executes when the current scope is exiting, which may not be the same as when the function returns.

The defer keyword may be defined inside of a scope. In this example, it is defined in a function:

// This will always execute before exiting the scope
defer {
    // perform some cleanup operation here
    // statements
}

// rest of the statements

In this example, the defer keyword is defined inside a docatch block:

do {

// This will always execute before exiting the scope
    defer {
        // perform some cleanup operation here
        // statements
    }

    // rest of the statements that may throw error
    let result = try await fetchData()
} catch {
    // Handle errors here
}

Even in cases where an error is thrown or where there are lots of cleanup statements, the defer statement will still allow us to execute code right before the scope is exited. defer helps keep the code more readable and maintainable.

Now, let’s look at a few examples using the defer statement.

Locking

The most common use case for the Swift defer statement is to unlock a lock. defer can ensure this state is updated even if the code has multiple paths. This removes any worry about forgetting to unlock, which could result in a memory leak or a deadlock.

The below code locks the lock, adds the content from the parameters to the given array, and unlocks the lock in the defer statements. In this example, the lock is always unlocked before transferring the program control to another method.

We made a custom demo for .
No really. Click here to check it out.

func append(_ elements: [Element]) {
    lock.lock()

    defer {
        lock.unlock()
    }

    array.append(contentsOf: elements)
}

Networking

While performing network requests, it’s not unusual to have to handle errors, bad server responses, or missing data. Using a defer block when we call the completion handler will help ensure that we do not miss any of these errors.

func fetchQuotes(from url: URL, completion: @escaping (Result<[Quote], Error>) -> ()) {
    var result: Result<[Quote], Error>

    defer {
        completion(result)
    }

    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            result = .failure(error)
        }

        guard let response = response else {
            result = .failure(URLError(.badServerResponse))
        }

        guard let data = data else {
            result = .failure(QuoteFetchError.missingData)
        }

        result = .success(quoteResponse(for: data))
    }
    task.resume()
}

Updating layout

With the setNeedsLayout() method, we can use defer to update the view. It may be necessary to call this method multiple times. By using defer , there’s no worry about forgetting to execute the setNeedsLayout() method. defer will ensure that the method is always executed before exiting the scope.

func reloadAuthorsData() {
    defer {
        self.setNeedsLayout()
    }

    removeAllViews()

    guard let dataSource = quotingDataSource else { return }

    let itemsCount = dataSource.numberOfItems(in: self)

    for index in itemsCount.indices {
        let view: AuthorView = getViewForIndex(index)

        addSubview(view)

        authorViews.append(view)
    }
}

If we are updating the constraints programmatically, we can put layoutIfNeeded() inside the defer statement. This will enable us to update the constraints without any worry of forgetting to call layoutIfNeeded():

func updateViewContstraints() {
    defer {
        self.layoutIfNeeded()
    }

    // One conditional statement to check for constraint and can return early

    // Another statement to update another constraint
}

Loading indicator

The defer statement may be used with the loading indicator. In this case, the defer statement will ensure that the loading indicator executes even if there is an error, and it will not have to be repeated for any other condition in the future:

func performLogin() {
    shouldShowProgressView = true

    defer {
        shouldShowProgressView = false
    }

    do {
        let _ = try await LoginManager.performLogin()

        DispatchQueue.main.async {
            self.coordinator?.successfulLogin()
        }

    } catch {
        let error = error
        showErrorMessage = true
    }
}

Committing changes

The defer statement may be used to commit all changes made using CATransaction. This ensures that the animation transaction will always be committed even if there is conditional code after the defer statement that returns early.

Let’s say we want to update the properties of a UIButton’s layer and then add animation to update the UIButton’s frame. We can do so by calling the commit() method inside the defer statement:

CATransaction.begin()

defer {
   CATransaction.commit()
}

// Configurations
CATransaction.setAnimationDuration(0.5)
button.layer.opacity = 0.2
button.layer.backgroundColor = UIColor.green.cgColor
button.layer.cornerRadius = 16

// View and layer animation statements

A similar use case is with AVCaptureSession. We call commitConfiguration() at the end to commit configuration changes. However, many docatch statements result in an early exit when an error is thrown. By calling this method inside the defer statement, we ensure the configuration changes are committed before the exit.

func setupCaptureSession() {
    cameraSession.beginConfiguration()

    defer {
        cameraSession.commitConfiguration()
    }

    // Statement to check for device input, and return if there is any error
    do {
        deviceInput = try AVCaptureDeviceInput(device: device)
    } catch let error {
        print(error.localizedDescription)
        return
    }

    // Statements to update the cameraSession
    cameraSession.addInput(deviceInput)
}

Unit testing

Asynchronous code can be difficult to test. We can use the defer statement so that we do not forget to wait until the asynchronous test meets the expectation or times out.

func testQuotesListShouldNotBeEmptyy() {
    var quoteList: [Quote] = []

    let expectation = XCTestExpectation(description: #function)

    defer {
        wait(for: [expectation], timeout: 2.0)
    }

    QuoteKit.fetchQuotes { result in
        switch result {
            case .success(let quotes):
                quoteList = quote
                expectation.fulfill()
            case .failure(let error):
                XCTFail("Expected quotes list, but failed \(error).")
        }
    }
    XCTAssert(quoteList.count > 0, "quotes list is empty")
}

Similarly, if multiple guard statements are present while we are checking for the response, we can use the defer statement with the fulfill() method to ensure the asynchronous test fulfills the expectation:

defer {
    expectation.fulfill()
}

// Many guard statements where we call expectation.fulfill() individually.

Conclusion

Swift defer statements are powerful for cleaning up resources and improving code. The defer statement will keep your iOS application code running smoothly, even if a team member updates a method or adds a conditional statement. defer executes no matter how we exit and future proofs projects from changes that may alter the scope flow, reducing the possibility of an error.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Leave a Reply