<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bhavesh chaudhari</title>
    <description>The latest articles on DEV Community by bhavesh chaudhari (@bhavesh_chaudhari_88efcc9).</description>
    <link>https://dev.to/bhavesh_chaudhari_88efcc9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2363014%2F9bb40e29-8d6d-4bec-8d80-80835476b9b8.png</url>
      <title>DEV Community: bhavesh chaudhari</title>
      <link>https://dev.to/bhavesh_chaudhari_88efcc9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bhavesh_chaudhari_88efcc9"/>
    <language>en</language>
    <item>
      <title>Is it wrong to use a ViewModel in a simple SwiftUI screen with a list and API call? is there any better approach</title>
      <dc:creator>bhavesh chaudhari</dc:creator>
      <pubDate>Thu, 22 May 2025 09:06:42 +0000</pubDate>
      <link>https://dev.to/bhavesh_chaudhari_88efcc9/is-it-wrong-to-use-a-viewmodel-in-a-simple-swiftui-screen-with-a-list-and-api-call-is-there-any-4hgj</link>
      <guid>https://dev.to/bhavesh_chaudhari_88efcc9/is-it-wrong-to-use-a-viewmodel-in-a-simple-swiftui-screen-with-a-list-and-api-call-is-there-any-4hgj</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;Here is a simplified version of my implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import SwiftUI
@MainActor
class NotificationListViewModel: ObservableObject, APIParamFiledType {

    let router: InterCeptor&amp;lt;ProfileEndPoint&amp;gt;

    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&amp;lt;ProfileEndPoint&amp;gt;, 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 &amp;amp;&amp;amp; currentOffset == notificationList.count - 1 {
            pagedObject.increasePageIndex()
            await loadMoreNotification()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;here is my view&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import SwiftUI
import JIGUIKit

struct NotificationListView: View {

    var backButtonClick: (() -&amp;gt; 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) -&amp;gt; some View {
        VStack {
            Spacer()
            HStack {
                Spacer()
                Text(error).font(.headline).foregroundStyle(Color.white)
                Spacer()
            }
            Spacer()
        }
    }

    func showList(notifications: [Notification]) -&amp;gt; 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)
                    }
                }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
