Best examples of practical examples of using closures in Swift

If you write Swift for more than a week, you bump into closures. They’re everywhere: in UIKit callbacks, SwiftUI views, async networking, and collection transformations. But the real learning happens when you see **examples of practical examples of using closures in Swift** that mirror what you actually do in production code. In this guide, we’ll walk through real examples that show closures doing actual work: sorting data, handling completion callbacks, capturing state, avoiding retain cycles, and powering async/await bridges. Instead of toy snippets, you’ll see how a closure shows up in networking, animations, SwiftUI views, and background tasks. Along the way, I’ll call out common pitfalls (like capture lists and memory leaks) and how modern Swift (up through Swift 5.10 in 2024) encourages closure-heavy APIs. If you’ve ever stared at a closure signature and thought, “Okay, but how would I use this in a real app?”, this article is for you.
Written by
Jamie
Published
Updated

Real examples of practical examples of using closures in Swift

Let’s start with where closures show up constantly: sorting, filtering, and mapping collections. These are some of the best examples of using closures in Swift because they’re short, readable, and you’ll use them every day.

struct User {
    let id: Int
    let name: String
    let lastLogin: Date
}

var users: [User] = loadUsersFromDisk()

// Sort by most recent login using a closure
let sortedByLastLogin = users.sorted { lhs, rhs in
    lhs.lastLogin > rhs.lastLogin
}

// Filter active users based on a closure predicate
let activeUsers = users.filter { user in
    user.lastLogin > Date().addingTimeInterval(-7 * 24 * 60 * 60) // last 7 days
}

// Map to just names using a closure
let userNames = users.map { $0.name }

These examples of practical examples of using closures in Swift show three patterns you’ll see constantly:

  • Passing a closure as a comparator (sorted)
  • Passing a closure as a predicate (filter)
  • Passing a closure as a transformer (map)

They’re not just syntactic sugar. These closure-based APIs encourage you to write small, focused bits of logic and pass them around like values.


Networking and async work: examples of completion closures in Swift

Networking is where many people first meet completion handlers. A classic example of practical examples of using closures in Swift is a function that fetches data and calls a closure when it’s done.

enum NetworkError: Error {
    case invalidURL
    case requestFailed
}

func fetchUsers(completion: @escaping (Result<[User], NetworkError>) -> Void) {
    guard let url = URL(string: "https://example.com/api/users") else {
        completion(.failure(.invalidURL))
        return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let _ = error {
            completion(.failure(.requestFailed))
            return
        }

        guard let data = data else {
            completion(.failure(.requestFailed))
            return
        }

        do {
            let users = try JSONDecoder().decode([User].self, from: data)
            completion(.success(users))
        } catch {
            completion(.failure(.requestFailed))
        }
    }.resume()
}

This is a very typical example of using closures in Swift networking code:

  • The completion closure captures the result of the async work.
  • The function returns immediately, but the closure runs later.
  • @escaping tells Swift that the closure outlives the function call.

With Swift’s async/await (stabilized in Swift 5.5 and widely used by 2024), you often bridge older closure-based APIs into async functions.

