我在异步网络调用后面临更新底部工作表视图 (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)
}
}
好吧,由于您的代码既不是最小的也不是可重现的,我会尝试给您一个提示,告诉您如何处理这种情况。再说一次,你的代码在我的机器上不起作用,而且太长了。代码应该与重现问题一样长,也应该模拟网络调用,就像我在这里所做的那样:
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 属性,从而修改工作表的高度。 让我知道这是否对您有帮助。