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

如何从ViewModel为View中的@State属性赋值?(分页触底场景)

问题

我有一个具备基础列表功能的电影列表视图,希望在分页到达最后一页时显示提示弹窗。当前使用reachedLastPage属性控制弹窗显示,viewModel.state是一个枚举,其中.movies关联值包含moreRemaining属性,用于标识是否还有更多分页数据。需要在moreRemaining变为false时,将reachedLastPage设为true以触发弹窗,请问最优实现方式是什么?

import SwiftUI
import SwiftUIRefresh

struct MovieListingView<T>: View where T: BaseMoviesListViewModel {
  
  @ObservedObject var viewModel: T
  @State var title: String
  @State var reachedLastPage: Bool = false
  
  var body: some View {
    NavigationView {
      ZStack {
        switch viewModel.state {
        case .loading:
          LoadingView(title: "Loading Movies...")
            .onAppear {
              fetchMovies()
            }
        case .error(let error):
          ErrorView(message: error.localizedDescription, buttonTitle: "Retry") {
            fetchMovies()
          }
        case .noData:
          Text("No data")
            .multilineTextAlignment(.center)
            .font(.system(size: 20))
        case .movies(let data):
          List {
            ForEach(data.movies) { movie in
              NavigationLink(destination: LazyView(MovieDetailView(viewModel: MovieDetailViewModel(id: movie.id)))) {
                MovieViewRow(movie: movie)
                  .onAppear {
                    if movie == data.movies.last && data.moreRemaining {
                      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                        fetchMovies()
                      }
                    }
                  }
              }
              if movie == data.movies.last && data.moreRemaining {
                HStack {
                  Spacer()
                  ActivityIndicator(isAnimating: .constant(data.moreRemaining))
                  Spacer()
                }
              }
            }
          }.pullToRefresh(isShowing: .constant(data.isRefreshing)) {
            print("Refresheeeee")
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
              refreshMovies()
            }
          }
        }
      }
      .navigationViewStyle(.stack)
      .navigationBarTitle("\(title)", displayMode: .inline)
      .alert(isPresented: $reachedLastPage) {
        Alert(title: Text("You have reached to the end of the list."))
      }
    }
  }
  
  private func fetchMovies() {
    viewModel.trigger(.fetchMovies(false))
  }
  
  private func refreshMovies() {
    viewModel.trigger(.fetchMovies(true))
  }
}
最优实现方案

针对这个需求,有两种符合SwiftUI响应式设计的实现方式,分别适合不同场景:

方式一:视图层直接监听状态变化(快速实现)

在视图中通过onChange修饰符监听viewModel.state的变化,当检测到.movies状态下moreRemaining变为false时触发弹窗,同时添加标记避免重复弹窗。

修改后的完整代码:

import SwiftUI
import SwiftUIRefresh

struct MovieListingView<T>: View where T: BaseMoviesListViewModel {
  
  @ObservedObject var viewModel: T
  @State var title: String
  @State var reachedLastPage: Bool = false
  // 标记是否已经显示过末尾弹窗,避免重复触发
  @State private var hasShownEndAlert = false
  
