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 .lazy for large collections
  • Prefer reduce(into:) over reduce
  • Non-escaping closures are more optimized
  • Break long chains for debugging

Code Organization

  • Use typealias for 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 }.first

Try 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 })