A complete guide to the Swift defer statement - LogRocket Blog (2023)

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:

  • Locking
  • Networking
  • Updating layout
  • Loading indicator
  • Committing changes
  • Unit testing

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 scopedefer { // 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.

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()}// ConfigurationsCATransaction.setAnimationDuration(0.5)button.layer.opacity = 0.2button.layer.backgroundColor = UIColor.green.cgColorbutton.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.

Over 200k developers use LogRocket to create better digital experiencesLearn more →
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.

LogRocket: Full visibility into your web and mobile 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.

Try it for free.

Top Articles
Latest Posts
Article information

Author: Tish Haag

Last Updated: 12/26/2022

Views: 6020

Rating: 4.7 / 5 (67 voted)

Reviews: 82% of readers found this page helpful

Author information

Name: Tish Haag

Birthday: 1999-11-18

Address: 30256 Tara Expressway, Kutchburgh, VT 92892-0078

Phone: +4215847628708

Job: Internal Consulting Engineer

Hobby: Roller skating, Roller skating, Kayaking, Flying, Graffiti, Ghost hunting, scrapbook

Introduction: My name is Tish Haag, I am a excited, delightful, curious, beautiful, agreeable, enchanting, fancy person who loves writing and wants to share my knowledge and understanding with you.