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

异步任务中Bottom Sheet Detent更新失败问题求助

异步请求完成后Bottom Sheet Detent更新失效的解决思路

问题场景

在ParentView中点击按钮,以Bottom Sheet形式展示PlannerView,其detent尺寸由bottomSheetDetent控制,默认设为.large。根据ViewModel的routePath.isEmpty或chargerLocations.isEmpty条件,动态配置可选的detent集合:条件满足时允许.large和.medium,否则仅保留.medium。

PlannerView发起异步网络请求后,期望将detent切换为.medium,让用户能与底层的iOS 17 Map视图交互,但detent更新未生效。

核心问题分析

  • detent集合与选择的同步冲突:当ViewModel数据更新后,可选detent集合会从[.large, .medium]变为[.medium],此时直接设置bottomSheetDetent = .medium,SwiftUI可能因集合变更的时序问题无法正确响应状态更新。
  • 异步操作时序混乱:原代码用DispatchQueue.main.asyncAfter延迟设置detent,与ViewModel的异步请求无关联,无法保证在请求完成后执行;且ViewModel中用DispatchQueue.main.asyncAfter模拟网络请求,不符合Swift Concurrency规范,导致状态更新时机不可控。
  • 状态响应链路断裂:PlannerView直接修改绑定的bottomSheetDetent,但ParentView中presentationDetents的集合依赖ViewModel状态,两者未建立可靠的同步机制。

解决方案

  1. 统一用Swift Concurrency处理异步逻辑:替换ViewModel中的DispatchQueue.main.asyncAfter为Task.sleep,确保状态更新在@MainActor下正确触发视图刷新。
  2. 建立ViewModel与detent的响应绑定:在ParentView中通过onReceive监听ViewModel的routePath变化,自动更新bottomSheetDetent,确保数据就绪后detent自动切换为.medium。
  3. 优化detent集合的动态逻辑:将detent选项提取为计算属性,增强可读性,同时保证集合变更与detent选择的更新顺序正确。

修改后的代码示例

ViewModel优化

class ViewModel: ObservableObject {
    @Published var isLoading = false
    @Published var hasError = false
    @Published var routePath: [CLLocationCoordinate2D] = []
    @Published var chargerLocations: [CLLocationCoordinate2D] = [] // 补充原代码缺失的属性

    @MainActor
    func fetchRouteData(
        carModel: String,
        from startLocation: CLLocationCoordinate2D,
        to endLocation: CLLocationCoordinate2D,
        initialSocPerc: Int
    ) async {
        isLoading = true
        defer { isLoading = false }
        
        // 用Task.sleep模拟异步网络请求(单位:纳秒)
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        
        let mockData = Planner(result: Planner.Route(steps: [
            Planner.Step(path: [[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
        ]))
        handlePlannerData(mockData)
    }

    @MainActor
    func handlePlannerData(_ data: Planner) {
        guard let route = data.result?.steps.first else { return }
        
        var routePathCoordinates = [CLLocationCoordinate2D]()
        if let stepPath = route.path {
            let stepCoordinates = stepPath.map { CLLocationCoordinate2D(latitude: $0[0], longitude: $0[1]) }
            routePathCoordinates.append(contentsOf: stepCoordinates)
        }
        routePath = routePathCoordinates
        // 模拟chargerLocations数据,确保切换detent的条件不满足
        chargerLocations = [CLLocationCoordinate2D(latitude: 0.5, longitude: 0.5)]
    }
}

ParentView优化

struct ParentView: View {
    @EnvironmentObject var viewModel: ViewModel
    @State private var bottomSheetDetent: PresentationDetent = .large
    @State private var showingPlannerView = false
    
    var body: some View {
        Button("Show Planner") {
            showingPlannerView = true
        }
        .sheet(isPresented: $showingPlannerView) {
            PlannerView(showingPlannerView: $showingPlannerView)
                .presentationDetents(detentOptions, selection: $bottomSheetDetent)
                .presentationContentInteraction(.resizes)
                .presentationBackgroundInteraction(.enabled(upThrough: .medium))
                .presentationDragIndicator(.visible)
        }
        // 监听routePath变化,自动切换detent
        .onReceive(viewModel.$routePath) { _ in
            if !viewModel.routePath.isEmpty && !viewModel.chargerLocations.isEmpty {
                bottomSheetDetent = .medium
            }
        }
    }
    
    // 计算属性动态生成detent选项
    private var detentOptions: Set<PresentationDetent> {
        viewModel.routePath.isEmpty || viewModel.chargerLocations.isEmpty ? [.large, .medium] : [.medium]
    }
}

PlannerView优化

struct PlannerView: View {
    @EnvironmentObject var viewModel: ViewModel
    @Binding var showingPlannerView: Bool
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                Text("Loading route data...")
            } else {
                Text("Route Data Ready")
                // 展示路线数据
            }
            Button("Fetch Route") {
                Task {
                    await viewModel.fetchRouteData(
                        carModel: "CarModel",
                        from: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0),
                        to: CLLocationCoordinate2D(latitude: 1.0, longitude: 1.0),
                        initialSocPerc: 50
                    )
                }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者Emre Değirmenci

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 07:44:54