Lesson 49Closures
Closure Best Practices
Writing Better Closures
Follow these guidelines to write clean, efficient, and safe closure code!
Top 10 Best Practices
1
Use trailing closure syntax
Cleaner code when closure is last argument
2
Shorthand for simple closures
Use $0, $1 when intent is obvious
3
Named params for complex closures
Improves readability in long closures
4
Avoid retain cycles
Use [weak self] or [unowned self]
5
Capture values intentionally
Use capture lists [value] when needed
Memory Safety
[weak self]
Makes self optional, prevents cycles
Use when self might become nil
[unowned self]
Non-optional but not strong
Use when self won't be nil
Performance Tips
- Use
.lazyfor large collections - Prefer
reduce(into:)overreduce - Non-escaping closures are more optimized
- Break long chains for debugging
Code Organization
- Use
typealiasfor complex closure types - Prefer pure functions (no side effects)
- Extract complex closures into named functions
main.swift
// CLOSURE BEST PRACTICES
// 1. USE TRAILING CLOSURE SYNTAX
// Bad
let doubled1 = numbers.map({ $0 * 2 })
// Good
let doubled2 = numbers.map { $0 * 2 }
// 2. USE SHORTHAND FOR SIMPLE CLOSURES
// Verbose (unnecessary)
let sorted1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
return a < b
})
// Good - shorthand when clear
let sorted2 = numbers.sorted { $0 < $1 }
// Best - operator when applicable
let sorted3 = numbers.sorted(by: <)
// 3. USE NAMED PARAMETERS FOR COMPLEX CLOSURES
// Hard to read
let result = data.reduce(0) { $0 + ($1.price * Double($1.quantity)) }
// Better - named parameters
let result2 = data.reduce(0) { total, item in
total + (item.price * Double(item.quantity))
}
// 4. AVOID RETAIN CYCLES WITH [weak self]
class ViewController {
var data: String = ""
func fetchData() {
// BAD - creates retain cycle!
NetworkManager.fetch { result in
self.data = result // Strong reference to self
}
// GOOD - use [weak self]
NetworkManager.fetch { [weak self] result in
guard let self = self else { return }
self.data = result
}
// Alternative - [unowned self] if self won't be nil
NetworkManager.fetch { [unowned self] result in
self.data = result // Crashes if self is nil!
}
}
}
// 5. CAPTURE VALUES WHEN NEEDED
var counter = 0
// Captures by reference (default)
let increment = { counter += 1 }
// Captures by value (use capture list)
let capturedCounter = { [counter] in
print("Captured: \(counter)")
}
counter = 100
capturedCounter() // Prints: Captured: 0
// 6. PREFER PURE FUNCTIONS
// Bad - modifies external state
var total = 0
numbers.forEach { total += $0 }
// Good - pure function, no side effects
let total2 = numbers.reduce(0, +)
// 7. USE TYPEALIASES FOR COMPLEX CLOSURE TYPES
// Hard to read
func fetchUser(completion: (Result<User, Error>) -> Void) { }
func updateUser(completion: (Result<User, Error>) -> Void) { }
// Better - typealias
typealias UserCompletion = (Result<User, Error>) -> Void
func fetchUser(completion: UserCompletion) { }
func updateUser(completion: UserCompletion) { }
// 8. BREAK COMPLEX CHAINS
// Hard to debug
let final = data
.filter { $0.isActive }
.map { $0.value * 2 }
.filter { $0 > 100 }
.sorted()
.prefix(5)
// Better - intermediate variables for debugging
let activeItems = data.filter { $0.isActive }
let doubledValues = activeItems.map { $0.value * 2 }
let largeValues = doubledValues.filter { $0 > 100 }
let topFive = Array(largeValues.sorted().prefix(5))
// 9. USE @escaping ONLY WHEN NECESSARY
// Non-escaping (default) - more optimized
func process(handler: () -> Void) {
handler() // Called within function
}
// Escaping - only when storing/async
func store(handler: @escaping () -> Void) {
savedHandlers.append(handler)
}
// 10. CONSIDER LAZY FOR EXPENSIVE OPERATIONS
let numbers = [1, 2, 3, 4, 5]
// Eager - creates intermediate array
let eager = numbers.filter { $0 > 2 }.map { $0 * 2 }.first
// Lazy - no intermediate arrays
let lazy = numbers.lazy.filter { $0 > 2 }.map { $0 * 2 }.firstTry It Yourself!
Refactor this code using best practices: array.filter({ (x: Int) -> Bool in return x > 5 }).map({ (x: Int) -> Int in return x * 2 })