异步任务中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状态,两者未建立可靠的同步机制。
解决方案
- 统一用Swift Concurrency处理异步逻辑:替换ViewModel中的
DispatchQueue.main.asyncAfter为Task.sleep,确保状态更新在@MainActor下正确触发视图刷新。 - 建立ViewModel与detent的响应绑定:在ParentView中通过
onReceive监听ViewModel的routePath变化,自动更新bottomSheetDetent,确保数据就绪后detent自动切换为.medium。 - 优化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
相关产品推荐
相关产品推荐