func fetchUsersAsync() async throws -> [User] {
    try await withCheckedThrowingContinuation { continuation in
        fetchUsers { result in
            switch result {
            case .success(let users):
                continuation.resume(returning: users)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

This is one of the best examples of practical examples of using closures in Swift in 2024-era code: closures act as the glue between legacy completion handlers and modern async/await.


UI callbacks: examples include button actions and animations

UIKit and SwiftUI are full of closure-based APIs. These UI callbacks are some of the clearest real examples of practical examples of using closures in Swift.

UIButton actions with closures (UIKit)

Instead of using target–action with selectors, many teams wrap button taps in closures:

final class ClosureButton: UIButton {
    private var onTap: (() -> Void)?

    func setOnTap(_ handler: @escaping () -> Void) {
        onTap = handler
        addTarget(self, action: #selector(handleTap), for: .touchUpInside)
    }

    @objc private func handleTap() {
        onTap?()
    }
}

// Usage
let button = ClosureButton(type: .system)
button.setOnTap { [weak self] in
    self?.showDetailScreen()
}

Here the closure becomes the action. You capture self weakly to avoid retain cycles (more on that in a moment).

UIView animations

Apple’s animation APIs are textbook examples of practical examples of using closures in Swift:

UIView.animate(withDuration: 0.3, animations: {
    view.alpha = 0
}, completion: { finished in
    if finished {
        view.removeFromSuperview()
    }
})

Two closures:

  • The animations closure describes what changes over time.
  • The completion closure runs when the animation ends.

This pattern—“describe behavior now, run it later”—is exactly why closures are so widely used in UI frameworks.


SwiftUI: closures everywhere (view builders and actions)

SwiftUI is basically a masterclass in examples of practical examples of using closures in Swift. View builders, event handlers, and modifiers all lean on closures.

struct UserListView: View {
    let users: [User]
    let onSelectUser: (User) -> Void

    var body: some View {
        List(users, id: \ .id) { user in
            Button(user.name) {
                onSelectUser(user)
            }
        }
    }
}

struct ContentView: View {
    @State private var selectedUser: User?

    var body: some View {
        UserListView(users: loadUsersFromDisk()) { user in
            selectedUser = user
        }
    }
}

Here are a few interesting closure-related details:

  • List takes a closure that builds each row.
  • Button takes a closure for its action.
  • UserListView itself exposes a closure (onSelectUser) so parents can inject behavior.

This is a real example of how closures enable inversion of control: the child view doesn’t know what happens when a user is selected; it just calls the closure.


Capturing values and avoiding retain cycles

Closures capture values from their surrounding scope. That’s powerful, but it’s also how you accidentally create memory leaks.

Consider this example of practical examples of using closures in Swift where a view model owns a service that has a callback:

final class UserViewModel {
    private let service: UserService

    init(service: UserService) {
        self.service = service
    }

    func startListening() {
        service.onUserUpdate = { [weak self] users in
            self?.handleUpdate(users)
        }
    }

    private func handleUpdate(_ users: [User]) {
        // update state
    }
}

final class UserService {
    var onUserUpdate: (([User]) -> Void)?
}

Without [weak self], the closure would capture self strongly, and if UserService also holds UserViewModel strongly, you’d get a retain cycle.

This pattern—storing a closure property and using a capture list—is one of the most important real examples of how closures interact with ARC. Apple’s Swift Programming Language Guide has an in-depth section on closure capture semantics.


Background work and GCD: closures as work units

Grand Central Dispatch (GCD) is another area where the best examples of using closures in Swift appear. Dispatch queues use closures as the unit of work.

let backgroundQueue = DispatchQueue(label: "com.example.background", qos: .userInitiated)

func processLargeFile(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
    backgroundQueue.async {
        do {
            let data = try Data(contentsOf: url)
            DispatchQueue.main.async {
                completion(.success(data))
            }
        } catch {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
}

Here you see two layers of closure usage:

  • A closure passed to backgroundQueue.async to run work off the main thread.
  • A closure passed as completion to notify the caller.

This is a classic example of practical examples of using closures in Swift to separate where work runs from what the work actually is.


Functional pipelines: transforming data with chained closures

Swift’s standard library encourages a functional style. Chaining map, filter, and reduce is one of the cleanest examples of practical examples of using closures in Swift for data processing.

struct Order {
    let id: Int
    let total: Double
    let isRefunded: Bool
}

let orders: [Order] = loadOrders()

let totalRevenue = orders
    .filter { !$0.isRefunded }      // closure as predicate
    .map { $0.total }               // closure as transformer
    .reduce(0.0, +)                 // closure as accumulator

You can contrast this with a more imperative version using loops and if statements. The closure-based pipeline is shorter, often easier to test, and reads like a description of the business rule.

For more on this style of thinking, many university courses (for example, functional programming courses at MIT) discuss higher-order functions, which is exactly what Swift’s collection methods are.


Escaping vs non-escaping closures in modern Swift

By 2024, the Swift compiler is very good at inferring when a closure is escaping. Still, understanding the difference is important, and examples of practical examples of using closures in Swift make it easier to see.

A non-escaping closure is used only during the function’s execution:

func performTwice(_ operation: () -> Void) {
    operation()
    operation()
}

performTwice {
    print("Hello")
}

Here, operation doesn’t outlive performTwice, so no @escaping is needed.

Compare that to this example of an escaping closure:

var storedHandlers: [() -> Void] = []

func storeHandler(_ handler: @escaping () -> Void) {
    storedHandlers.append(handler)
}

storeHandler {
    print("I’ll run later")
}

The closure is stored for later use, so it must be marked @escaping. Many of the earlier examples of practical examples of using closures in Swift—networking, GCD, callbacks—are escaping closures by nature.

Apple’s official docs on @escaping are worth reading if you want the language-lawyer details.


Timers and scheduled work: closures as delayed actions

Timers are another real example of practical examples of using closures in Swift. Modern APIs let you schedule repeating work with a closure instead of selectors.

final class HeartbeatLogger {
    private var timer: Timer?

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
            self?.logHeartbeat()
        }
    }

    func stop() {
        timer?.invalidate()
        timer = nil
    }

    private func logHeartbeat() {
        print("Heartbeat at \(Date())")
    }
}

This pattern shows up in analytics, health tracking, and background monitoring. While you’d use more specialized frameworks for medical-grade monitoring (see resources from organizations like the NIH for serious health applications), the closure-based timer pattern is the same.


Higher-order utilities: building APIs that accept closures

So far we’ve mostly passed closures into system APIs. Let’s flip it around and build our own small utility that takes closures.

func measureExecutionTime(label: String, _ block: () -> Void) {
    let start = CFAbsoluteTimeGetCurrent()
    block()
    let end = CFAbsoluteTimeGetCurrent()
    print("[\(label)] took \(end - start) seconds")
}

measureExecutionTime(label: "Heavy work") {
    // some expensive task
    for _ in 0..<1_000_000 {
        _ = UUID().uuidString
    }
}

This is a compact example of practical examples of using closures in Swift to build reusable utilities:

  • The measureExecutionTime function doesn’t care what the work is.
  • The caller injects behavior via a closure.
  • You can reuse this utility anywhere in your codebase.

This pattern scales all the way up to full-blown frameworks: pass behavior in, and the framework controls when and how it runs.


FAQ: short answers about real closure usage

Q: What are some real-world examples of using closures in Swift apps?
Examples include networking completion handlers, SwiftUI button actions, collection transformations (map, filter, reduce), GCD background tasks, timers, and view model callbacks that notify views of state changes.

Q: Can you give an example of avoiding retain cycles with closures?
Yes. Anytime a long-lived object stores a closure that references self, you typically write [weak self] in the capture list and then use self? inside the closure. The UserViewModel and HeartbeatLogger examples above both show this pattern.

Q: Are closures slower than regular functions in Swift?
In everyday app code, the difference is usually negligible. Swift’s optimizer is quite good at inlining small closures, especially non-escaping ones. For performance-sensitive code, you can measure it using a utility like measureExecutionTime or Instruments.

Q: How do closures relate to async/await in modern Swift?
Many older APIs use completion-handler closures. In Swift 5.5+ you can bridge those into async functions using withCheckedContinuation or withCheckedThrowingContinuation, as shown in the fetchUsersAsync example. Under the hood, async/await is still coordinating work in a closure-like way.

Q: When should I use a closure instead of a protocol?
Closures shine when you just need to pass behavior—"do this when X happens"—without defining a full type. Protocols are better when you need a named capability with multiple requirements or when you’ll have several conforming types. Many modern Swift APIs favor closures for simple callbacks and keep protocols for more structured abstractions.

Explore More Swift Code Snippets

Discover more examples and insights in this category.

View All Swift Code Snippets