  var body: some View {
    NavigationView {
      ZStack {
        switch viewModel.state {
        case .loading:
          LoadingView(title: "加载电影中...")
            .onAppear {
              fetchMovies()
            }
        case .error(let error):
          ErrorView(message: error.localizedDescription, buttonTitle: "重试") {
            fetchMovies()
          }
        case .noData:
          Text("暂无数据")
            .multilineTextAlignment(.center)
            .font(.system(size: 20))
        case .movies(let data):
          List {
            ForEach(data.movies) { movie in
              NavigationLink(destination: LazyView(MovieDetailView(viewModel: MovieDetailViewModel(id: movie.id)))) {
                MovieViewRow(movie: movie)
                  .onAppear {
                    if movie == data.movies.last && data.moreRemaining {
                      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                        fetchMovies()
                      }
                    }
                  }
              }
              if movie == data.movies.last && data.moreRemaining {
                HStack {
                  Spacer()
                  ActivityIndicator(isAnimating: .constant(data.moreRemaining))
                  Spacer()
                }
              }
            }
          }.pullToRefresh(isShowing: .constant(data.isRefreshing)) {
            print("开始刷新")
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
              refreshMovies()
              // 下拉刷新时重置弹窗标记,允许再次触发
              hasShownEndAlert = false
            }
          }
        }
      }
      .navigationViewStyle(.stack)
      .navigationBarTitle("\(title)", displayMode: .inline)
      .alert(isPresented: $reachedLastPage) {
        Alert(title: Text("已到达列表末尾")) {
          Button("确定") {
            // 弹窗关闭后重置标记,避免后续无法触发
            hasShownEndAlert = false
          }
        }
      }
      // 监听ViewModel状态变化,触发弹窗逻辑
      .onChange(of: viewModel.state) { newState in
        guard case .movies(let data) = newState else { return }
        // 无更多数据且未显示过弹窗时触发
        if !data.moreRemaining && !hasShownEndAlert {
          hasShownEndAlert = true
          reachedLastPage = true
        }
      }
    }
  }
  
  private func fetchMovies() {
    viewModel.trigger(.fetchMovies(false))
  }
  
  private func refreshMovies() {
    viewModel.trigger(.fetchMovies(true))
  }
}

方式二:将逻辑移至ViewModel(MVVM架构推荐)

更规范的做法是在ViewModel中添加@Published属性管理弹窗状态,当moreRemaining变为false时由ViewModel主动发送触发信号,视图仅负责监听和展示。

ViewModel修改示例:

class BaseMoviesListViewModel: ObservableObject {
  enum State {
    case loading
    case error(Error)
    case noData
    case movies(MovieData)
  }
  
  struct MovieData {
    var movies: [Movie]
    var moreRemaining: Bool
    var isRefreshing: Bool
  }
  
  enum Action {
    case fetchMovies(Bool)
  }
  
  @Published var state: State = .loading
  @Published var showEndAlert: Bool = false
  private var hasTriggeredEndAlert = false
  
  func trigger(_ action: Action) {
    switch action {
    case .fetchMovies(let isRefresh):
      // 模拟数据请求逻辑(替换为真实网络请求)
      fetchMockData(isRefresh: isRefresh) { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .success(let data):
          self.state = .movies(data)
          // 无更多数据且未触发过弹窗时,设置弹窗状态
          if !data.moreRemaining && !self.hasTriggeredEndAlert {
            self.hasTriggeredEndAlert = true
            self.showEndAlert = true
          }
          // 下拉刷新重置标记
          if isRefresh {
            self.hasTriggeredEndAlert = false
          }
        case .failure(let error):
          self.state = .error(error)
        }
      }
    }
  }
  
  // 模拟数据请求方法
  private func fetchMockData(isRefresh: Bool, completion: @escaping (Result<MovieData, Error>) -> Void) {
    // 此处替换为真实网络请求逻辑
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
      let mockMovies = (0..<20).map { Movie(id: $0, title: "电影\($0)") }
      let data = MovieData(movies: mockMovies, moreRemaining: false, isRefreshing: false)
      completion(.success(data))
    }
  }
  
  // 重置弹窗标记的方法(供视图调用)
  func resetEndAlertFlag() {
    hasTriggeredEndAlert = false
  }
}

视图层修改:

struct MovieListingView<T>: View where T: BaseMoviesListViewModel {
  
  @ObservedObject var viewModel: T
  @State var title: String
  
  var body: some View {
    NavigationView {
      // 原视图内容保持不变...
    }
    .alert(isPresented: $viewModel.showEndAlert) {
      Alert(title: Text("已到达列表末尾")) {
        Button("确定") {
          viewModel.resetEndAlertFlag()
        }
      }
    }
  }
  
  // 原fetchMovies、refreshMovies方法不变...
}

方案对比

  • 方式一:无需修改ViewModel,快速落地,适合小型项目或临时需求。
  • 方式二:遵循MVVM架构,将业务逻辑集中在ViewModel,视图仅负责UI展示,代码更易维护和测试,推荐用于中大型项目。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 12:36:25