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 do
–catch
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() } // 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 do
–catch
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.
The post A complete guide to the Swift <code>defer</code> statement appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/lHmbGhO
via Read more