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
89: Combine vs async/await: When to Use Which
I remember a developer on my team a few years ago who was building a real-time search feature for a massive product catalog. He had just learned async/await and was enamored with how clean it made the code look. He tried to implement the search by triggering an async function every time the user typed a character. The result? A chaotic mess of race conditions where an older, slower network request would return after a newer one, overwriting the search results with stale data. He spent two days trying to manually manage Task handles and cancellation flags before he came to me, frustrated that the "modern" way felt so fragile.
The problem wasn't that async/await is flawed; it's that he was trying to use a hammer to fix a plumbing problem. He was treating a continuous stream of events as a series of isolated, one-off tasks.
The Linear Path vs. The Continuous Stream
The easiest way to decide between these two is to ask yourself: "Am I waiting for a single value, or am I observing a sequence of values over time?"
async/await is designed for linear flow. You call a function, you suspend, and eventually, you get a result (or an error) back. It's perfect for things like fetching a user profile, uploading a photo, or reading a file from disk. It turns asynchronous code into something that reads like a story: first do this, then do that, then show the result. If your logic is "Give me this one thing," async/await is almost always the right choice. It's more readable, easier to debug, and has much lower overhead.
Combine, on the other hand, is for "streams." Think of a stream as a pipe where data drops in at unpredictable intervals. A search bar is a classic example: the user types 'A', then 'Ap', then 'App'. That's not three separate, unrelated requests; it's one continuous stream of input. Combine gives you the tools to manipulate that pipe. You can debounce the input (wait for the user to stop typing for 300ms), filter out short strings, and switchToLatest to automatically cancel the previous network request when a new character is typed.
When Combine is Still the Only Tool for the Job
Even in a world where async/await is the star, Combine remains essential for reactive state management. If you're building a complex UI where five different components need to react instantly to a single change in a data model, trying to coordinate that with async functions becomes a nightmare of manual callbacks.
I often suggest sticking with Combine when you need to:
- Combine multiple asynchronous sources into one (e.g., waiting for both a GPS lock and a network connection before starting a process).
- Throttle or debounce high-frequency events (like scroll offsets or keystrokes).
- Bind a data source directly to a UI element using
@Publishedandassign(to:).
Bridging the Two Worlds
You don't actually have to choose one for your entire project. In fact, the best architecture usually mixes them. You might use Combine to handle the "messy" UI input and then trigger an async function to do the actual heavy lifting of the network request.
// Using Combine to handle the stream, then bridging to async/await
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] text in
Task {
// Bridge into the linear world of async/await
do {
self?.results = try await self?.searchAPI(query: text)
} catch {
self?.handleError(error)
}
}
}
.store(in: &cancellables)
Notice how the "noise" of the user's typing is handled by the Combine pipeline, but the actual data fetch—which is a simple "one-and-done" request—is handled by async/await. This is where you get the best of both worlds: the power of reactive streams and the readability of structured concurrency.
📋 Practical Task
Exercise: Implementing a Debounced Search Pipeline
You are tasked with fixing a "stuttering" search feature in a City Finder app. Currently, the app makes a network request on every single keystroke, which is overloading the API and causing the UI to flicker.
Your Goal: Create a CitySearchViewModel that uses Combine to monitor a searchText property and only triggers a search when the user has stopped typing for 500 milliseconds.
Requirements:
- Use a
@Publishedstring for the search text. - Use
debounceto prevent rapid-fire API calls. - Use
removeDuplicatesso the search doesn't re-run if the user types a character and then immediately deletes it. - Inside the
sink, trigger a mockasyncfunction calledfetchCities(query: String). - Ensure the subscription is stored in a
Set<AnyCancellable>to prevent it from being deallocated immediately.
There are no comments for now.