Model-View-ViewModel with SwiftUI & Combine
MVVM Overview
AdventureTube uses MVVM (Model-View-ViewModel) architecture pattern with SwiftUI’s reactive framework.
Why MVVM?
✅ Separation of Concerns – UI, business logic, and data are separated
✅ Testability – ViewModels can be unit tested without UI
✅ Reactive – Automatic UI updates via Combine
✅ Reusability – ViewModels can be shared across views
✅ SwiftUI Native – Perfect match for declarative UI
Architecture Layers
1. Model Layer
Responsibilities: Data structures, Core Data entities, Network DTOs
Files: StoryEntity, UserModel, YoutubeContentResource
2. ViewModel Layer
Responsibilities: UI state management, business logic, data transformation, API calls
Naming: MyStoryListViewVM for MyStoryListView
Key Protocols: ObservableObject
3. View Layer
Responsibilities: UI rendering, user interaction, observing ViewModel, navigation
Property Wrappers
@Published (ViewModel)
Notifies observers when property changes
class MyStoryListViewVM: ObservableObject {
@Published var youtubeContentItems: [YoutubeContentItem] = []
@Published var isShowRefreshAlert = false
}
@StateObject (View)
Creates and owns a ViewModel instance
struct MyStoryListView: View {
@StateObject private var viewModel = MyStoryListViewVM()
}
@ObservedObject (View)
Observes an externally-owned ViewModel
struct StoryDetailView: View {
@ObservedObject var viewModel: MyStoryCommonDetailVM
}
@EnvironmentObject (View)
Inject shared ViewModel down view hierarchy
@EnvironmentObject var loginManager: LoginManager
ViewModel Patterns
Pattern 1: Reactive Core Data Integration
class MyStoryListViewVM: ObservableObject {
@Published var stories: [StoryEntity] = []
func listenCoreDataChanges() {
coreDataStorage.didSavePublisher(
for: StoryEntity.self,
in: context,
changeTypes: [.inserted, .updated, .deleted]
)
.sink { [weak self] changes in
self?.handleChanges(changes)
}
.store(in: &cancellables)
}
}
Pattern 2: API Integration with Combine
func downloadYoutubeContent(completion: @escaping () -> Void) {
isLoading = true
youtubeAPIService.youtubeContentResourcePublisher { publisher in
publisher.sink(
receiveCompletion: { [weak self] result in
self?.isLoading = false
completion()
},
receiveValue: { [weak self] resource in
self?.youtubeContentItems.append(contentsOf: resource.items)
}
)
}
}
Pattern 3: State Management
enum ViewState {
case idle
case loading
case success
case error(String)
}
@Published var viewState: ViewState = .idle
Best Practices
✅ Do
- Use final class for ViewModels
- Always use [weak self] in closures
- Store cancellables
- Keep Views dumb – business logic in ViewModel
- Use descriptive @Published property names
❌ Don’t
- Don’t import SwiftUI in ViewModel
- Don’t reference View from ViewModel
- Don’t use @State in ViewModel
- Don’t perform UI operations in ViewModel
- Don’t create retain cycles


