Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
116: Defer Statements for Cleanup
When I first started working with defer in Swift, I fell into a trap that I see a lot of developers fall into today. I assumed that defer was essentially a "catch" or "error" block—something that only triggered if the function failed or threw an exception. I treated it like a safety net for when things went sideways.
"Defer only runs when an error occurs"
That assumption is completely wrong. If you write your cleanup logic assuming defer is only for failures, you're going to be very surprised when your resources start closing in the middle of a perfectly successful execution. Let me show you what I mean.
func processUserPreferences() {
print("Opening preferences file...")
defer {
print("Closing preferences file.")
}
print("Reading settings...")
if true {
print("Found a conflict, returning early!")
return
}
print("This line will never be reached.")
}
processUserPreferences()
// Output:
// Opening preferences file...
// Reading settings...
// Found a conflict, returning early!
// Closing preferences file.
Notice that nothing "went wrong" here. We didn't throw an error; we simply decided to return early. Yet, the defer block executed anyway. defer doesn't care why you are leaving the scope—whether it's a normal return, a throw, or simply reaching the closing brace of the function—it just ensures that the code inside it runs before the scope is officially destroyed.
Guaranteeing cleanup regardless of the exit path
The real power of defer is that it allows you to place your cleanup logic immediately next to your initialization logic. In the old days of C or Java, you'd open a file at the top of a function and then have to remember to call close() at every single return point in that function. If you had five different if/else blocks returning early, you had to write close() five times. It was a nightmare for maintenance.
With defer, you "schedule" the cleanup the moment you acquire the resource. I personally find this much safer because I don't have to scroll through 50 lines of logic to make sure I didn't forget to release a lock or close a handle.
func uploadLogFile(url: URL) throws {
let fileHandle = try FileHandle(forReadingFrom: url)
// I schedule the closure right here, so I can forget about it
defer {
fileHandle.closeFile()
print("File handle released.")
}
let data = fileHandle.readDataToEndOfFile()
if data.isEmpty {
throw LogError.emptyFile // defer still runs!
}
try networkClient.upload(data) // defer still runs!
}
One detail that often trips people up: if you have multiple defer statements in one block, they execute in reverse order (Last-In, First-Out). Think of it like a stack of plates. The last cleanup task you schedule is the first one that gets executed. This is actually incredibly useful when your resources depend on each other—for example, if you open a database connection and then start a transaction, you want to commit/close the transaction before you close the connection.
📋 Practical Task
Exercise: Managing a Mock Database Connection
You are building a data migration tool. You need to create a function called migrateUserData() that simulates interacting with a database. To make this realistic, you should simulate the following sequence:
- Print "Connecting to Database..."
- Use a
deferblock to print "Database connection closed." - Print "Starting Transaction..."
- Use a second
deferblock to print "Transaction committed/rolled back." - Simulate a conditional failure: Create a boolean variable
shouldFail. If it is true, print "Migration failed!" and return from the function early. - If it doesn't fail, print "Migration completed successfully!"
Goal: Verify that both cleanup messages print in the correct reverse order (Transaction first, then Connection) regardless of whether shouldFail is true or false.
There are no comments for now.