SwiftUI带列表与API调用的简单页面使用ViewModel是否合理?有无更优方案
SwiftUI通知列表页面架构咨询
我正在开发一个展示通知列表的SwiftUI页面,用ViewModel处理状态与API调用,页面包含头部,支持下拉刷新、无限滚动的分页功能。最近看到一些观点说SwiftUI里用ViewModel是反模式或没必要(尤其是简单视图),这让我怀疑当前架构是不是过度设计,不符合SwiftUI最佳实践。
之前我试过把所有逻辑(状态管理、API调用)都放在View内部,但视图变得杂乱不堪,还没法测试,所以把逻辑移到了独立的ObservableObject ViewModel里,实现关注点分离。下面是简化后的代码:
ViewModel代码
import SwiftUI @MainActor class NotificationListViewModel: ObservableObject, APIParamFiledType { let router: InterCeptor<ProfileEndPoint> enum NotificationState: Equatable { case loading case loaded([Notification]) case paginating([Notification]) case empty(String) case error(String) } @Published var notificationList = [Notification]() @Published private(set) var state = NotificationState.loading var userModelController: UserModelController var pagedObject = PageStruct(indxe: 1, size: 50) init(router: InterCeptor<ProfileEndPoint>, userModelController: UserModelController) { self.router = router self.userModelController = userModelController } func loadMoreNotification() async { let request = NotificationList.Request(country: userCountry, userInfoId: userInfoId, doctorID: doctorId, pageIndex: pagedObject.index, pageSize: pagedObject.size) do { let response: NotificationList.Response = try await router.request(endPoint: .notificationList(param: request, authToken: token)) if notificationList.isEmpty { notificationList.append(contentsOf: response.result ?? []) if notificationList.isEmpty { state = .empty("No new notifications") } else { state = .loaded(notificationList) } } else { notificationList.append(contentsOf: response.result ?? []) state = .paginating(notificationList) } pagedObject.totalCount = response.totalCount } catch let error { state = .error(error.localizedDescription) } } func resetNotification() async { notificationList.removeAll() pagedObject.resetPageIndex() await loadMoreNotification() } func shouldLoadMore(currentOffset: Int) async { if pagedObject.shouldLoadMore && currentOffset == notificationList.count - 1 { pagedObject.increasePageIndex() await loadMoreNotification() } } }
视图代码
import SwiftUI import JIGUIKit struct NotificationListView: View { var backButtonClick: (() -> Void)? @ObservedObject var viewModel: NotificationListViewModel var body: some View { ZStack { GradientBlueView() .ignoresSafeArea() VStack(spacing: 0) { headerView contentView }.frame(maxHeight: .infinity, alignment: .top).onAppear { UIRefreshControl.appearance().tintColor = .white UIApplication.shared.applicationIconBadgeNumber = 0 Task { await viewModel.loadMoreNotification() } } .ignoresSafeArea(.container, edges: [.top, .leading, .trailing]) } } private var headerView: some View { HeaderViewWrapper(backButtonClick: backButtonClick) .frame(height: 100) } @ViewBuilder private var contentView: some View { switch viewModel.state { case .loading: initalLoadingView case .loaded(let notifications), .paginating(let notifications): List { showList(notifications: notifications) if case .paginating = viewModel.state { loaderView.listRowBackground(Color.clear) } }.refreshable(action: { Task { await viewModel.resetNotification() } }) .padding(.horizontal, 16) .listStyle(.plain) .applyScrollIndicatorHiddenIfAvailable() case .empty(let emptyNotification), .error(let emptyNotification): showError(error: emptyNotification) } } private var initalLoadingView: some View { VStack { Spacer() loaderView Spacer() } } private var loaderView: some View { HStack { Spacer() BallPulseSync(ballSize: 20, ballColor: .buttonBackground) Spacer() }.frame(height: 100) } func showError(error: String) -> some View { VStack { Spacer() HStack { Spacer() Text(error).font(.headline).foregroundStyle(Color.white) Spacer() } Spacer() } } func showList(notifications: [Notification]) -> some View { ForEach(notifications.indices, id: \.self) { index in let notification = notifications[index] NotificationRow(notification: notification) .padding(.vertical, 10) .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) .listRowBackground(Color.clear) .onAppear { Task { await viewModel.shouldLoadMore(currentOffset: index) } } } } }
咨询问题
当前使用ViewModel的实现是否合理?是否存在更符合SwiftUI最佳实践的方案?
回答
你的ViewModel实现完全合理
你当前的方案完全符合SwiftUI最佳实践,理由如下:
- 关注点分离:把API调用、分页逻辑、状态管理从View中抽离,让View只负责渲染UI,代码职责清晰,维护成本更低。
- 可测试性:ViewModel是独立的类,可以单独写单元测试验证分页逻辑、API错误处理等,这是把逻辑塞在View里做不到的。
- 状态可控:用
@Published和NotificationState统一管理UI状态,避免View里出现零散的状态变量,减少状态不一致的风险。
那些说ViewModel是反模式的观点,大多针对超简单视图(比如只有几个按钮的静态页面),而你的页面涉及分页、下拉刷新、API调用、多状态切换,属于有业务逻辑的复杂视图,ViewModel是最优解之一。
可优化的点(更贴合SwiftUI实践)
- 改用
@StateObject而非@ObservedObject:在View中声明ViewModel时,用@StateObject var viewModel: NotificationListViewModel,因为@StateObject会确保ViewModel在View生命周期内只初始化一次,避免不必要的重建。 - 简化状态逻辑:你现在同时维护了
notificationList和state两个Published变量,其实可以把notificationList整合到NotificationState里,避免数据冗余。比如:
@Published private(set) var state: NotificationState = .loading // 去掉单独的notificationList,从state中取数据 // 在View中通过switch state获取列表
- 分页逻辑封装:把分页相关的逻辑(页码、是否还有更多数据)封装到单独的
PaginationManager类,让ViewModel更专注于业务逻辑,进一步解耦。 - 避免用索引遍历ForEach:当前用
notifications.indices遍历,建议直接遍历notifications数组,给Notification实现Identifiable协议,这样更符合SwiftUI的习惯,也避免索引越界风险:
// 让Notification实现Identifiable struct Notification: Identifiable { let id: String // 其他属性 } // 在View中遍历 ForEach(notifications) { notification in NotificationRow(notification: notification) // ...其他修饰符 .onAppear { if notification == notifications.last { Task { await viewModel.shouldLoadMore() } } } }
总结
你的架构方向是对的,不存在过度设计。SwiftUI的核心是数据驱动UI,ViewModel作为数据和业务逻辑的载体,在复杂视图中是不可或缺的。那些反对ViewModel的观点有其适用场景,但不适用于你的分页通知列表页面。
内容的提问来源于stack exchange,提问作者john Smith
相关产品推荐
相关产品推荐

