You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MVVM架构:如何从环境向子视图ViewModel传递主ViewModel实例?

MVVM架构下跨ViewModel共享数据源的最佳实践问题

我正在学习MVVM架构,遇到了一个需要解决的问题:我有一个带标签页的MainView,对应的MainViewModel里有自定义数据类型Event的数组,我用Environment实现了这个数组在整个App里共享。现在核心问题是,HomePageView有自己的HomePageViewModel,但为了实现刷新Event数组这类功能,需要调用MainViewModel的方法,而且必须是同一个实例来保证单一数据源。我已有可行方案,但不确定是否正确,想请教:是每次把MainViewModel或其数据单独传入方法更合适,还是有更好的方式直接把整个MainViewModel传入HomePageViewModel让它直接访问?


现有代码

HomePageView

import SwiftUI
import SwiftData
import MapKit

struct HomePageView: View {
    
    @EnvironmentObject var mainViewModel: MainViewViewModel
    @StateObject private var homePageViewModel = HomePageViewModel()
    @Environment(\.modelContext) var modelContext
    @Query private var savedEvents: [FavoriteEvent] = []
    @Environment(\.scenePhase) var scenePhase
    @State private var cameFromBackground = true
    
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack {
                    
                    //MARK: saved/favorite events
                    HStack {
                        Text("Uložené akce")
                            .font(.title2.bold())
                        Spacer()
                    }
                    .padding([.top, .horizontal])
                    
                    // a fallback and a hint to help user discover the app's functions
                    if homePageViewModel.shouldShowNoEventsSavedView(savedEvents: savedEvents) {
                        ContentUnavailableView(
                            "Žádné uložené akce.",
                            systemImage: "calendar.badge.plus",
                            description: Text("Přejděte do záložky Akce a v detailu si přidejte svou první událost!")
                        )
                        .padding()
                    } else {
                        savedEventsView
                    }
                    
                    //MARK: current events
                    HStack {
                        Text("Právě se děje")
                            .font(.title2.bold())
                        Spacer()
                    }
                    .padding()
                    
                    
                    // a current events view with fallback options
                    switch mainViewModel.status {
                    case .notStarted:
                        ContentUnavailableView("Objevte nové akce.", systemImage: "magnifyingglass", description: Text("Potažením dolu obnovte stránku a objevte nové akce."))
                    case .fetching:
                        ProgressView()
                    case .success:
                        if homePageViewModel.shouldShowNoEventsHappeningView() {
                            ContentUnavailableView(
                                "Dnes se nekonají žádné akce",
                                systemImage: "calendar.badge.exclamationmark",
                                description: Text("Přejděte do záložky Akce a prohlédněte si nadcházející události!")
                            )
                            .padding()
                        } else {
                            todayEventsView
                        }
                        
                    case .failed:
                        ContentUnavailableView("Nepodařilo se najít žádné akce.", systemImage: "exclamationmark.magnifyingglass", description: Text("Bohužel se nepodařilo nalézt žádné akce. Zkuste obnovit hledání potažením dolu."))
                    }
                }
                .scrollIndicators(.hidden)
                .preferredColorScheme(.dark)
                .navigationTitle("Přehled")
            }
            .scrollIndicators(.hidden)
            .refreshable {
                Task {
                    await homePageViewModel.refreshView(with: mainViewModel)
                }
            }
        }
        // all events are loaded on appear only when there are none yet
        .onAppear() {
            if homePageViewModel.shouldLoadAllEvents(allEvents: mainViewModel.allEvents) {
                Task {
                    await homePageViewModel.refreshView(with: mainViewModel)
                }
            }
        }
    }

HomePageViewModel

import Foundation
import SwiftUI
import SwiftData


class HomePageViewModel: ObservableObject {
    
    @Published var sortedAllEvents: [Event] = []
    @Published var todayEvents: [Event] = []
    
    // a method that filters sorted events and keeps only those that are happening today
    @MainActor func getTodayEvents(allEvents: [Event]) {
        var events: [Event] = []
        for event in allEvents {
            if isEventToday(eventProperties: event.properties) {
                events.append(event)
            }
        }
        let sortedEvents = events.sorted { $0.properties.dateFrom < $1.properties.dateFrom }
        todayEvents = sortedEvents
    }
    
