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

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实践)

  1. 改用@StateObject而非@ObservedObject:在View中声明ViewModel时,用@StateObject var viewModel: NotificationListViewModel,因为@StateObject会确保ViewModel在View生命周期内只初始化一次,避免不必要的重建。
  2. 简化状态逻辑:你现在同时维护了notificationList和state两个Published变量,其实可以把notificationList整合到NotificationState里,避免数据冗余。比如:
@Published private(set) var state: NotificationState = .loading

// 去掉单独的notificationList,从state中取数据
// 在View中通过switch state获取列表
  1. 分页逻辑封装:把分页相关的逻辑(页码、是否还有更多数据)封装到单独的PaginationManager类,让ViewModel更专注于业务逻辑,进一步解耦。
  2. 避免用索引遍历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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:29:51