From 9ed84d950be2761f33655129b4e6aee792557788 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 9 May 2025 16:47:01 -0700 Subject: [PATCH 1/4] Add `FloatingPanelDetent.Preference` --- .../FloatingPanel/FloatingPanel.swift | 40 ++++++++++++++++--- .../FloatingPanelDetent.Preference.swift | 31 ++++++++++++++ .../FloatingPanel/FloatingPanelModifier.swift | 2 +- 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.Preference.swift diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 1a9d00ea4..d4da1353b 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,11 @@ struct FloatingPanel: View where Content: View { /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .zero + /// The detent that was the active detent until a FloatingPanelDetentPreference was set. + /// + /// When the FloatingPanelDetentPreference is unset, this detent should be restored to the active detent.. + @State private var overriddenDetent: FloatingPanelDetent? + var body: some View { GeometryReader { geometryProxy in VStack(spacing: 0) { @@ -64,6 +69,24 @@ 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 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 + } + } if !isPortraitOrientation { Divider() makeHandleView() @@ -98,7 +121,7 @@ struct FloatingPanel: View where Content: View { .onChange(of: isPresented) { updateHeight() } - .onChange(of: selectedDetent) { + .onChange(of: activeDetent) { updateHeight() } .onKeyboardStateChanged { state, height in @@ -138,11 +161,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 +210,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 ) From af280a50acfa831b6aa63ac81d27b9671d37ceea Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 9 May 2025 17:25:56 -0700 Subject: [PATCH 2/4] Doc --- .../FloatingPanel/FloatingPanel.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index d4da1353b..284c6e231 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -52,9 +52,10 @@ struct FloatingPanel: View where Content: View { /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .zero - /// The detent that was the active detent until a FloatingPanelDetentPreference was set. + /// Stores the detent that was active before a `FloatingPanelDetent.Preference` was applied. /// - /// When the FloatingPanelDetentPreference is unset, this detent should be restored to the active detent.. + /// 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 { @@ -71,18 +72,16 @@ struct FloatingPanel: View where Content: View { .clipped() .onPreferenceChange(FloatingPanelDetent.Preference.self) { preference 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. + // 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 FloatingPanelDetentPreference is - // unset, restore the overridden detent as the - // active detent. + // When the preference is unset, restore the + // overridden detent as the active detent. activeDetent = overriddenDetent self.overriddenDetent = nil } From e81b5a092c923371f40b585a52c29ff45784f6de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 16 May 2025 16:06:41 -0700 Subject: [PATCH 3/4] Apply Xcode 16.2 patch --- .../FloatingPanel/FloatingPanel.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 284c6e231..36bc72db1 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -71,6 +71,7 @@ struct FloatingPanel: View where Content: View { .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 @@ -85,6 +86,26 @@ struct FloatingPanel: View where Content: View { 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() From 19348749c8ec200cc9d6fc8d54fdf3d619f67c7e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 16 May 2025 16:07:45 -0700 Subject: [PATCH 4/4] Doc --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 36bc72db1..8521d3ed8 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -71,7 +71,7 @@ struct FloatingPanel: View where Content: View { .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 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