1
\$\begingroup\$

I'm building a SwiftUI screen that displays a list of notifications, using a ViewModel to handle state and API calls. The screen is fairly straightforward: it has a header and a paginated notification list that supports pull-to-refresh and infinite scrolling.

Recently, I came across a blog post suggesting that using ViewModels in SwiftUI might be an anti-pattern or unnecessary, especially in simple views (https://developer.apple.com/forums/thread/699003). This made me question whether my current architecture is overkill or misaligned with SwiftUI best practices.

Here is a simplified version of my implementation:

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() } } } 

here is my view

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) } } } } } 

I experimented with managing all logic inside the View itself, including state management and API calls, without using a separate ViewModel. However, the view became cluttered and harder to test, so I moved the logic into a dedicated ObservableObject ViewModel for better separation of concerns.

\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.