MVVM Architecture Pattern – AdventureTube iOS

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

  1. Use final class for ViewModels
  2. Always use [weak self] in closures
  3. Store cancellables
  4. Keep Views dumb – business logic in ViewModel
  5. Use descriptive @Published property names

❌ Don’t

  1. Don’t import SwiftUI in ViewModel
  2. Don’t reference View from ViewModel
  3. Don’t use @State in ViewModel
  4. Don’t perform UI operations in ViewModel
  5. Don’t create retain cycles
]]>

Leave a Comment

Your email address will not be published. Required fields are marked *