diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 1a9d00ea4..8521d3ed8 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -25,8 +25,8 @@ struct FloatingPanel: View where Content: View { let attributionBarHeight: CGFloat /// The background color of the floating panel. let backgroundColor: Color? - /// A binding to the currently selected detent. - @Binding var selectedDetent: FloatingPanelDetent + /// A binding to the current active detent. + @Binding var activeDetent: FloatingPanelDetent /// A binding to a Boolean value that determines whether the view is presented. @Binding var isPresented: Bool /// The content shown in the floating panel. @@ -52,6 +52,12 @@ struct FloatingPanel: View where Content: View { /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .zero + /// Stores the detent that was active before a `FloatingPanelDetent.Preference` was applied. + /// + /// This value allows the panel to restore the previous state when the preference is cleared. + /// It is only set when a new preference is first applied and cleared when the preference is removed. + @State private var overriddenDetent: FloatingPanelDetent? + var body: some View { GeometryReader { geometryProxy in VStack(spacing: 0) { @@ -64,6 +70,43 @@ struct FloatingPanel: View where Content: View { .padding(.bottom, isPortraitOrientation ? keyboardHeight - geometryProxy.safeAreaInsets.bottom : .zero) .frame(height: height) .clipped() + .onPreferenceChange(FloatingPanelDetent.Preference.self) { preference in +#if swift(<6.0.3) || swift(>=6.1) // Xcode 16.2 (Swift 6.0.3) needs special handling + if let preference { + // Only set the overridden detent if it's `nil`. + // This prevents a preference from being saved + // as the overridden detent. + if overriddenDetent == nil { + overriddenDetent = activeDetent + } + activeDetent = preference + } else if let overriddenDetent { + // When the preference is unset, restore the + // overridden detent as the active detent. + activeDetent = overriddenDetent + self.overriddenDetent = nil + } +#else + Task { @MainActor in + if let preference { + // Only update the overridden detent if one + // wasn't already saved. This prevents a + // FloatingPanelDetentPreference from being + // saved as the overridden detent. + if overriddenDetent == nil { + overriddenDetent = activeDetent + } + activeDetent = preference + } else if let overriddenDetent { + // When the FloatingPanelDetentPreference is + // unset, restore the overridden detent as the + // active detent. + activeDetent = overriddenDetent + self.overriddenDetent = nil + } + } +#endif + } if !isPortraitOrientation { Divider() makeHandleView() @@ -98,7 +141,7 @@ struct FloatingPanel: View where Content: View { .onChange(of: isPresented) { updateHeight() } - .onChange(of: selectedDetent) { + .onChange(of: activeDetent) { updateHeight() } .onKeyboardStateChanged { state, height in @@ -138,11 +181,18 @@ struct FloatingPanel: View where Content: View { let predictedEndLocation = $0.predictedEndLocation.y let inferredHeight = isPortraitOrientation ? maximumHeight - predictedEndLocation : predictedEndLocation - selectedDetent = [.summary, .half, .full] + activeDetent = [.summary, .half, .full] .map { (detent: $0, height: heightFor(detent: $0)) } .min { abs(inferredHeight - $0.height) < abs(inferredHeight - $1.height) }! .detent + if overriddenDetent != nil { + // Update the overridden detent with the user's choice to + // prevent the user's choice from being unset when the + // FloatingPanelDetentPreference is unset. + overriddenDetent = activeDetent + } + if $0.translation.height.magnitude > 100 { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } @@ -180,7 +230,7 @@ struct FloatingPanel: View where Content: View { } else if keyboardState == .opening || keyboardState == .open { return heightFor(detent: .full) } else { - return heightFor(detent: selectedDetent) + return heightFor(detent: activeDetent) } }() withAnimation { height = max(0, (newHeight - .handleFrameHeight)) } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.Preference.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.Preference.swift new file mode 100644 index 000000000..81a622ff7 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.Preference.swift @@ -0,0 +1,31 @@ +// Copyright 2025 Esri +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI + +extension FloatingPanelDetent { + /// Use this preference to override the active FloatingPanelDetent. + /// + /// This can be used when a view shown in a Floating Panel needs to communicate that the view behind + /// the Floating Panel should be revealed (e.g. to reveal a map for user interaction). + /// + /// When the Floating Panel can be re-expanded, set the preference to `nil`. + struct Preference: PreferenceKey { + static let defaultValue: FloatingPanelDetent? = nil + + static func reduce(value: inout FloatingPanelDetent?, nextValue: () -> FloatingPanelDetent?) { + value = nextValue() + } + } +} diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index 1e7657088..710a9e2ae 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -125,7 +125,7 @@ private struct FloatingPanelModifier: ViewModifier where PanelCont FloatingPanel( attributionBarHeight: attributionBarHeight, backgroundColor: backgroundColor, - selectedDetent: boundDetent ?? $managedDetent, + activeDetent: boundDetent ?? $managedDetent, isPresented: isPresented, content: panelContent )