异步任务中的底板制动更新

问题描述 投票:0回答:1

我在异步网络调用后面临更新底部工作表视图 (PlannerView) 的定位大小的问题。设置如下:

在我的 ParentView 中,有一个按钮可以触发 PlannerView 作为底部工作表的呈现。该底部片材的定位尺寸由 plannerBottomSheetDetent 控制,默认设置为 .medium。但是,根据我的 ViewModel 中的某些条件(viewModel.routePath.isEmpty 或 viewModel.locations.isEmpty),定位尺寸应更改为 .large。如果不满足这些条件,则应保持为.中。

enum BottomSheetType: Int, Identifiable {
    case poiDetailView, routePlannerDetailView
    
    var id :Int {
        return self.rawValue
    }
}

struct ParentView: View {
    @EnvironmentObject var viewModel: ViewModel
    
    @State private var plannerBottomSheet: Bool = false
    @State private var plannerBottomSheetDetent: PresentationDetent = .medium
    @State private var poiData: POIDatum?
    @State private var routePlannerCharger: Charger?
    
    @Binding var selectedResult: MKMapItem?
    @Binding var bottomSheetType: BottomSheetType?
    
    var body: some View {
        NavigationStack {
            ZStack {
                mapView
                    .mapStyle(
                        .standard(
                            elevation: .automatic,
                            pointsOfInterest: .excluding(
                                [.atm]
                            )
                        )
                    )
                    .onAppear {
                        selectedTabBarButton = "EV"
                    }
                
                VStack(spacing: 50) {
                    HStack {
                        Spacer()
                        routePlannerButton
                    }
                    .padding(.bottom, 120)
                }
                .edgesIgnoringSafeArea(.bottom)
            }
            
            var mapView: some View {
                Map(
                    position: $viewModel.cameraPosition,
                    selection: $selectedResult
                ) {
                    if !mapViewModel.routePath.isEmpty || !mapViewModel.chargerLocations.isEmpty,
                       let yourLocation = mapViewModel.routePath.first,
                       let targetLocation = mapViewModel.routePath.last
                    {
                        Marker("Your location", coordinate: yourLocation)
                        Marker("Target location", coordinate: targetLocation)
                        
                        ForEach(mapViewModel.chargerLocations, id: \.self) { chargerLocation in
                            let charger = mapViewModel.routePlannerDataMap[chargerLocation]
                            Annotation(
                                "Charger",
                                coordinate: CLLocationCoordinate2D(
                                    latitude: chargerLocation.latitude,
                                    longitude: chargerLocation.longitude
                                )
                            ) {
                                Button(action: {
                                    self.routePlannerCharger = charger
                                    bottomSheetType = .routePlannerDetailView
                                    let selectedRoutePlannerChargerMapItem = MKMapItem(
                                        placemark: MKPlacemark(
                                            coordinate: chargerLocation
                                        )
                                    )
                                    selectedResult = selectedRoutePlannerChargerMapItem
                                }) {
                                    Image(systemName: "circle.fill")
                                        .resizable()
                                        .foregroundStyle(.orange)
                                        .frame(width: 24, height: 24)
                                        .background(.white)
                                        .clipShape(.circle)
                                }
                            }
                        }
                        
                        MapPolyline(coordinates: mapViewModel.routePath, contourStyle: .geodesic)
                            .stroke(.red, lineWidth: 5)
                    } else {
                        
                        ForEach(mapViewModel.annotations) { result in
                            let poiDatum = mapViewModel.poiDataMap[result]
                            Annotation(result.placemark.name ?? "", coordinate: result.coordinate) {
                                Button(action: {
                                    self.poiData = poiDatum
                                    bottomSheetType = .poiDetailView
                                    selectedResult = result
                                }) {
                                    Image(systemName: "circle.fill")
                                        .resizable()
                                        .foregroundStyle(.red)
                                        .frame(width: 24, height: 24)
                                        .background(.white)
                                        .clipShape(.circle)
                                }
                            }
                        }
                        UserAnnotation()
                    }
                }
                .readSize(onChange: { newValue in
                    mapViewModel.mapSize = newValue
                })
                .onChange(of: selectedResult) {
                    if (
                        !mapViewModel.routePath.isEmpty || !mapViewModel.chargerLocations.isEmpty
                    ) {
                        if let selecedMapItem = selectedResult {
                            self.routePlannerCharger = mapViewModel.routePlannerDataMap[selecedMapItem.coordinate]
                            bottomSheetType = .routePlannerDetailView
                        }
                    } else {
                        if let selecedMapItem = selectedResult {
                            self.poiData = mapViewModel.poiDataMap[selecedMapItem]
                            bottomSheetType = .poiDetailView
                        }
                    }
                }
                .sheet(item: $bottomSheetType, content: { type in
                    switch type {
                        case .poiDetailView:
                            ItemInfoView(
                                navigateToImagePicker: $navigateToImagePicker,
                                poiData: $poiData,
                                routePlannerCharger: Binding.constant(nil),
                                selectedTabBarButton: $selectedTabBarButton,
                                selectedResult: $selectedResult,
                                bottomSheetType: $bottomSheetType,
                                travelViewModel: travelViewModel,
                                url: self.shareLocation()
                            )
                            .presentationDetents(
                                [/*.large,*/ .medium]
                            )
                            .presentationBackgroundInteraction(.enabled(upThrough: .medium))
                            .presentationDragIndicator(.visible)
                        case .routePlannerDetailView:
                            ItemInfoView(
                                navigateToImagePicker: $navigateToImagePicker,
                                poiData: Binding.constant(nil),
                                routePlannerCharger: $routePlannerCharger,
                                selectedTabBarButton: $selectedTabBarButton,
                                selectedResult: $selectedResult,
                                bottomSheetType: $bottomSheetType,
                                travelViewModel: travelViewModel,
                                url: self.shareLocation()
                            )
                            .presentationDetents(
                                [/*.large,*/ .medium]
                            )
                            .presentationBackgroundInteraction(.enabled(upThrough: .medium))
                            .presentationDragIndicator(.visible)
                    }
                })
                .onMapCameraChange { context in
                    mapViewModel.currentRegion = context.region
                }
                .onMapCameraChange(frequency: .onEnd) { context in
                    Task.detached {
                        await mapViewModel.reloadAnnotations()
                    }
                }
            }
            
            var plannerButton: some View {
                Button(action: {
                    plannerBottomSheetDetent = true
                    if viewModel.routePath.isEmpty || viewModel.chargerLocations.isEmpty {
                        plannerBottomSheetDetent = .large
                    } else {
                        plannerBottomSheetDetent = .medium
                    }
                }) {
                    Image(systemName: "point.arrowtriangle.uturn.scurvepath.fill")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 20, height: 20)
                        .padding()
                }
                .background(Color("backgroundText").opacity(0.6))
                .cornerRadius(8)
                .shadow(radius: 8)
                .padding([.trailing, .bottom], 4)
                .sheet(isPresented: $plannerBottomSheet) {
                    PlannerView(
                        plannerBottomSheetDetent: $plannerBottomSheetDetent,
                        plannerBottomSheet: $plannerBottomSheet
                    )
                    .presentationDetents(
                        ((viewModel.routePath.isEmpty || viewModel.locations.isEmpty) ? [.large, .medium] : [.medium]),
                        selection: $plannerBottomSheetDetent
                    )
                    .presentationContentInteraction(.resizes)
                    .presentationBackgroundInteraction(.enabled(upThrough: .medium))
                    .presentationDragIndicator(.visible)
                }
            }
        }
    }
}

