Reactive Data Flow with Apple’s Combine Framework
Combine Overview
Combine is Apple’s framework for processing values over time, enabling reactive programming in Swift.
Why Combine in AdventureTube?
✅ Reactive UI – Automatic view updates via @Published
✅ Async Operations – Handle network requests reactively
✅ Core Data Integration – Cross-context observation
✅ Composable – Chain and transform data streams
✅ Type-Safe – Compile-time error checking
✅ Memory Safe – Automatic subscription cleanup
Core Concepts
Publisher
Definition: Emits values over time
// Examples of Publishers in AdventureTube
@Published var stories: [StoryEntity] = [] // Published<[StoryEntity]>
URLSession.shared.dataTaskPublisher(for: url) // DataTaskPublisher
CoreDataFetchResultsPublisher // Custom publisher
Subscriber
Definition: Receives values from a publisher
// Sink is the most common subscriber
publisher.sink(
receiveCompletion: { completion in
// Handle completion or error
},
receiveValue: { value in
// Handle received value
}
)
AnyCancellable
Definition: Token that cancels subscription when deallocated
private var cancellables = Set()
publisher.sink { ... }
.store(in: &cancellables) // Stores subscription
Publishers in AdventureTube
1. @Published (Most Common)
Location: ViewModels
Purpose: Auto-notify observers when property changes
class MyStoryListViewVM: ObservableObject {
@Published var youtubeContentItems: [YoutubeContentItem] = []
@Published var isLoading = false
@Published var errorMessage: String?
}
2. URLSession.DataTaskPublisher
Location: API Services
Purpose: Network requests
3. PassthroughSubject
Location: Custom publishers, event streams
Purpose: Manually send values
private let playListIdPublisher = PassthroughSubject()
// Send value
playListIdPublisher.send("UUMg4QJXtDH-VeoJvlEpfEYg")
// Send completion
playListIdPublisher.send(completion: .finished)
Common Patterns
Pattern 1: Chaining Network Requests
func fetchChannelThenVideos() {
fetchChannelInfo()
.flatMap { channelInfo in
return self.fetchVideos(playlistId: channelInfo.uploadsPlaylistId)
}
.sink(
receiveCompletion: { completion in
// Handle final completion
},
receiveValue: { videos in
// Process videos
}
)
.store(in: &cancellables)
}
Pattern 2: Debouncing Search Input
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [Result] = []
private var cancellables = Set()
init() {
$searchText
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] text in
self?.performSearch(text)
}
.store(in: &cancellables)
}
}
Memory Management
AnyCancellable Storage
// BAD - Cancels immediately
publisher.sink { value in
print(value)
}
// GOOD - Stored, cancels when viewModel deallocates
publisher.sink { value in
print(value)
}
.store(in: &cancellables)
Weak Self Pattern
// GOOD - No retain cycle
publisher.sink { [weak self] value in
self?.processValue(value)
}
.store(in: &cancellables)
Operator Cheat Sheet
| Operator | Purpose | Example |
|---|---|---|
map |
Transform values | .map { $0.uppercased() } |
filter |
Filter values | .filter { $0.count > 5 } |
compactMap |
Map + remove nils | .compactMap { Int($0) } |
flatMap |
Chain publishers | .flatMap { fetchUser($0) } |
debounce |
Delay emissions | .debounce(for: .seconds(1), ...) |
retry |
Retry on failure | .retry(3) |
catch |
Handle errors | .catch { Just([]) } |
Best Practices
✅ Do
- Store cancellables:
.store(in: &cancellables) - Use [weak self]: Prevent retain cycles
- Handle both success and failure
- Use appropriate schedulers:
.receive(on: DispatchQueue.main)for UI - Type erase when needed:
.eraseToAnyPublisher()
❌ Don’t
- Don’t forget to store subscriptions
- Don’t create retain cycles
- Don’t perform UI updates on background threads
- Don’t ignore errors
- Don’t over-complicate with operators