    // a helper method that sorts events based on their starting date
    private func sortEventsByDate(fetchedEvents: FetchedEvents) {
        sortedAllEvents = fetchedEvents.features.sorted { $0.properties.dateFrom < $1.properties.dateFrom }
    }
    
    // a helper method used to refresh the page
    func refreshView(with mainViewModel: MainViewViewModel) async {
        Task {
            await mainViewModel.getAllEvents()
            await getTodayEvents(allEvents: mainViewModel.allEvents)
        }
    }
    
    // a simple method to determine whether to load events if there are none present
    @MainActor func shouldLoadAllEvents(allEvents: [Event]) -> Bool {
        return allEvents.isEmpty
    }
    
    func shouldShowNoEventsSavedView(savedEvents: [FavoriteEvent]) -> Bool {
        return savedEvents.isEmpty
    }
    
    func shouldShowNoEventsHappeningView() -> Bool {
        return todayEvents.isEmpty
    }
    
    
    // a helper method that returns true if the event is happening today
    private func isEventToday(eventProperties: EventDetails) -> Bool {
        
        let dateFrom = eventProperties.dateFrom / 1000
        let dateTo = eventProperties.dateTo / 1000
        let todayUnix = Date().timeIntervalSince1970
        
        if todayUnix >= dateFrom && todayUnix <= dateTo {
            return true
        } else {
            return false
        }
    }
}

方案对比与建议

1. 现有传参方式的优劣势

你当前的做法是在需要时把MainViewModel传入HomePageViewModel的方法(比如refreshView(with:)),这种方式:

  • 优点:依赖关系清晰,HomePageViewModel只在需要时才和MainViewModel交互,无强持有,符合单一职责原则,专注处理自身视图逻辑。
  • 缺点:每次调用都要传参,代码略显繁琐;如果需要多次访问MainViewModel,重复传参会增加冗余。

2. 直接注入MainViewModel的优劣势

如果在HomePageViewModel初始化时就通过构造函数注入MainViewModel实例:

  • 优点:HomePageViewModel可直接访问MainViewModel的方法和数据,无需每次传参,代码更简洁;能通过Combine订阅@Published属性,自动监听数据变化。
  • 缺点:两个ViewModel耦合度更高,不利于单元测试(MockMainViewModel更麻烦);若MainViewModel生命周期更长,需注意用weak self避免内存泄漏。

3. 更优的数据流解耦方案

结合你已用Environment共享MainViewModel的基础,推荐通过订阅数据流实现自动同步:

  • 在HomePageViewModel中订阅MainViewModel.allEvents的变化,数据源更新时自动重新计算todayEvents
  • 刷新操作直接在HomePageView中调用mainViewModel.getAllEvents(),由订阅自动触发HomePageViewModel的逻辑更新

示例调整代码:

class HomePageViewModel: ObservableObject {
    @Published var todayEvents: [Event] = []
    private var cancellables = Set<AnyCancellable>()
    
    init(mainViewModel: MainViewViewModel) {
        // 订阅MainViewModel的allEvents变化,自动同步todayEvents
        mainViewModel.$allEvents
            .receive(on: DispatchQueue.main)
            .sink { [weak self] events in
                self?.getTodayEvents(allEvents: events)
            }
            .store(in: &cancellables)
    }
    
    // 原getTodayEvents方法保持不变
}

在HomePageView中初始化HomePageViewModel时注入实例:

struct HomePageView: View {
    @EnvironmentObject var mainViewModel: MainViewViewModel
    @StateObject private var homePageViewModel: HomePageViewModel
    
    init() {
        // 从Environment获取mainViewModel并注入(注意:需确保mainViewModel已在父视图/App层级注册)
        _homePageViewModel = StateObject(wrappedValue: HomePageViewModel(mainViewModel: MainViewViewModel()))
    }
    
    // 其余代码不变
}

总结

  • 若HomePageViewModel仅偶尔调用MainViewModel方法,当前传参方式完全可行,且耦合度低
  • 若需频繁交互或监听数据变化,建议构造函数注入+Combine订阅,代码更简洁且数据同步自动
  • 无论哪种方式,都要注意用weak self避免强引用导致的内存泄漏

内容的提问来源于stack exchange,提问作者eatom

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 04:14:51