SwiftUI获取景点列表后,如何为每个景点调用API获取额外数据?
问题场景
页面加载时先调用fetchPlace()获取Place数据,待vm.place更新后调用fetchAttractions()获取景点列表。现需在fetchAttractions()执行完成后,为每个景点调用API获取额外数据,但此前在fetchAttractions()中循环调用导致滚动卡顿,寻求最优实现方案。
最优实现方案
核心思路是懒加载+并发控制,结合SwiftUI的视图生命周期和Combine的异步流特性,避免一次性发起大量请求阻塞主线程,同时减少不必要的网络请求。
步骤1:扩展模型与API层
首先需要为Place模型添加额外数据字段,并扩展服务层支持获取额外数据的接口:
// 扩展Place模型,添加额外数据字段 struct Place: Identifiable, Decodable { let id: Int // 原有字段(如name、description等) var extraData: ExtraData? // 新增:存储额外数据 } struct ExtraData: Decodable { // 额外数据对应的字段(根据实际API定义) let detail: String } // 扩展服务协议与端点 protocol PlaceServiceProtocol { func getPlace(source: String, sourceId: String) -> AnyPublisher<Response, Error> func getAttractions(placeId: Int, page: Int) -> AnyPublisher<Response, Error> func getPlaceExtraData(placeId: Int) -> AnyPublisher<ExtraDataResponse, Error> // 新增 } enum PlaceEndpoint: APIEndpoint { case getPlace(source: String, sourceId: String) case getAttractions(placeId: Int, page: Int) case getPlaceExtraData(placeId: Int) // 新增 var path: String { switch self { case .getPlace: return "/place/get" case .getAttractions: return "/place/attractions/get" case .getPlaceExtraData: return "/place/extra-data/get" // 对应API路径 } } var method: HTTPMethod { switch self { case .getPlace, .getAttractions, .getPlaceExtraData: return .post } } var parameters: [String: Any]? { switch self { case .getPlace(let source, let sourceId): return ["source": source, "source_id": sourceId] case .getAttractions(let placeId, let page): return ["place_id": placeId, "page": page] case .getPlaceExtraData(let placeId): return ["place_id": placeId] } } } class PlaceService: PlaceServiceProtocol { let apiClient = URLSessionAPIClient<PlaceEndpoint>() // 原有方法保持不变... func getPlaceExtraData(placeId: Int) -> AnyPublisher<ExtraDataResponse, Error> { return apiClient.request(.getPlaceExtraData(placeId: placeId)) } } // 新增额外数据响应模型 struct ExtraDataResponse: Decodable { let data: ExtraData? }
步骤2:优化ViewModel,支持懒加载与并发控制
在ViewModel中添加获取额外数据的方法,同时处理重复请求和并发限制:
class PlaceViewModel: ObservableObject { private var cancellables = Set<AnyCancellable>() let placeService: PlaceServiceProtocol @Published var place: Place? @Published var attractions: [Place] = [] @Published var hasMoreAttractions = true // 补全原有代码中缺失的变量 init(placeService: PlaceServiceProtocol) { self.placeService = placeService } // 原有fetchPlace、fetchAttractions方法保持不变... // 单景点获取额外数据:避免重复请求,后台执行 func fetchExtraDataForPlace(_ place: Place) { guard place.extraData == nil else { return } // 已有数据则跳过 placeService.getPlaceExtraData(placeId: place.id) .subscribe(on: DispatchQueue.global(qos: .background)) .receive(on: RunLoop.main) .sink(receiveCompletion: { completion in if case .failure(let error) = completion { print("获取额外数据失败:\(error)") } }, receiveValue: { [weak self] response in guard let self = self, let extraData = response.data else { return } // 更新对应景点的额外数据 if let index = self.attractions.firstIndex(where: { $0.id == place.id }) { self.attractions[index].extraData = extraData } }) .store(in: &cancellables) } // 可选:批量获取额外数据,限制并发数(如同时最多3个请求) func fetchExtraDataForAttractions(_ attractions: [Place]) { Publishers.Sequence(sequence: attractions) .flatMap(maxPublishers: .max(3)) { [weak self] place in guard let self = self, place.extraData == nil else { return Empty().eraseToAnyPublisher() } return self.placeService.getPlaceExtraData(placeId: place.id) .map { (place, $0.data) } .catch { _ in Just((place, nil)).eraseToAnyPublisher() } } .subscribe(on: DispatchQueue.global(qos: .background)) .receive(on: RunLoop.main) .sink { [weak self] (place, extraData) in guard let self = self, let extraData = extraData else { return } if let index = self.attractions.firstIndex(where: { $0.id == place.id }) { self.attractions[index].extraData = extraData } } .store(in: &cancellables) } }
步骤3:修改视图层,实现懒加载触发
利用SwiftUI的onAppear方法,在卡片进入屏幕可见区域时才触发额外数据请求:
struct MainView: View { @Environment(\.dismiss) var dismiss let source: String let sourceId: String @EnvironmentObject var startupViewModel: StartupViewModel @StateObject var vm = PlaceViewModel(placeService: PlaceService()) var body: some View { NavigationView { ScrollView(showsIndicators: false) { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { ForEach($vm.attractions) { $place in PlaceCardView(place: $place) .onAppear { vm.fetchExtraDataForPlace(place) } } } } } } } .task { vm.fetchPlace(source: source, sourceId: sourceId) } .onChange(of: vm.place) { newPlace in // 仅在place首次加载且景点列表为空时请求景点数据 if let placeId = newPlace?.id, vm.attractions.isEmpty { vm.fetchAttractions(placeId: placeId, page: 1) } } } .environmentObject(vm) } } // 修改卡片视图为绑定类型,支持数据更新 struct PlaceCardView: View { @Binding var place: Place var body: some View { VStack(alignment: .leading, spacing: 8) { Text(place.name) // 原有字段展示 .font(.headline) // 额外数据展示:加载中显示进度条,加载完成显示内容 if let extra = place.extraData { Text(extra.detail) .font(.caption) .foregroundColor(.secondary) } else { ProgressView() .frame(height: 20) } } .padding(12) .background(Color.white) .cornerRadius(8) .shadow(radius: 2) } }
方案优势
- 懒加载减少请求量:仅当卡片进入可视区域时才发起额外数据请求,避免一次性加载所有数据导致的网络和UI压力。
- 并发控制避免过载:批量请求时通过
flatMap(maxPublishers:)限制同时请求数量,防止网络请求拥堵。 - 主线程不阻塞:所有API请求在后台线程执行,完成后切回主线程更新UI,保证滚动流畅。
- 避免重复请求:ViewModel中判断景点是否已有额外数据,防止重复发起相同请求。
内容的提问来源于stack exchange,提问作者user5705458
相关产品推荐
相关产品推荐