PlannerView 负责进行异步网络调用。我的目的是让 PlannerView 在成功完成此网络调用后将其制动尺寸更新为 .medium。这对于允许用户与底层 ParentView 交互至关重要,这是我的应用程序中的 iOS 17 地图视图。

但是,我遇到了一个问题:PlannerView 中的网络调用完成后,制动尺寸没有按照我的预期更新为 .medium。我正在寻求有关如何确保 PlannerView 在网络调用后将其制动尺寸更新为 .medium 的建议,从而实现与 ParentView 的正确交互。任何有关如何解决此问题的建议或见解将不胜感激。

 enum Sheet: String, Identifiable {
        case manufacturer, targetSearch
        var id: String { rawValue }
    }
    
    struct PlannerView: View {
        @Binding var plannerBottomSheetDetent: PresentationDetent
        @Binding var plannerBottomSheet: Bool
        @State private var presentedSheet: Sheet?
        @State private var isPlanningRoute = false
        
        init(
            plannerBottomSheetDetent: Binding<PresentationDetent>,
            plannerBottomSheet: Binding<Bool>
        ) {
            self._plannerBottomSheetDetent = plannerBottomSheetDetent
            self._plannerBottomSheet = plannerBottomSheet
            self.loadSelections()
        }
        
        var body: some View {
            List {
                Section() {}
                
                Button(action: planRoute) {
                    if isPlanningRoute {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: .white))
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                    } else {
                        Text("Plan Route")
                            .font(.mediumTitle)
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .cornerRadius(40)
                    }
                }
                .disabled(selectedTypecode == nil || selectedVehicleDetails == nil || isPlanningRoute)
                .buttonStyle(CustomButtonStyle())
                .listStyle(GroupedListStyle())
                .navigationBarTitle("Route Planner", displayMode: .inline)
                .sheet(item: $presentedSheet, content: { sheet in
                    switch sheet {
                        case .manufacturer:
                            ManufacturerSelectionView()
                                .presentationDetents([.large])
                                .presentationDragIndicator(.visible)
                        case .targetSearch:
                            TargetLocationSearchView()
                    }
                })
                .ignoresSafeArea(.keyboard, edges: .bottom)
                .onAppear {
                    loadSelections() // Keychain reading
                }
            }
        }
        
        private func planRoute() {
            guard let userLocation = locationManager.userLocation?.coordinate,
                  let selectedTypecode = selectedTypecode else {
                return
            }
            
            let targetLocation = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
            
            isPlanningRoute = true
            saveSelections() // Keychain saving
            
            Task {
                do {
                    try await viewModel.fetchRouteData(
                        carModel: selectedTypecode,
                        from: userLocation,
                        to: targetLocation,
                        initialSocPerc: Int(currentCharge)
                    )
                    self.isPlanningRoute = false
                } catch {
                    self.isPlanningRoute = false
                }
            }
        }
    }

