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

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)
    }
}
方案优势
  1. 懒加载减少请求量:仅当卡片进入可视区域时才发起额外数据请求,避免一次性加载所有数据导致的网络和UI压力。
  2. 并发控制避免过载:批量请求时通过flatMap(maxPublishers:)限制同时请求数量,防止网络请求拥堵。
  3. 主线程不阻塞:所有API请求在后台线程执行,完成后切回主线程更新UI,保证滚动流畅。
  4. 避免重复请求:ViewModel中判断景点是否已有额外数据,防止重复发起相同请求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 15:37:03