Do You Know Best Use Case Of Swift defer statement
Behaviour & Common Use Case With Examples
A defer
statement is used for executing code just before transferring program control outside of the current block of code that the defer
statement appears in. It means, defer
statement is used to execute a piece of code exactly before the execution departs the current block of code. The current block of code might be a method
, an if
statement, a do
block, a loop
or a switch
case.
The statements within the defer
are executed no matter how program control is transferred, which makes it perfect for running clean-up code like closing files, closing connections to a database, manually allocated memory or releasing other system resources.
A defer
statements block is :
defer {
// Statements
}
Use cases of defer statement
There are many situations in the project where we can use defer
statements. Following are few of the real-world use cases in iOS swift development for perfect use of defer
statement doing clean-up code like closing files, closing connections to a database, manually allocated memory or releasing other system resources.
1. Unlock Lock (NSLock)
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.
var balance = 1000.0 // shared resource
let lock = NSLock()struct Bank { func withdraw(amount: Double) {
lock.lock()// defer will ensure lock is unlock before control is transferred
defer {
lock.unlock()
}
if balance > amount {
balance -= amount
print("balance is \(balance)")
} else {
print("insufficent balance")
}
}
}
2. Cleanup Manual Memory Allocation
If you access C APIs and create CoreFoundation objects, allocate memory, or read and write files with fopen, FileHandle. You can make sure to use defer statement to clean up properly in all cases with dealloc, free, close, deallocate, closeFile.
a) UnsafeMutablePointer:- UnsafeMutablePointer
provides no automated memory management or alignment guarantees. You are responsible for handling the life cycle of any memory you work with through unsafe pointers to avoid leaks or undefined behaviour. Use defer block to deallocate it.
do { // allocate memory let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count) // deallocate memory using defer defer {
pointer.deinitialize(count: count)
pointer.deallocate()
} pointer.pointee = 4
pointer.advanced(by: 1).pointee = 2
let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
for bufPointer in bufferPointer.enumerated() {
print("value \(bufPointer)")
}
}
b) UIGraphicsEndImageContext:- If we are using to creates a bitmap-based graphics context using UIGraphicsBeginImageContextWithOptions with the specified options. By calling UIGraphicsEndImageContext inside the defer
statement, we ensure the graphics context end before the exit.
private func drawRoundedImageWithCOrner() -> UIImage? {
let bounds = CGRect(x: 0, y: 0, width:200, height: 200)
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0) defer {
UIGraphicsEndImageContext()
} UIColor.black.setFill()
let path = UIBezierPath(roundedRect: bounds,
byRoundingCorners: UIRectCorner.allCorners,
cornerRadii: CGSize(width: 10, height: 10))
path.addClip()
UIRectFill(bounds)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
3. Update View Layout
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().
Similar way setNeedsLayout()
method can be used inside defer
to update the view which will ensure that the method is always executed before exiting the scope.
private func updateViewContstraints(_ blueView: UIView) {
defer {
self.view.layoutIfNeeded()
}
NSLayoutConstraint(item: blueView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 80.0).isActive = true
}
4. Committing Changes
a) CATransaction:- 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 a conditional code after the defer
statement that returns early.
private func shakeAnimateView(_ redView: UIView){
CATransaction.begin() defer {
CATransaction.commit()
} let animate: CABasicAnimation = CABasicAnimation(keyPath: "position")
animate.duration = 0.1
animate.repeatCount = 21
animate.autoreverses = true let from_point:CGPoint = CGPointMake(redView.center.x - 5, redView.center.y)
let from_value:NSValue = NSValue(cgPoint: from_point)
let to_point:CGPoint = CGPointMake(redView.center.x + 5, redView.center.y)
let to_value:NSValue = NSValue(cgPoint: to_point)
animate.fromValue = from_value
animate.toValue = to_value
redView.layer.add(animate, forKey: "position")
}
b) AVCaptureSession commitConfiguration:- By calling commitConfiguration()
inside the defer
statement, we ensure the AVCaptureSession configuration changes are committed before the exit. There may be many do
–catch
statements result in an early exit when an error is thrown
private func setupSessionInput(for position: AVCaptureDevice.Position) {
var captureSession: AVCaptureSession = AVCaptureSession()
do {
guard let device = return AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,for: AVMediaType.video, position: position) else {
}let input = try AVCaptureDeviceInput(device: device)
try device.lockForConfiguration()
captureSession.beginConfiguration()
defer {
captureSession.commitConfiguration()
}device.autoFocusRangeRestriction = .near
device.focusMode = .continuousAutoFocus
device.exposureMode = .continuousAutoExposureif let currentInput = captureSession.inputs.filter({$0 is AVCaptureDeviceInput}).first {
captureSession.removeInput(currentInput)
}captureSession.usesApplicationAudioSession = false
captureSession.addInput(input)
device.unlockForConfiguration()} catch(let error) {
print(error.localizedDescription)
return
}
}
Difference Between Defer And Return
The defer
statement is executed no matter how you exit and it works no matter you exit the current scope, which might not involve return
. defer
works for a function body, a while block, an if, a do block, and so on. There might be more than one return
in your method, or you might throw an error, or have a break
, or when you just reach the last line of the scope, the defer
is executed in every possible case. The defer
statement is executed after the return.
struct checkCounter: Sequence, IteratorProtocol {
var count: Int
mutating func next() -> Int? {
if count == 0 {
return nil
}
else {
defer { count -= 1 }
return count
}
}
}
In above code next() function, first return count
and then decrement it count value by 1.
Defer With Fallthrough
The fallthrough
keyword doesn’t check the case conditions for the switch
case that it causes execution to fall into. The fallthrough
keyword simply causes code execution to move directly to the statements inside the next case (or default
case) block, as in C’s standard switch
statement behaviour. In the below code snippet case:
block is the end of the scope of that block and fallthrough
statement doesn't maintain the scope of a switch so case 2 will also execute.
defer {
print("outer defer")
}
let counter = 1
switch counter {
case 0:
print("case 0")
case 1:
print("case 1")
defer { print("case 1 defer") }
fallthrough
case 2:
print("case 2")
default:
print("default")
}
// Output
case 1
case 1 defer
case 2
Using Multiple Defer Statements
If multiple
defer
statements appear in the same scope, eachdefer
statement is actually executed in a first-in-last-out (FILO) order. That means, the lastdefer
statement in the source code order executes first and the firstdefer
statement executes last.
We can see few examples below to understand the multiple defer
statements:
--------------------------------------------------------------------
Example 1
--------------------------------------------------------------------
func testDefer() {
defer { print("First defer") }
defer { print("Second defer") }
defer { print("Third defer") }
print("Do some work here")
}
// Output
Do some work here
Third defer
Second defer
First defer--------------------------------------------------------------------
Example 2
--------------------------------------------------------------------
func multipleDefer() {
defer {
print("Defer 1")
defer {
print("defer 2")
}
defer {
print("Defer 3")
defer {
print("Defer 4")
defer {
print("Defer 5")
}
}
defer {
print("Defer 6")
}
}
}
}// Output
Defer 1
Defer 3
Defer 6
Defer 4
Defer 5
defer 2
Limitations Of Defer
Any Swift statement can be included in the body of a defer
statement but there is one exception. The defer
statement can not contain any code that would transfer control out of the defer
statement. This means that defer
can’t include statements such as the break
or return
statements or throw any errors within the body of the defer
statement as these statements would cause execution of that defer
block to be immediately terminated.
let isDeferExecute: Bool?defer {
guard let _ = isDeferExecute else {
// ERROR 'return' cannot transfer control out of a defer statement
return
}
print("check defer with return")
Thanks for reading, you can follow me on Medium for updated articles.
If you have any comments, questions, or recommendations feel free to post them in the comment section below! 👇 and please share and give claps 👏👏 if you liked this post.