最后是进行相关 API 调用并检索数据的 ViewModel 代码:

final class ViewModel: ObservableObject {
    @Published var isLoading = false
    @Published var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)
    @Published var currentRegion: MKCoordinateRegion = .userLocation
    @Published var routePath: [CLLocationCoordinate2D] = []
    @Published var chargerLocations: [CLLocationCoordinate2D] = []
    
    @MainActor
    func fetchRouteData(
        carModel: String,
        from startLocation: CLLocationCoordinate2D,
        to endLocation: CLLocationCoordinate2D,
        initialSocPerc: Int
    ) async throws {
        isLoading = true
        defer { isLoading = false }
        let startDestination = Destination(
            lat: startLocation.latitude,
            lon: startLocation.longitude,
            address: nil
        )
        let endDestination = Destination(
            lat: endLocation.latitude,
            lon: endLocation.longitude,
            address: nil
        )
        
        do {
            let response: RoutePlanner = try await NetworkingManager.shared.request(
                .planRoute(
                    carModel: carModel,
                    destinations: [startDestination, endDestination],
                    initialSocPerc: initialSocPerc
                ),
                type: RoutePlanner.self
            )
            self.handleRoutePlannerData(response)
        } catch {
            self.hasError = true
            self.error = error as? NetworkingManager.NetworkingError
            self.isLoading = false
        }
    }
    
    @MainActor
    func handleRoutePlannerData(_ data: RoutePlanner) {
        guard let route = data.result?.routes?.first else { return }
        
        route.steps?.forEach { step in
            if (step.isCharger ?? false) || (step.chargeDuration ?? 0) > 0 {
                chargerPathCoordinates.append(
                    CLLocationCoordinate2D(
                        latitude: step.lat ?? 0.0,
                        longitude: step.lon ?? 0.0
                    )
                )
                
                let coordinate = CLLocationCoordinate2D(
                    latitude: step.lat ?? 0.0,
                    longitude: step.lon ?? 0.0
                )
                self.routePlannerDataMap[coordinate] = step.charger
            }
            if let stepPath = step.path {
                let stepCoordinates = stepPath.map {
                    CLLocationCoordinate2D(
                        latitude: $0[0],
                        longitude: $0[1]
                    )
                }
                routePathCoordinates.append(contentsOf: stepCoordinates)
            }
        }
        self.currentRegion = MKCoordinateRegion(routePathCoordinates.map {
            MKMapRect(
                origin: .init($0),
                size: .init(width: 1, height: 1)
            )
        }
            .reduce(MKMapRect.null) { $0.union($1) })
        cameraPosition = .region(currentRegion)
    }
}
swiftui async-await bottom-sheet ios17
1个回答
0
投票

好吧,由于您的代码既不是最小的也不是可重现的,我会尝试给您一个提示,告诉您如何处理这种情况。再说一次,你的代码在我的机器上不起作用,而且太长了。代码应该与重现问题一样长,也应该模拟网络调用,就像我在这里所做的那样:

struct ContentView: View {
    
    @StateObject private var vm = ViewModel()
    @State private var show = false
    
    var body: some View {
        NavigationStack {
            Button("Show") {
                show.toggle()
            }
            .sheet(isPresented: $show, content: {
                VStack {
                    Text("My awesome sheet is on detent \(vm.detent == .medium ? "Medium" : "Large")")
                        .presentationDetents([vm.detent])
                        .presentationBackgroundInteraction(.enabled(upThrough: .medium))
                        .presentationDragIndicator(.visible)
                        .task {
                            await myFantasticTask()
                        }
                        .onDisappear {
                            vm.detent = .medium
                        }
                }
            })
        }
    }
    
    private func myFantasticTask() async {
        /// Your async stuff here
        try? await Task.sleep(for: .seconds(Int.random(in: 1...3)))
        let condition = vm.myCondition
        print("Condition \(condition)")
        if condition {
            vm.detent = PresentationDetent.large
        }
    }
    
}

class ViewModel: ObservableObject {
    
    @Published var detent: PresentationDetent = .medium
    
    var myCondition: Bool {
        Bool.random()
    }
    
}

此代码模拟一个简单的异步任务,该任务基于计算变量表示的给定条件修改 ViewModel 的 detent 属性,从而修改工作表的高度。 让我知道这是否对您有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.