From 5755c5b0435bd3e57afae0ee39ae91e23b2f7c76 Mon Sep 17 00:00:00 2001 From: Joel Koo Date: Wed, 20 Dec 2023 17:12:25 +0900 Subject: [PATCH 01/10] Add hidden property Add Detents annotations --- Sources/BottomSheet/BottomSheet.swift | 5 ++- .../BottomSheet/Detents/DetentDefaults.swift | 1 + .../BottomSheet/Detents/DetentHelpers.swift | 2 + Sources/BottomSheet/Detents/Detents.swift | 37 +++++++++++++++++++ .../Preference Keys/ConfigKey.swift | 2 +- 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Sources/BottomSheet/BottomSheet.swift b/Sources/BottomSheet/BottomSheet.swift index 3bb7cf6..323b964 100644 --- a/Sources/BottomSheet/BottomSheet.swift +++ b/Sources/BottomSheet/BottomSheet.swift @@ -144,7 +144,10 @@ struct SheetPlus: ViewModifier .onPreferenceChange(SheetPlusKey.self) { value in /// Quick hack to prevent the scrollview from resetting the height when keyboard shows up. /// Replace if the root cause has been located. - if value.detents.count == 0 { return } + if value.detents.count == 0 || value.selectedDetent == .hidden { + isPresented = false + return + } sheetConfig = value translation = value.translation diff --git a/Sources/BottomSheet/Detents/DetentDefaults.swift b/Sources/BottomSheet/Detents/DetentDefaults.swift index da8a3aa..f6c2fb1 100644 --- a/Sources/BottomSheet/Detents/DetentDefaults.swift +++ b/Sources/BottomSheet/Detents/DetentDefaults.swift @@ -11,4 +11,5 @@ internal struct PresentationDetentDefaults { static let small: CGFloat = UIScreen.main.bounds.height * 0.2 static let medium: CGFloat = UIScreen.main.bounds.height * 0.5 static let large: CGFloat = UIScreen.main.bounds.height * 0.9 + static let hidden: CGFloat = 0 } diff --git a/Sources/BottomSheet/Detents/DetentHelpers.swift b/Sources/BottomSheet/Detents/DetentHelpers.swift index 2aa2689..94f55ce 100644 --- a/Sources/BottomSheet/Detents/DetentHelpers.swift +++ b/Sources/BottomSheet/Detents/DetentHelpers.swift @@ -20,6 +20,8 @@ internal func detentLimits(detents: Set) -> (min: CGFloat, m return PresentationDetentDefaults.medium case .large: return PresentationDetentDefaults.large + case .hidden: + return PresentationDetentDefaults.hidden case .fraction(let fraction): return UIScreen.main.bounds.height * fraction case .height(let height): diff --git a/Sources/BottomSheet/Detents/Detents.swift b/Sources/BottomSheet/Detents/Detents.swift index 2e2aa16..b6b7a09 100644 --- a/Sources/BottomSheet/Detents/Detents.swift +++ b/Sources/BottomSheet/Detents/Detents.swift @@ -7,11 +7,46 @@ import SwiftUI +/** + An enumeration to represent various form of PresentationDetent + + - `small`: A small sized bottom sheet. `.fraction(0.2)` + - `medium`: A medium sized bottom sheet, `.fraction(0.5)` + - `large`: A large sized bottom sheet. `.fraction(0.9)` + - `hidden`: Hide bottom sheet. + - `fraction`: Relative to screen height. + - `height`: A constant height. + */ + public enum PresentationDetent: Hashable { + /** + .fraction(0.2) + */ case small + + /** + .fraction(0.5) + */ case medium + + /** + .fraction(0.9) + */ case large + + /** + Hide bottom sheet + */ + case hidden + + /** + CGFloat 0 to 1 + */ case fraction(CGFloat) + + /** + A constant height. + */ case height(CGFloat) public var size: CGFloat { @@ -22,6 +57,8 @@ public enum PresentationDetent: Hashable { return PresentationDetentDefaults.medium case .large: return PresentationDetentDefaults.large + case .hidden: + return PresentationDetentDefaults.hidden case .fraction(let fraction): return min( UIScreen.main.bounds.height * fraction, diff --git a/Sources/BottomSheet/Preference Keys/ConfigKey.swift b/Sources/BottomSheet/Preference Keys/ConfigKey.swift index 6837078..c9d2b26 100644 --- a/Sources/BottomSheet/Preference Keys/ConfigKey.swift +++ b/Sources/BottomSheet/Preference Keys/ConfigKey.swift @@ -19,7 +19,7 @@ struct SheetPlusConfig: Equatable { } struct SheetPlusKey: PreferenceKey { - static var defaultValue: SheetPlusConfig = SheetPlusConfig(detents: [], selectedDetent: .constant(.height(.zero)), translation: 0) + static var defaultValue: SheetPlusConfig = SheetPlusConfig(detents: [], selectedDetent: .constant(.hidden), translation: 0) static func reduce(value: inout SheetPlusConfig, nextValue: () -> SheetPlusConfig) { /// This prevents the translation changes to be called whenever the keyboard is triggered. From 397660b12374e922445ee9b413082814e8323d85 Mon Sep 17 00:00:00 2001 From: Wouter Date: Wed, 27 Dec 2023 23:12:06 +0100 Subject: [PATCH 02/10] feat: added initial setup for sheet dismissal through swipe --- .../BottomSheetExample/ExampleOverview.swift | 5 +-- .../Examples/StaticScrollViewExample.swift | 2 +- Sources/BottomSheet/BottomSheet.swift | 32 +++++++++++++++---- .../BottomSheet/Detents/DetentDefaults.swift | 1 - .../BottomSheet/Detents/DetentHelpers.swift | 2 -- Sources/BottomSheet/Detents/Detents.swift | 8 ----- Sources/BottomSheet/Helpers/Snapping.swift | 26 ++++++++++++--- .../Preference Keys/ConfigKey.swift | 4 +-- .../InteractiveDismissKey.swift | 17 ++++++++++ .../UIKit Views/UIScrollViewWrapper.swift | 8 +++-- .../View+InteractiveDismiss.swift | 20 ++++++++++++ 11 files changed, 94 insertions(+), 31 deletions(-) create mode 100644 Sources/BottomSheet/Preference Keys/InteractiveDismissKey.swift create mode 100644 Sources/BottomSheet/View Modifiers/View+InteractiveDismiss.swift diff --git a/Example/BottomSheetExample/ExampleOverview.swift b/Example/BottomSheetExample/ExampleOverview.swift index 05eaedb..af4454a 100644 --- a/Example/BottomSheetExample/ExampleOverview.swift +++ b/Example/BottomSheetExample/ExampleOverview.swift @@ -53,11 +53,12 @@ struct ExampleOverview: View { case .staticScrollView: StaticScrollViewContent() .presentationDetentsPlus( - [.height(244), .height(380), .height(480), .large], + [.height(380), .height(480), .large], selection: $settings.selectedDetent ) .presentationDragIndicatorPlus(.visible) - .presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380))) +// .presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380))) + .interactiveDismissDisabledPlus(false) default: EmptyView() } diff --git a/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift b/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift index 93b1cdc..6ceaa46 100644 --- a/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift +++ b/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift @@ -12,7 +12,7 @@ struct StaticScrollViewExample: View { var body: some View { VStack { - Button("Close") { + Button(settings.isPresented ? "Close" : "Show") { settings.isPresented.toggle() } diff --git a/Sources/BottomSheet/BottomSheet.swift b/Sources/BottomSheet/BottomSheet.swift index 323b964..da136f6 100644 --- a/Sources/BottomSheet/BottomSheet.swift +++ b/Sources/BottomSheet/BottomSheet.swift @@ -7,14 +7,15 @@ import SwiftUI -struct SheetPlus: ViewModifier, KeyboardReader { +struct SheetPlus: ViewModifier { @Binding private var isPresented: Bool @State private var translation: CGFloat = 0 @State private var sheetConfig: SheetPlusConfig? @State private var showDragIndicator: VisibilityPlus? @State private var allowBackgroundInteraction: PresentationBackgroundInteractionPlus? - + @State private var isInteractiveDismissDisabled = true + @State private var newValue = 0.0 @State private var startTime: DragGesture.Value? @@ -88,8 +89,15 @@ struct SheetPlus: ViewModifier let yVelocity: CGFloat = -1 * ((distance / time) / 1000) startTime = nil - - if let result = snapBottomSheet(translation, detents, yVelocity) { + + print(isInteractiveDismissDisabled) + + if let result = snapBottomSheet( + translation, + detents, + yVelocity, + isInteractiveDismissDisabled + ) { translation = result.size sheetConfig?.selectedDetent = result } @@ -99,6 +107,7 @@ struct SheetPlus: ViewModifier UIScrollViewWrapper( translation: $translation, preferenceKey: $sheetConfig, + isInteractiveDismissDisabled: $isInteractiveDismissDisabled, limits: limits, detents: detents ) { @@ -115,17 +124,23 @@ struct SheetPlus: ViewModifier .onChange(of: translation) { newValue in // Small little hack to make the iOS scroll behaviour work smoothly if limits.max == 0 { return } - translation = min(limits.max, max(newValue, limits.min)) + + let minValue = isInteractiveDismissDisabled ? limits.min : 0 + translation = min(limits.max, max(newValue, minValue)) currentGlobalTranslation = translation } .onAnimationChange(of: translation) { value in onDrag(value) + + if value <= 0 && sheetConfig?.selectedDetent == .height(.zero) { + isPresented = false + } } .offset(y: UIScreen.main.bounds.height - translation) .onDisappear { translation = 0 - detents = [] +// detents = [] onDismiss() } @@ -144,7 +159,7 @@ struct SheetPlus: ViewModifier .onPreferenceChange(SheetPlusKey.self) { value in /// Quick hack to prevent the scrollview from resetting the height when keyboard shows up. /// Replace if the root cause has been located. - if value.detents.count == 0 || value.selectedDetent == .hidden { + if value.detents.count == 0 { isPresented = false return } @@ -161,5 +176,8 @@ struct SheetPlus: ViewModifier .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in allowBackgroundInteraction = value } + .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in + isInteractiveDismissDisabled = value + } } } diff --git a/Sources/BottomSheet/Detents/DetentDefaults.swift b/Sources/BottomSheet/Detents/DetentDefaults.swift index f6c2fb1..da8a3aa 100644 --- a/Sources/BottomSheet/Detents/DetentDefaults.swift +++ b/Sources/BottomSheet/Detents/DetentDefaults.swift @@ -11,5 +11,4 @@ internal struct PresentationDetentDefaults { static let small: CGFloat = UIScreen.main.bounds.height * 0.2 static let medium: CGFloat = UIScreen.main.bounds.height * 0.5 static let large: CGFloat = UIScreen.main.bounds.height * 0.9 - static let hidden: CGFloat = 0 } diff --git a/Sources/BottomSheet/Detents/DetentHelpers.swift b/Sources/BottomSheet/Detents/DetentHelpers.swift index 94f55ce..2aa2689 100644 --- a/Sources/BottomSheet/Detents/DetentHelpers.swift +++ b/Sources/BottomSheet/Detents/DetentHelpers.swift @@ -20,8 +20,6 @@ internal func detentLimits(detents: Set) -> (min: CGFloat, m return PresentationDetentDefaults.medium case .large: return PresentationDetentDefaults.large - case .hidden: - return PresentationDetentDefaults.hidden case .fraction(let fraction): return UIScreen.main.bounds.height * fraction case .height(let height): diff --git a/Sources/BottomSheet/Detents/Detents.swift b/Sources/BottomSheet/Detents/Detents.swift index b6b7a09..2714dbd 100644 --- a/Sources/BottomSheet/Detents/Detents.swift +++ b/Sources/BottomSheet/Detents/Detents.swift @@ -13,7 +13,6 @@ import SwiftUI - `small`: A small sized bottom sheet. `.fraction(0.2)` - `medium`: A medium sized bottom sheet, `.fraction(0.5)` - `large`: A large sized bottom sheet. `.fraction(0.9)` - - `hidden`: Hide bottom sheet. - `fraction`: Relative to screen height. - `height`: A constant height. */ @@ -33,11 +32,6 @@ public enum PresentationDetent: Hashable { .fraction(0.9) */ case large - - /** - Hide bottom sheet - */ - case hidden /** CGFloat 0 to 1 @@ -57,8 +51,6 @@ public enum PresentationDetent: Hashable { return PresentationDetentDefaults.medium case .large: return PresentationDetentDefaults.large - case .hidden: - return PresentationDetentDefaults.hidden case .fraction(let fraction): return min( UIScreen.main.bounds.height * fraction, diff --git a/Sources/BottomSheet/Helpers/Snapping.swift b/Sources/BottomSheet/Helpers/Snapping.swift index 976bc3c..c11e8b2 100644 --- a/Sources/BottomSheet/Helpers/Snapping.swift +++ b/Sources/BottomSheet/Helpers/Snapping.swift @@ -13,18 +13,34 @@ import Foundation /// - detents: the detents the translation can snap to /// - yVelocity: the speed at which the drag gesture ended. Used to compute a snapping behaviour /// - Returns: The snapping position distance -internal func snapBottomSheet(_ translation: CGFloat, _ detents: Set, _ yVelocity: CGFloat) -> PresentationDetent? { - let detents = detents.sorted(by: { $0.size < $1.size }) - +internal func snapBottomSheet( + _ translation: CGFloat, + _ detents: Set, + _ yVelocity: CGFloat, + _ isInteractiveDismissDisabled: Bool +) -> PresentationDetent? { + var detents = detents.sorted(by: { $0.size < $1.size }) + + // Insert a custom detent at 0. So we can hide the sheet all the way at the bottom. + if (!isInteractiveDismissDisabled) { + detents.insert(PresentationDetent.height(.zero), at: 0) + } + let position: [PresentationDetent] = detents.enumerated().compactMap { idx, detent in if idx < detents.index(before: detents.count) { let detentBracket = ( lower: detents[idx], middle: detents[idx].size + ((detents[idx + 1].size - detents[idx].size) / 2), upper: detents[idx + 1] - ) - + ) + if detentBracket.lower.size...detentBracket.upper.size ~= translation { + // Exception for snapping to dismiss position more easily on smaller sheets. + // Last swipe down is just to dismiss it always. + if (detentBracket.lower.size == 0 && translation < (detentBracket.upper.size - 50)) { + return detentBracket.lower + } + if abs(yVelocity) > 1.8 { return yVelocity > 0 ? detentBracket.upper : detentBracket.lower } else { diff --git a/Sources/BottomSheet/Preference Keys/ConfigKey.swift b/Sources/BottomSheet/Preference Keys/ConfigKey.swift index c9d2b26..a4ec7d4 100644 --- a/Sources/BottomSheet/Preference Keys/ConfigKey.swift +++ b/Sources/BottomSheet/Preference Keys/ConfigKey.swift @@ -19,8 +19,8 @@ struct SheetPlusConfig: Equatable { } struct SheetPlusKey: PreferenceKey { - static var defaultValue: SheetPlusConfig = SheetPlusConfig(detents: [], selectedDetent: .constant(.hidden), translation: 0) - + static var defaultValue: SheetPlusConfig = SheetPlusConfig(detents: [], selectedDetent: .constant(.height(.zero)), translation: 0) + static func reduce(value: inout SheetPlusConfig, nextValue: () -> SheetPlusConfig) { /// This prevents the translation changes to be called whenever the keyboard is triggered. /// If the keyboard gets triggered it will also reset the whole configkey and losing the binding. diff --git a/Sources/BottomSheet/Preference Keys/InteractiveDismissKey.swift b/Sources/BottomSheet/Preference Keys/InteractiveDismissKey.swift new file mode 100644 index 0000000..e9d8d69 --- /dev/null +++ b/Sources/BottomSheet/Preference Keys/InteractiveDismissKey.swift @@ -0,0 +1,17 @@ +// +// File.swift +// +// +// Created by Wouter van de Kamp on 25/12/2023. +// + +import Foundation +import SwiftUI + +struct SheetPlusInteractiveDismissDisabledKey: PreferenceKey { + static var defaultValue: Bool = true + + static func reduce(value: inout Bool, nextValue: () -> Bool) { + value = nextValue() + } +} diff --git a/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift b/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift index ff040fc..ced60a6 100644 --- a/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift +++ b/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift @@ -12,10 +12,11 @@ import UIKit internal struct UIScrollViewWrapper: UIViewRepresentable { @Binding var translation: CGFloat @Binding var preferenceKey: SheetPlusConfig? - + @Binding var isInteractiveDismissDisabled: Bool + let limits: (min: CGFloat, max: CGFloat) let detents: Set - + let content: () -> Content func makeUIView(context: Context) -> UIScrollView { @@ -138,7 +139,8 @@ internal struct UIScrollViewWrapper: UIViewRepresentable { if let result = snapBottomSheet( representable.translation, detents, - scrollView.contentOffset.y > 0 ? 0 : velocity.y + scrollView.contentOffset.y > 0 ? 0 : velocity.y, + representable.isInteractiveDismissDisabled ) { representable.translation = result.size representable.preferenceKey?.selectedDetent = result diff --git a/Sources/BottomSheet/View Modifiers/View+InteractiveDismiss.swift b/Sources/BottomSheet/View Modifiers/View+InteractiveDismiss.swift new file mode 100644 index 0000000..aaf33cd --- /dev/null +++ b/Sources/BottomSheet/View Modifiers/View+InteractiveDismiss.swift @@ -0,0 +1,20 @@ +// +// File.swift +// +// +// Created by Wouter van de Kamp on 25/12/2023. +// + +import Foundation +import SwiftUI + +extension View { + public func interactiveDismissDisabledPlus( + _ isDisabled: Bool + ) -> some View { + return self.preference( + key: SheetPlusInteractiveDismissDisabledKey.self, + value: isDisabled + ) + } +} From fcf9c84ad64b0ae48b209d21cb10b9fd974a7f72 Mon Sep 17 00:00:00 2001 From: Wouter Date: Mon, 1 Jan 2024 17:20:32 +0100 Subject: [PATCH 03/10] fix: added the ability to re-open after dismissing the view on the initial detent --- .../Apple Applications/StocksExample.swift | 1 + .../BottomSheetExample/ExampleOverview.swift | 4 +-- .../Examples/StaticScrollViewExample.swift | 1 + Sources/BottomSheet/BottomSheet.swift | 26 ++++++++++++------- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Example/BottomSheetExample/Apple Applications/StocksExample.swift b/Example/BottomSheetExample/Apple Applications/StocksExample.swift index 344ea16..5aab043 100644 --- a/Example/BottomSheetExample/Apple Applications/StocksExample.swift +++ b/Example/BottomSheetExample/Apple Applications/StocksExample.swift @@ -26,6 +26,7 @@ struct StocksExample: View { .navigationTitle("\(settings.translation.rounded())") .onAppear { settings.isPresented = true + settings.selectedDetent = .medium settings.activeSheetType = .stocks } } diff --git a/Example/BottomSheetExample/ExampleOverview.swift b/Example/BottomSheetExample/ExampleOverview.swift index af4454a..e9e7bb2 100644 --- a/Example/BottomSheetExample/ExampleOverview.swift +++ b/Example/BottomSheetExample/ExampleOverview.swift @@ -54,7 +54,7 @@ struct ExampleOverview: View { StaticScrollViewContent() .presentationDetentsPlus( [.height(380), .height(480), .large], - selection: $settings.selectedDetent + selection: $settings.selectedDetent // .constant(.medium) ) .presentationDragIndicatorPlus(.visible) // .presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380))) @@ -78,7 +78,7 @@ struct ExampleOverview: View { .onAppear { settings.isPresented = false settings.activeSheetType = .home - settings.selectedDetent = .medium + settings.selectedDetent = .height(.zero) } } .navigationViewStyle(.stack) diff --git a/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift b/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift index 6ceaa46..fd887a7 100644 --- a/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift +++ b/Example/BottomSheetExample/Examples/StaticScrollViewExample.swift @@ -25,6 +25,7 @@ struct StaticScrollViewExample: View { .navigationTitle("\(settings.translation.rounded())") .onAppear { settings.isPresented = true + settings.selectedDetent = .medium settings.activeSheetType = .staticScrollView } } diff --git a/Sources/BottomSheet/BottomSheet.swift b/Sources/BottomSheet/BottomSheet.swift index da136f6..d42136c 100644 --- a/Sources/BottomSheet/BottomSheet.swift +++ b/Sources/BottomSheet/BottomSheet.swift @@ -21,7 +21,9 @@ struct SheetPlus: ViewModifier @State private var detents: Set = [] @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) - + + @State private var initialSelectedDetent: PresentationDetent? = nil + let mainContent: MContent let headerContent: HContent let animationCurve: SheetAnimation @@ -53,7 +55,14 @@ struct SheetPlus: ViewModifier ZStack() { content .allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true) - + .onChange(of: isPresented) { newValue in + guard let initialSelectedDetent = initialSelectedDetent else { return } + + if sheetConfig?.selectedDetent == PresentationDetent.height(0.0) && newValue == true { + sheetConfig?.selectedDetent = initialSelectedDetent + } + } + if isPresented { GeometryReader { geometry in VStack(spacing: 0) { @@ -90,8 +99,6 @@ struct SheetPlus: ViewModifier let yVelocity: CGFloat = -1 * ((distance / time) / 1000) startTime = nil - print(isInteractiveDismissDisabled) - if let result = snapBottomSheet( translation, detents, @@ -103,7 +110,7 @@ struct SheetPlus: ViewModifier } } ) - + UIScrollViewWrapper( translation: $translation, preferenceKey: $sheetConfig, @@ -139,9 +146,6 @@ struct SheetPlus: ViewModifier } .offset(y: UIScreen.main.bounds.height - translation) .onDisappear { - translation = 0 -// detents = [] - onDismiss() } .animation( @@ -163,7 +167,11 @@ struct SheetPlus: ViewModifier isPresented = false return } - + + if initialSelectedDetent == nil { + initialSelectedDetent = value.selectedDetent + } + sheetConfig = value translation = value.translation From 7b1cd15b0dc92ca16c52b86fdac0056c7660b883 Mon Sep 17 00:00:00 2001 From: Wouter Date: Tue, 2 Jan 2024 08:42:44 +0100 Subject: [PATCH 04/10] fix: various other dismiss fixes --- Sources/BottomSheet/BottomSheet.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Sources/BottomSheet/BottomSheet.swift b/Sources/BottomSheet/BottomSheet.swift index d42136c..0f75a33 100644 --- a/Sources/BottomSheet/BottomSheet.swift +++ b/Sources/BottomSheet/BottomSheet.swift @@ -23,6 +23,7 @@ struct SheetPlus: ViewModifier @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) @State private var initialSelectedDetent: PresentationDetent? = nil + @State private var isDismissed = false let mainContent: MContent let headerContent: HContent @@ -58,12 +59,19 @@ struct SheetPlus: ViewModifier .onChange(of: isPresented) { newValue in guard let initialSelectedDetent = initialSelectedDetent else { return } - if sheetConfig?.selectedDetent == PresentationDetent.height(0.0) && newValue == true { + if newValue == true { + isDismissed = false sheetConfig?.selectedDetent = initialSelectedDetent } + + if newValue == false { + print("Running") + translation = 0 + sheetConfig?.selectedDetent = PresentationDetent.height(.zero) + } } - if isPresented { + if !isDismissed { GeometryReader { geometry in VStack(spacing: 0) { // If / else statement here breaks the animation from the bottom level @@ -132,15 +140,16 @@ struct SheetPlus: ViewModifier // Small little hack to make the iOS scroll behaviour work smoothly if limits.max == 0 { return } - let minValue = isInteractiveDismissDisabled ? limits.min : 0 + let minValue = isInteractiveDismissDisabled && isPresented ? limits.min : 0 translation = min(limits.max, max(newValue, minValue)) currentGlobalTranslation = translation } .onAnimationChange(of: translation) { value in - onDrag(value) + onDrag(value > 0 ? value : 0) if value <= 0 && sheetConfig?.selectedDetent == .height(.zero) { + isDismissed = true isPresented = false } } From 98d9e176c91e8ef340199ee969e0e2a220f4a98e Mon Sep 17 00:00:00 2001 From: Wouter Date: Sat, 10 Feb 2024 23:00:49 +0100 Subject: [PATCH 05/10] feat: rewrite towards height animations and open issue resolving --- .../BottomSheetExample/SheetV2Example.swift | 18 ++ Sources/BottomSheet/BottomSheet.swift | 1 - Sources/BottomSheet/BottomSheetV2.swift | 181 ++++++++++++++++++ .../Preference Keys/ConfigKey.swift | 8 +- .../PresentationBackgroundKey.swift | 29 +++ .../PresentationCornerRadiusKey.swift | 16 ++ .../UIKit Views/UIScrollViewWrapper.swift | 7 +- .../View+PresentationBackground.swift | 20 ++ .../View+PresentationCornerRadius.swift | 19 ++ .../View Modifiers/View+SheetPlus.swift | 22 +++ Sources/BottomSheet/Views/DragIndicator.swift | 2 +- 11 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 Example/BottomSheetExample/SheetV2Example.swift create mode 100644 Sources/BottomSheet/BottomSheetV2.swift create mode 100644 Sources/BottomSheet/Preference Keys/PresentationBackgroundKey.swift create mode 100644 Sources/BottomSheet/Preference Keys/PresentationCornerRadiusKey.swift create mode 100644 Sources/BottomSheet/View Modifiers/View+PresentationBackground.swift create mode 100644 Sources/BottomSheet/View Modifiers/View+PresentationCornerRadius.swift diff --git a/Example/BottomSheetExample/SheetV2Example.swift b/Example/BottomSheetExample/SheetV2Example.swift new file mode 100644 index 0000000..3a2ac0b --- /dev/null +++ b/Example/BottomSheetExample/SheetV2Example.swift @@ -0,0 +1,18 @@ +// +// SheetV2Example.swift +// BottomSheetExample +// +// Created by Wouter van de Kamp on 06/01/2024. +// + +import SwiftUI + +struct SheetV2Example: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + SheetV2Example() +} diff --git a/Sources/BottomSheet/BottomSheet.swift b/Sources/BottomSheet/BottomSheet.swift index 0f75a33..5b9d5a8 100644 --- a/Sources/BottomSheet/BottomSheet.swift +++ b/Sources/BottomSheet/BottomSheet.swift @@ -65,7 +65,6 @@ struct SheetPlus: ViewModifier } if newValue == false { - print("Running") translation = 0 sheetConfig?.selectedDetent = PresentationDetent.height(.zero) } diff --git a/Sources/BottomSheet/BottomSheetV2.swift b/Sources/BottomSheet/BottomSheetV2.swift new file mode 100644 index 0000000..a67cef1 --- /dev/null +++ b/Sources/BottomSheet/BottomSheetV2.swift @@ -0,0 +1,181 @@ +// +// SwiftUIView.swift +// +// +// Created by Wouter van de Kamp on 06/01/2024. +// + +import SwiftUI + +struct SheetPlusV2: ViewModifier { + @Binding private var isPresented: Bool + + @State private var translation: CGFloat = 0 + @State private var newValue = 0.0 + @State private var startTime: DragGesture.Value? + + @State private var sheetConfig: SheetPlusConfig? + @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) + + @State private var detents: Set = [] + + @State private var showDragIndicator: VisibilityPlus? + @State private var cornerRadius: CGFloat? + @State private var background: EquatableBackground? + @State private var isInteractiveDismissDisabled = true + + let animationCurve: SheetAnimation + let onDrag: (CGFloat) -> Void + let mainContent: MContent + let headerContent: HContent + + init( + isPresented: Binding, + animationCurve: SheetAnimation, + onDrag: @escaping (CGFloat) -> Void = { _ in }, + @ViewBuilder hcontent: () -> HContent, + @ViewBuilder mcontent: () -> MContent + ) { + self._isPresented = isPresented + self.animationCurve = animationCurve + + self.onDrag = onDrag + + self.headerContent = hcontent() + self.mainContent = mcontent() + } + + func body(content: Content) -> some View { + ZStack { + content + .zIndex(1) + + + if isPresented { + // VStack to stick the sheet to the bottom + VStack(spacing: 0) { + Spacer() + + // VStack to keep the drag indicator and header at the top of the card + VStack(spacing: 0) { + VStack(spacing: 0) { + if showDragIndicator == .visible { + DragIndicator( + translation: $translation, + detents: detents + ) + } + + headerContent + .contentShape(Rectangle()) + } + .gesture( + DragGesture(coordinateSpace: .global) + .onChanged { value in + translation -= value.location.y - value.startLocation.y - newValue + newValue = value.location.y - value.startLocation.y + + if startTime == nil { + startTime = value + } + } + .onEnded { value in + // Reset the distance on release so we start with a + // clean translation next time + newValue = 0 + + // Calculate velocity based on pt/s so it matches the UIPanGesture + let distance: CGFloat = value.translation.height + let time: CGFloat = value.time.timeIntervalSince(startTime!.time) + + let yVelocity: CGFloat = -1 * ((distance / time) / 1000) + startTime = nil + + if let result = snapBottomSheet( + translation, + detents, + yVelocity, + isInteractiveDismissDisabled + ) { + translation = result.size + + if result.size != .zero { + sheetConfig?.selectedDetent = result + } + } + } + ) + + Spacer() + + UIScrollViewWrapper( + translation: $translation, + preferenceKey: $sheetConfig, + isInteractiveDismissDisabled: $isInteractiveDismissDisabled, + limits: limits, + detents: detents + ) { + mainContent + } + + Spacer() + } + .frame( + width: UIScreen.main.bounds.width, + height: translation + ) + .clipped() + .background( + ZStack( + alignment: background?.alignment ?? .center + ) { + background?.view + } + .cornerRadius(cornerRadius ?? 0) + ) + .onChange(of: translation) { value in + translation = min(limits.max, max(value, isInteractiveDismissDisabled ? limits.min : .zero)) + } + .onAnimationChange(of: translation) { value in + onDrag(value > .zero ? value : .zero) + + if (translation == 0 && value == .zero) { + isPresented = false + } + } + .onDisappear { + print("Dismissed") + } + .animation( + .interpolatingSpring( + mass: animationCurve.mass, + stiffness: animationCurve.stiffness, + damping: animationCurve.damping + ) + ) + } + .edgesIgnoringSafeArea(.bottom) + .zIndex(2) + } + } + .onPreferenceChange(SheetPlusKey.self) { value in + sheetConfig = value + translation = value.translation + + detents = value.detents + limits = detentLimits(detents: detents) + } + .onPreferenceChange(SheetPlusIndicatorKey.self) { value in + showDragIndicator = value + } + .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in + isInteractiveDismissDisabled = value + } + .onPreferenceChange(SheetPlusPresentationCornerRadiusKey.self) { value in + cornerRadius = value + } + .onPreferenceChange(SheetPlusPresentationBackgroundKey.self) { value in + background = value + } + } +} diff --git a/Sources/BottomSheet/Preference Keys/ConfigKey.swift b/Sources/BottomSheet/Preference Keys/ConfigKey.swift index a4ec7d4..661f10e 100644 --- a/Sources/BottomSheet/Preference Keys/ConfigKey.swift +++ b/Sources/BottomSheet/Preference Keys/ConfigKey.swift @@ -22,9 +22,11 @@ struct SheetPlusKey: PreferenceKey { static var defaultValue: SheetPlusConfig = SheetPlusConfig(detents: [], selectedDetent: .constant(.height(.zero)), translation: 0) static func reduce(value: inout SheetPlusConfig, nextValue: () -> SheetPlusConfig) { + value = nextValue() + } +} + /// This prevents the translation changes to be called whenever the keyboard is triggered. /// If the keyboard gets triggered it will also reset the whole configkey and losing the binding. /// https://stackoverflow.com/questions/67644164/preferencekey-issue-swiftui-sometimes-seems-to-generate-additional-views-that - value = nextValue() != defaultValue ? nextValue() : value - } -} +// value = nextValue() != defaultValue ? nextValue() : value diff --git a/Sources/BottomSheet/Preference Keys/PresentationBackgroundKey.swift b/Sources/BottomSheet/Preference Keys/PresentationBackgroundKey.swift new file mode 100644 index 0000000..16e611e --- /dev/null +++ b/Sources/BottomSheet/Preference Keys/PresentationBackgroundKey.swift @@ -0,0 +1,29 @@ +// +// PresentationBackgroundKey.swift +// +// +// Created by Wouter van de Kamp on 03/02/2024. +// + +import SwiftUI + +struct EquatableBackground: Equatable { + let id = UUID().uuidString + let alignment: Alignment + let view: AnyView + + static func == (lhs: EquatableBackground, rhs: EquatableBackground) -> Bool { + return lhs.id == rhs.id + } +} + +struct SheetPlusPresentationBackgroundKey: PreferenceKey { + static var defaultValue: EquatableBackground = EquatableBackground(alignment: .center, view: AnyView(EmptyView())) + + static func reduce( + value: inout EquatableBackground, + nextValue: () -> EquatableBackground + ) { + value = nextValue() + } +} diff --git a/Sources/BottomSheet/Preference Keys/PresentationCornerRadiusKey.swift b/Sources/BottomSheet/Preference Keys/PresentationCornerRadiusKey.swift new file mode 100644 index 0000000..f0b16a3 --- /dev/null +++ b/Sources/BottomSheet/Preference Keys/PresentationCornerRadiusKey.swift @@ -0,0 +1,16 @@ +// +// File.swift +// +// +// Created by Wouter van de Kamp on 30/01/2024. +// + +import SwiftUI + +struct SheetPlusPresentationCornerRadiusKey: PreferenceKey { + static var defaultValue: CGFloat? = nil + + static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { + value = nextValue() + } +} diff --git a/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift b/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift index ced60a6..85c6c1c 100644 --- a/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift +++ b/Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift @@ -49,7 +49,7 @@ internal struct UIScrollViewWrapper: UIViewRepresentable { func updateUIView(_ scrollView: UIScrollView, context: Context) { context.coordinator.limits = limits context.coordinator.detents = detents - + context.coordinator.hostingController.rootView = self.content() } @@ -143,7 +143,10 @@ internal struct UIScrollViewWrapper: UIViewRepresentable { representable.isInteractiveDismissDisabled ) { representable.translation = result.size - representable.preferenceKey?.selectedDetent = result + + if result.size != .zero { + representable.preferenceKey?.selectedDetent = result + } } scrollOffset = 0 diff --git a/Sources/BottomSheet/View Modifiers/View+PresentationBackground.swift b/Sources/BottomSheet/View Modifiers/View+PresentationBackground.swift new file mode 100644 index 0000000..1ba3ada --- /dev/null +++ b/Sources/BottomSheet/View Modifiers/View+PresentationBackground.swift @@ -0,0 +1,20 @@ +// +// File.swift +// +// +// Created by Wouter van de Kamp on 27/01/2024. +// + +import SwiftUI + +extension View { + public func presentationBackgroundPlus( + alignment: Alignment = .center, + @ViewBuilder content: () -> V + ) -> some View { + return self.preference( + key: SheetPlusPresentationBackgroundKey.self, + value: EquatableBackground(alignment: alignment, view: AnyView(content())) + ) + } +} diff --git a/Sources/BottomSheet/View Modifiers/View+PresentationCornerRadius.swift b/Sources/BottomSheet/View Modifiers/View+PresentationCornerRadius.swift new file mode 100644 index 0000000..61c763b --- /dev/null +++ b/Sources/BottomSheet/View Modifiers/View+PresentationCornerRadius.swift @@ -0,0 +1,19 @@ +// +// View+PresentationCornerRadius.swift +// +// +// Created by Wouter van de Kamp on 30/01/2024. +// + +import SwiftUI + +extension View { + public func presentationCornerRadiusPlus( + cornerRadius: CGFloat? + ) -> some View { + return self.preference( + key: SheetPlusPresentationCornerRadiusKey.self, + value: cornerRadius + ) + } +} diff --git a/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift b/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift index dea298c..81fb4c0 100644 --- a/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift +++ b/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift @@ -33,4 +33,26 @@ extension View { ) ) } + + public func sheetPlusV2( + isPresented: Binding, + animationCurve: SheetAnimation = SheetAnimation( + mass: SheetAnimationDefaults.mass, + stiffness: SheetAnimationDefaults.stiffness, + damping: SheetAnimationDefaults.damping + ), + onDrag: @escaping (CGFloat) -> Void = { _ in }, + header: () -> HContent = { EmptyView() }, + main: () -> MContent + ) -> some View { + modifier( + SheetPlusV2( + isPresented: isPresented, + animationCurve: animationCurve, + onDrag: onDrag, + hcontent: header, + mcontent: main + ) + ) + } } diff --git a/Sources/BottomSheet/Views/DragIndicator.swift b/Sources/BottomSheet/Views/DragIndicator.swift index edeb7f9..f72f629 100644 --- a/Sources/BottomSheet/Views/DragIndicator.swift +++ b/Sources/BottomSheet/Views/DragIndicator.swift @@ -24,7 +24,7 @@ struct DragIndicator: View { if let nextDetent = nextDetent { translation = nextDetent.size } else { - translation = sortedDetents.first!.size + translation = sortedDetents.first?.size ?? 0 } } } From 42018ea5eda5346ee84d596dd688da165f35aaa2 Mon Sep 17 00:00:00 2001 From: Wouter Date: Sun, 11 Feb 2024 10:02:40 +0100 Subject: [PATCH 06/10] fix: various fixes for sheet dismissal --- Sources/BottomSheet/BottomSheetV2.swift | 73 ++++++++++++------- .../View Modifiers/View+SheetPlus.swift | 2 + 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/Sources/BottomSheet/BottomSheetV2.swift b/Sources/BottomSheet/BottomSheetV2.swift index a67cef1..0f71797 100644 --- a/Sources/BottomSheet/BottomSheetV2.swift +++ b/Sources/BottomSheet/BottomSheetV2.swift @@ -24,14 +24,18 @@ struct SheetPlusV2: ViewModifier { @State private var background: EquatableBackground? @State private var isInteractiveDismissDisabled = true + @State private var viewWillPresent = false + let animationCurve: SheetAnimation let onDrag: (CGFloat) -> Void let mainContent: MContent let headerContent: HContent + let onDismiss: () -> Void init( isPresented: Binding, animationCurve: SheetAnimation, + onDismiss: @escaping () -> Void, onDrag: @escaping (CGFloat) -> Void = { _ in }, @ViewBuilder hcontent: () -> HContent, @ViewBuilder mcontent: () -> MContent @@ -39,6 +43,7 @@ struct SheetPlusV2: ViewModifier { self._isPresented = isPresented self.animationCurve = animationCurve + self.onDismiss = onDismiss self.onDrag = onDrag self.headerContent = hcontent() @@ -49,9 +54,15 @@ struct SheetPlusV2: ViewModifier { ZStack { content .zIndex(1) + .onChange(of: isPresented) { value in + if (value == true) { + viewWillPresent = value + } else { + translation = .zero + } + } - - if isPresented { + if viewWillPresent { // VStack to stick the sheet to the bottom VStack(spacing: 0) { Spacer() @@ -80,29 +91,7 @@ struct SheetPlusV2: ViewModifier { } } .onEnded { value in - // Reset the distance on release so we start with a - // clean translation next time - newValue = 0 - - // Calculate velocity based on pt/s so it matches the UIPanGesture - let distance: CGFloat = value.translation.height - let time: CGFloat = value.time.timeIntervalSince(startTime!.time) - - let yVelocity: CGFloat = -1 * ((distance / time) / 1000) - startTime = nil - - if let result = snapBottomSheet( - translation, - detents, - yVelocity, - isInteractiveDismissDisabled - ) { - translation = result.size - - if result.size != .zero { - sheetConfig?.selectedDetent = result - } - } + onDragEnded(with: value) } ) @@ -134,17 +123,21 @@ struct SheetPlusV2: ViewModifier { .cornerRadius(cornerRadius ?? 0) ) .onChange(of: translation) { value in - translation = min(limits.max, max(value, isInteractiveDismissDisabled ? limits.min : .zero)) + let minLimit = isInteractiveDismissDisabled && isPresented ? limits.min : .zero + translation = min(limits.max, max(value, minLimit)) } .onAnimationChange(of: translation) { value in onDrag(value > .zero ? value : .zero) + print(translation) + if (translation == 0 && value == .zero) { isPresented = false + viewWillPresent = false } } .onDisappear { - print("Dismissed") + onDismiss() } .animation( .interpolatingSpring( @@ -178,4 +171,30 @@ struct SheetPlusV2: ViewModifier { background = value } } + + func onDragEnded(with value: DragGesture.Value) { + // Reset the distance on release so we start with a + // clean translation next time + newValue = 0 + + // Calculate velocity based on pt/s so it matches the UIPanGesture + let distance: CGFloat = value.translation.height + let time: CGFloat = value.time.timeIntervalSince(startTime!.time) + + let yVelocity: CGFloat = -1 * ((distance / time) / 1000) + startTime = nil + + if let result = snapBottomSheet( + translation, + detents, + yVelocity, + isInteractiveDismissDisabled + ) { + translation = result.size + + if result.size != .zero { + sheetConfig?.selectedDetent = result + } + } + } } diff --git a/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift b/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift index 81fb4c0..08e1a10 100644 --- a/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift +++ b/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift @@ -41,6 +41,7 @@ extension View { stiffness: SheetAnimationDefaults.stiffness, damping: SheetAnimationDefaults.damping ), + onDismiss: @escaping () -> Void = {}, onDrag: @escaping (CGFloat) -> Void = { _ in }, header: () -> HContent = { EmptyView() }, main: () -> MContent @@ -49,6 +50,7 @@ extension View { SheetPlusV2( isPresented: isPresented, animationCurve: animationCurve, + onDismiss: onDismiss, onDrag: onDrag, hcontent: header, mcontent: main From 176c59b07b64278469d50652a8838468276956e6 Mon Sep 17 00:00:00 2001 From: Wouter Date: Wed, 27 Mar 2024 23:51:02 +0100 Subject: [PATCH 07/10] feat: brought back the background interaction key --- .../project.pbxproj | 20 ++- .../xcschemes/BottomSheetExample.xcscheme | 2 +- .../Apple Applications/StocksExample.swift | 2 +- .../BottomSheetExampleApp.swift | 2 +- .../BottomSheetExample/ExampleOverview.swift | 2 +- .../BottomSheetExample/SheetV2Example.swift | 69 +++++++++- Sources/BottomSheet/BottomSheet.swift | 10 +- Sources/BottomSheet/BottomSheetV2.swift | 120 +++++++++++------- Sources/BottomSheet/Detents/Detents.swift | 10 +- .../BackgroundInteractionKey.swift | 13 +- .../Preference Keys/ConfigKey.swift | 6 - .../View Modifiers/View+DragIndicator.swift | 1 + 12 files changed, 180 insertions(+), 77 deletions(-) diff --git a/Example/BottomSheetExample.xcodeproj/project.pbxproj b/Example/BottomSheetExample.xcodeproj/project.pbxproj index 4f4d4b6..8d7bcb5 100644 --- a/Example/BottomSheetExample.xcodeproj/project.pbxproj +++ b/Example/BottomSheetExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6C0359AC2B496FB90012B90A /* SheetV2Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0359AB2B496FB90012B90A /* SheetV2Example.swift */; }; 6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */; }; 6C0BD47127DA862D000AF3CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47027DA862D000AF3CD /* Assets.xcassets */; }; 6C0BD47427DA862D000AF3CD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */; }; @@ -18,6 +19,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 6C0359AB2B496FB90012B90A /* SheetV2Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetV2Example.swift; sourceTree = ""; }; 6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BottomSheetExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetExampleApp.swift; sourceTree = ""; }; 6C0BD47027DA862D000AF3CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -63,6 +65,7 @@ isa = PBXGroup; children = ( 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */, + 6C0359AB2B496FB90012B90A /* SheetV2Example.swift */, 6C763146283D774500463709 /* ExampleOverview.swift */, 6CF78516293D3703000E6581 /* Apple Applications */, 6C8CBF1B2AED12C60007E10E /* Examples */, @@ -144,7 +147,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1320; - LastUpgradeCheck = 1320; + LastUpgradeCheck = 1510; TargetAttributes = { 6C0BD46827DA862D000AF3CD = { CreatedOnToolsVersion = 13.2.1; @@ -201,7 +204,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n swiftlint --fix && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -210,6 +213,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6C0359AC2B496FB90012B90A /* SheetV2Example.swift in Sources */, 6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */, 6C8CBF1D2AED12E00007E10E /* StaticScrollViewExample.swift in Sources */, 6CDF5A0D27ED33C7004609F4 /* CornerRadius.swift in Sources */, @@ -258,6 +262,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -272,7 +277,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -319,6 +324,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -327,7 +333,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -347,6 +353,7 @@ DEVELOPMENT_ASSET_PATHS = "BottomSheetExample/Preview\\ Content"; DEVELOPMENT_TEAM = KZAMEFAGHT; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -359,7 +366,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.chargetrip.BottomSheetExample; + PRODUCT_BUNDLE_IDENTIFIER = com.test.BottomSheetExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -377,6 +384,7 @@ DEVELOPMENT_ASSET_PATHS = "BottomSheetExample/Preview\\ Content"; DEVELOPMENT_TEAM = KZAMEFAGHT; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -389,7 +397,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.chargetrip.BottomSheetExample; + PRODUCT_BUNDLE_IDENTIFIER = com.test.BottomSheetExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/Example/BottomSheetExample.xcodeproj/xcshareddata/xcschemes/BottomSheetExample.xcscheme b/Example/BottomSheetExample.xcodeproj/xcshareddata/xcschemes/BottomSheetExample.xcscheme index 2071993..be8edd6 100644 --- a/Example/BottomSheetExample.xcodeproj/xcshareddata/xcschemes/BottomSheetExample.xcscheme +++ b/Example/BottomSheetExample.xcodeproj/xcshareddata/xcschemes/BottomSheetExample.xcscheme @@ -1,6 +1,6 @@ : ViewModifier func body(content: Content) -> some View { ZStack() { content - .allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true) +// .allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true) .onChange(of: isPresented) { newValue in guard let initialSelectedDetent = initialSelectedDetent else { return } @@ -142,7 +142,7 @@ struct SheetPlus: ViewModifier let minValue = isInteractiveDismissDisabled && isPresented ? limits.min : 0 translation = min(limits.max, max(newValue, minValue)) - currentGlobalTranslation = translation +// currentGlobalTranslation = translation } .onAnimationChange(of: translation) { value in onDrag(value > 0 ? value : 0) @@ -189,9 +189,9 @@ struct SheetPlus: ViewModifier .onPreferenceChange(SheetPlusIndicatorKey.self) { value in showDragIndicator = value } - .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in - allowBackgroundInteraction = value - } +// .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in +// allowBackgroundInteraction = value +// } .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in isInteractiveDismissDisabled = value } diff --git a/Sources/BottomSheet/BottomSheetV2.swift b/Sources/BottomSheet/BottomSheetV2.swift index 0f71797..206aa9f 100644 --- a/Sources/BottomSheet/BottomSheetV2.swift +++ b/Sources/BottomSheet/BottomSheetV2.swift @@ -24,6 +24,9 @@ struct SheetPlusV2: ViewModifier { @State private var background: EquatableBackground? @State private var isInteractiveDismissDisabled = true + @State private var backgroundInteractionDetentLimit: PresentationDetent? + @State private var isBackgroundInteractionEnabled = true + @State private var viewWillPresent = false let animationCurve: SheetAnimation @@ -50,9 +53,14 @@ struct SheetPlusV2: ViewModifier { self.mainContent = mcontent() } + func isInteractionEnabled() -> Bool { + return true + } + func body(content: Content) -> some View { ZStack { content + .allowsHitTesting(!isPresented || isBackgroundInteractionEnabled) .zIndex(1) .onChange(of: isPresented) { value in if (value == true) { @@ -69,68 +77,81 @@ struct SheetPlusV2: ViewModifier { // VStack to keep the drag indicator and header at the top of the card VStack(spacing: 0) { + + // Holds the background so it animates VStack(spacing: 0) { - if showDragIndicator == .visible { - DragIndicator( - translation: $translation, - detents: detents - ) - } - headerContent - .contentShape(Rectangle()) - } - .gesture( - DragGesture(coordinateSpace: .global) - .onChanged { value in - translation -= value.location.y - value.startLocation.y - newValue - newValue = value.location.y - value.startLocation.y - - if startTime == nil { - startTime = value - } - } - .onEnded { value in - onDragEnded(with: value) + // Holds the header customization + VStack(spacing: 0) { + if showDragIndicator == .visible { + DragIndicator( + translation: $translation, + detents: detents + ) } - ) - Spacer() + headerContent + .contentShape(Rectangle()) + } + .gesture( + DragGesture(coordinateSpace: .global) + .onChanged { value in + translation -= value.location.y - value.startLocation.y - newValue + newValue = value.location.y - value.startLocation.y + + if startTime == nil { + startTime = value + } + } + .onEnded { value in + onDragEnded(with: value) + } + ) + + Spacer() + +// UIScrollViewWrapper( +// translation: $translation, +// preferenceKey: $sheetConfig, +// isInteractiveDismissDisabled: $isInteractiveDismissDisabled, +// limits: limits, +// detents: detents +// ) { + HStack { + Spacer() + mainContent + Spacer() + } + - UIScrollViewWrapper( - translation: $translation, - preferenceKey: $sheetConfig, - isInteractiveDismissDisabled: $isInteractiveDismissDisabled, - limits: limits, - detents: detents - ) { - mainContent +// } + + Spacer() } - Spacer() } + .background( + ZStack() { + background?.view + .cornerRadius(cornerRadius ?? 0) + } + ) .frame( width: UIScreen.main.bounds.width, height: translation ) .clipped() - .background( - ZStack( - alignment: background?.alignment ?? .center - ) { - background?.view - } - .cornerRadius(cornerRadius ?? 0) - ) .onChange(of: translation) { value in let minLimit = isInteractiveDismissDisabled && isPresented ? limits.min : .zero translation = min(limits.max, max(value, minLimit)) + + if let interactionDetent = backgroundInteractionDetentLimit { + isBackgroundInteractionEnabled = translation < interactionDetent.size + } } .onAnimationChange(of: translation) { value in onDrag(value > .zero ? value : .zero) - print(translation) - if (translation == 0 && value == .zero) { isPresented = false viewWillPresent = false @@ -161,6 +182,19 @@ struct SheetPlusV2: ViewModifier { .onPreferenceChange(SheetPlusIndicatorKey.self) { value in showDragIndicator = value } + .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in + // Custom assignment for now until I figure out a way to use SPBIK with a struct. + backgroundInteractionDetentLimit = PresentationBackgroundInteractionPlus.detent + + if value == .automatic || value == .enabled { + isBackgroundInteractionEnabled = true + return + } + + isBackgroundInteractionEnabled = false + + print(isBackgroundInteractionEnabled) + } .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in isInteractiveDismissDisabled = value } @@ -179,7 +213,7 @@ struct SheetPlusV2: ViewModifier { // Calculate velocity based on pt/s so it matches the UIPanGesture let distance: CGFloat = value.translation.height - let time: CGFloat = value.time.timeIntervalSince(startTime!.time) + let time: CGFloat = startTime != nil ? value.time.timeIntervalSince(startTime!.time) : 0 let yVelocity: CGFloat = -1 * ((distance / time) / 1000) startTime = nil diff --git a/Sources/BottomSheet/Detents/Detents.swift b/Sources/BottomSheet/Detents/Detents.swift index 2714dbd..e1b7f6d 100644 --- a/Sources/BottomSheet/Detents/Detents.swift +++ b/Sources/BottomSheet/Detents/Detents.swift @@ -19,27 +19,27 @@ import SwiftUI public enum PresentationDetent: Hashable { /** - .fraction(0.2) + The system detent for a sheet that’s approximately a quarter height of the screen, and is inactive in compact height. */ case small /** - .fraction(0.5) + The system detent for a sheet that’s approximately half the height of the screen, and is inactive in compact height. */ case medium /** - .fraction(0.9) + The system detent for a sheet at full height. */ case large /** - CGFloat 0 to 1 + A custom detent with the specified fractional height. */ case fraction(CGFloat) /** - A constant height. + A custom detent with the specified height. */ case height(CGFloat) diff --git a/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift b/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift index 66d1e07..00f3399 100644 --- a/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift +++ b/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift @@ -7,18 +7,17 @@ import SwiftUI -// Currently using a global var. -// Might want to rework this by setting up the view modifiers a bit different. -// Probably something that we can hold translation in 1 var. Now both need to be in sync. -var currentGlobalTranslation: CGFloat = 0 - -public enum PresentationBackgroundInteractionPlus { +public enum PresentationBackgroundInteractionPlus: Equatable { case automatic case disabled case enabled + internal static var detent: PresentationDetent? + internal static var enabledUpThrough: PresentationBackgroundInteractionPlus = .enabledUpThrough + public static func enabled(upThrough detent: PresentationDetent) -> PresentationBackgroundInteractionPlus { - currentGlobalTranslation > detent.size ? .disabled : .enabled + self.detent = detent + return enabledUpThrough } } diff --git a/Sources/BottomSheet/Preference Keys/ConfigKey.swift b/Sources/BottomSheet/Preference Keys/ConfigKey.swift index 661f10e..feb3718 100644 --- a/Sources/BottomSheet/Preference Keys/ConfigKey.swift +++ b/Sources/BottomSheet/Preference Keys/ConfigKey.swift @@ -12,7 +12,6 @@ struct SheetPlusConfig: Equatable { @Binding var selectedDetent: PresentationDetent let translation: CGFloat - static func == (lhs: SheetPlusConfig, rhs: SheetPlusConfig) -> Bool { return lhs.selectedDetent == rhs.selectedDetent && lhs.translation == rhs.translation && lhs.detents == rhs.detents } @@ -25,8 +24,3 @@ struct SheetPlusKey: PreferenceKey { value = nextValue() } } - - /// This prevents the translation changes to be called whenever the keyboard is triggered. - /// If the keyboard gets triggered it will also reset the whole configkey and losing the binding. - /// https://stackoverflow.com/questions/67644164/preferencekey-issue-swiftui-sometimes-seems-to-generate-additional-views-that -// value = nextValue() != defaultValue ? nextValue() : value diff --git a/Sources/BottomSheet/View Modifiers/View+DragIndicator.swift b/Sources/BottomSheet/View Modifiers/View+DragIndicator.swift index 2cc62d0..33e3842 100644 --- a/Sources/BottomSheet/View Modifiers/View+DragIndicator.swift +++ b/Sources/BottomSheet/View Modifiers/View+DragIndicator.swift @@ -7,6 +7,7 @@ import SwiftUI +@frozen public enum VisibilityPlus { case hidden case visible From 15792b8f9fda74a8b4018b15c9a46c5c57d594fc Mon Sep 17 00:00:00 2001 From: Wouter Date: Wed, 27 Mar 2024 23:55:25 +0100 Subject: [PATCH 08/10] fix: reenable the scrollview --- Sources/BottomSheet/BottomSheetV2.swift | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Sources/BottomSheet/BottomSheetV2.swift b/Sources/BottomSheet/BottomSheetV2.swift index 206aa9f..8bedecd 100644 --- a/Sources/BottomSheet/BottomSheetV2.swift +++ b/Sources/BottomSheet/BottomSheetV2.swift @@ -110,22 +110,20 @@ struct SheetPlusV2: ViewModifier { Spacer() -// UIScrollViewWrapper( -// translation: $translation, -// preferenceKey: $sheetConfig, -// isInteractiveDismissDisabled: $isInteractiveDismissDisabled, -// limits: limits, -// detents: detents -// ) { - HStack { - Spacer() - mainContent - Spacer() + UIScrollViewWrapper( + translation: $translation, + preferenceKey: $sheetConfig, + isInteractiveDismissDisabled: $isInteractiveDismissDisabled, + limits: limits, + detents: detents + ) { + HStack { + Spacer() + mainContent + Spacer() + } } - -// } - Spacer() } From 1905ff6d0829962f7985cbad0cb553467138ef18 Mon Sep 17 00:00:00 2001 From: Wouter Date: Tue, 4 Jun 2024 23:51:57 +0200 Subject: [PATCH 09/10] fix: background interaction modifier through struct --- .../Apple Applications/StocksExample.swift | 5 - .../BottomSheetExampleApp.swift | 2 +- .../BottomSheetExample/ExampleOverview.swift | 15 +- .../BottomSheetExample/SheetV2Example.swift | 4 +- Sources/BottomSheet/BottomSheet.swift | 242 ++++++++++-------- Sources/BottomSheet/BottomSheetLegacy.swift | 199 ++++++++++++++ Sources/BottomSheet/BottomSheetV2.swift | 232 ----------------- Sources/BottomSheet/Detents/Detents.swift | 2 +- .../BackgroundInteractionKey.swift | 30 ++- .../View+BackgroundInteraction.swift | 14 +- .../View Modifiers/View+SheetPlus.swift | 28 +- 11 files changed, 374 insertions(+), 399 deletions(-) create mode 100644 Sources/BottomSheet/BottomSheetLegacy.swift delete mode 100644 Sources/BottomSheet/BottomSheetV2.swift diff --git a/Example/BottomSheetExample/Apple Applications/StocksExample.swift b/Example/BottomSheetExample/Apple Applications/StocksExample.swift index 0012447..2ce14ad 100644 --- a/Example/BottomSheetExample/Apple Applications/StocksExample.swift +++ b/Example/BottomSheetExample/Apple Applications/StocksExample.swift @@ -13,10 +13,6 @@ struct StocksExample: View { var body: some View { VStack { - Button(settings.isPresented ? "Close" : "Show") { - settings.isPresented.toggle() - } - Button("Change") { settings.selectedDetent = .large } @@ -44,7 +40,6 @@ struct StocksHeader: View { Text("From Yahoo Finance") .foregroundColor(Color(UIColor.secondaryLabel)) } - .padding(.top, 10) .padding(.bottom, 16) Spacer() diff --git a/Example/BottomSheetExample/BottomSheetExampleApp.swift b/Example/BottomSheetExample/BottomSheetExampleApp.swift index 30f9d47..1d0b0bc 100644 --- a/Example/BottomSheetExample/BottomSheetExampleApp.swift +++ b/Example/BottomSheetExample/BottomSheetExampleApp.swift @@ -11,7 +11,7 @@ import SwiftUI struct BottomSheetExampleApp: App { var body: some Scene { WindowGroup { - NavigationViewV2() + ExampleOverview() } } } diff --git a/Example/BottomSheetExample/ExampleOverview.swift b/Example/BottomSheetExample/ExampleOverview.swift index c801db0..c6bdd32 100644 --- a/Example/BottomSheetExample/ExampleOverview.swift +++ b/Example/BottomSheetExample/ExampleOverview.swift @@ -50,14 +50,23 @@ struct ExampleOverview: View { [.height(244), .medium, .large], selection: $settings.selectedDetent ) + .presentationBackgroundInteractionPlus(.automatic) + .presentationBackgroundPlus { + Color(UIColor.secondarySystemBackground) + } + .presentationCornerRadiusPlus(cornerRadius: 12) case .staticScrollView: StaticScrollViewContent() .presentationDetentsPlus( [.height(380), .height(480), .large], selection: $settings.selectedDetent ) + .presentationBackgroundInteractionPlus(.enabled(upThrough: .medium)) + .presentationBackgroundPlus { + Color(UIColor.red) + } + .presentationCornerRadiusPlus(cornerRadius: 12) .presentationDragIndicatorPlus(.visible) -// .presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380))) .interactiveDismissDisabledPlus(false) default: EmptyView() @@ -86,10 +95,6 @@ struct ExampleOverview: View { .environmentObject(settings) .sheetPlus( isPresented: $settings.isPresented, - background: ( - Color(UIColor.secondarySystemBackground) - .cornerRadius(12, corners: [.topLeft, .topRight]) - ), onDrag: { translation in settings.translation = translation }, diff --git a/Example/BottomSheetExample/SheetV2Example.swift b/Example/BottomSheetExample/SheetV2Example.swift index f814f1a..b6e9116 100644 --- a/Example/BottomSheetExample/SheetV2Example.swift +++ b/Example/BottomSheetExample/SheetV2Example.swift @@ -33,7 +33,7 @@ struct NavigationViewV2: View { } } .environmentObject(settings) - .sheetPlusV2( + .sheetPlus( isPresented: $settings.isPresented, header: { Text("Test") @@ -55,7 +55,7 @@ struct NavigationViewV2: View { .presentationBackgroundPlus { Color.yellow } -// .presentationBackgroundInteractionPlus(.enabled(upThrough: .fraction(0.2))) + .presentationBackgroundInteractionPlus(.automatic) .presentationDragIndicatorPlus(.visible) .presentationCornerRadiusPlus(cornerRadius: 12) .interactiveDismissDisabledPlus(false) diff --git a/Sources/BottomSheet/BottomSheet.swift b/Sources/BottomSheet/BottomSheet.swift index b9f9e24..c415ba2 100644 --- a/Sources/BottomSheet/BottomSheet.swift +++ b/Sources/BottomSheet/BottomSheet.swift @@ -1,158 +1,157 @@ // -// BottomSheet.swift +// SwiftUIView.swift +// // -// -// Created by Wouter van de Kamp on 26/11/2022. +// Created by Wouter van de Kamp on 06/01/2024. // import SwiftUI -struct SheetPlus: ViewModifier { +struct SheetPlus: ViewModifier { @Binding private var isPresented: Bool - + @State private var translation: CGFloat = 0 + @State private var newValue = 0.0 + @State private var startTime: DragGesture.Value? + @State private var sheetConfig: SheetPlusConfig? + @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) + + @State private var detents: Set = [] + @State private var showDragIndicator: VisibilityPlus? - @State private var allowBackgroundInteraction: PresentationBackgroundInteractionPlus? + @State private var cornerRadius: CGFloat? + @State private var background: EquatableBackground? @State private var isInteractiveDismissDisabled = true - @State private var newValue = 0.0 - @State private var startTime: DragGesture.Value? - - @State private var detents: Set = [] - @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) + @State private var backgroundInteractionDetentLimit: CGFloat? + @State private var isBackgroundInteractionEnabled = true - @State private var initialSelectedDetent: PresentationDetent? = nil - @State private var isDismissed = false + @State private var viewWillPresent = false + let animationCurve: SheetAnimation + let onDrag: (CGFloat) -> Void let mainContent: MContent let headerContent: HContent - let animationCurve: SheetAnimation let onDismiss: () -> Void - let onDrag: (CGFloat) -> Void - let background: Background - + init( isPresented: Binding, animationCurve: SheetAnimation, - background: Background, onDismiss: @escaping () -> Void, - onDrag: @escaping (CGFloat) -> Void, + onDrag: @escaping (CGFloat) -> Void = { _ in }, @ViewBuilder hcontent: () -> HContent, @ViewBuilder mcontent: () -> MContent ) { self._isPresented = isPresented - self.animationCurve = animationCurve - self.background = background + self.onDismiss = onDismiss self.onDrag = onDrag - + self.headerContent = hcontent() self.mainContent = mcontent() } - - func body(content: Content) -> some View { - ZStack() { - content -// .allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true) - .onChange(of: isPresented) { newValue in - guard let initialSelectedDetent = initialSelectedDetent else { return } - if newValue == true { - isDismissed = false - sheetConfig?.selectedDetent = initialSelectedDetent - } + func isInteractionEnabled() -> Bool { + return true + } - if newValue == false { - translation = 0 - sheetConfig?.selectedDetent = PresentationDetent.height(.zero) + func body(content: Content) -> some View { + ZStack { + content + .allowsHitTesting(!isPresented || isBackgroundInteractionEnabled) + .zIndex(1) + .onChange(of: isPresented) { value in + if (value == true) { + viewWillPresent = value + } else { + translation = .zero } } - if !isDismissed { - GeometryReader { geometry in - VStack(spacing: 0) { - // If / else statement here breaks the animation from the bottom level - // Might want to see if we can refactor the top level animation a bit - DragIndicator( - translation: $translation, - detents: detents - ) - .frame(height: showDragIndicator == .visible ? 22 : 0) - .opacity(showDragIndicator == .visible ? 1 : 0) + if viewWillPresent { + // VStack to stick the sheet to the bottom + VStack(spacing: 0) { + Spacer() - headerContent + // VStack to keep the drag indicator and header at the top of the card + VStack(spacing: 0) { + // Geometry reader keeps the content outside the header vertically centered +// GeometryReader { geometry in + // Holds the header customization + VStack(spacing: 0) { + DragIndicator( + translation: $translation, + detents: detents + ) + .frame(height: showDragIndicator == .hidden ? 0 : 22) + .opacity(showDragIndicator == .hidden ? 0 : 1) + + headerContent + } +// .frame(width: geometry.size.width) .contentShape(Rectangle()) .gesture( DragGesture(coordinateSpace: .global) .onChanged { value in translation -= value.location.y - value.startLocation.y - newValue newValue = value.location.y - value.startLocation.y - + if startTime == nil { startTime = value } } .onEnded { value in - // Reset the distance on release so we start with a - // clean translation next time - newValue = 0 - - // Calculate velocity based on pt/s so it matches the UIPanGesture - let distance: CGFloat = value.translation.height - let time: CGFloat = value.time.timeIntervalSince(startTime!.time) - - let yVelocity: CGFloat = -1 * ((distance / time) / 1000) - startTime = nil - - if let result = snapBottomSheet( - translation, - detents, - yVelocity, - isInteractiveDismissDisabled - ) { - translation = result.size - sheetConfig?.selectedDetent = result - } + onDragEnded(with: value) } ) - - UIScrollViewWrapper( - translation: $translation, - preferenceKey: $sheetConfig, - isInteractiveDismissDisabled: $isInteractiveDismissDisabled, - limits: limits, - detents: detents - ) { - mainContent - .frame(width: geometry.size.width) +// } + +// Spacer() + + GeometryReader { geometry in + UIScrollViewWrapper( + translation: $translation, + preferenceKey: $sheetConfig, + isInteractiveDismissDisabled: $isInteractiveDismissDisabled, + limits: limits, + detents: detents + ) { + mainContent + .frame(width: geometry.size.width) + } } + +// Spacer() } - .background(background) - .frame(height: - (limits.max - geometry.safeAreaInsets.top) > 0 - ? limits.max - geometry.safeAreaInsets.top - : limits.max + .background( + ZStack() { + background?.view + .cornerRadius(cornerRadius ?? 0) + } ) - .onChange(of: translation) { newValue in - // Small little hack to make the iOS scroll behaviour work smoothly - if limits.max == 0 { return } - - let minValue = isInteractiveDismissDisabled && isPresented ? limits.min : 0 - translation = min(limits.max, max(newValue, minValue)) + .frame( + width: UIScreen.main.bounds.width, + height: translation + ) + .clipped() + .onChange(of: translation) { value in + let minLimit = isInteractiveDismissDisabled && isPresented ? limits.min : .zero + translation = min(limits.max, max(value, minLimit)) -// currentGlobalTranslation = translation + if let interactionDetent = backgroundInteractionDetentLimit { + isBackgroundInteractionEnabled = translation < interactionDetent + } } .onAnimationChange(of: translation) { value in - onDrag(value > 0 ? value : 0) + onDrag(value > .zero ? value : .zero) - if value <= 0 && sheetConfig?.selectedDetent == .height(.zero) { - isDismissed = true + if (translation == 0 && value == .zero) { isPresented = false + viewWillPresent = false } } - .offset(y: UIScreen.main.bounds.height - translation) .onDisappear { onDismiss() } @@ -164,22 +163,11 @@ struct SheetPlus: ViewModifier ) ) } - .edgesIgnoringSafeArea([.bottom]) - .transition(.move(edge: .bottom)) + .edgesIgnoringSafeArea(.bottom) + .zIndex(2) } } .onPreferenceChange(SheetPlusKey.self) { value in - /// Quick hack to prevent the scrollview from resetting the height when keyboard shows up. - /// Replace if the root cause has been located. - if value.detents.count == 0 { - isPresented = false - return - } - - if initialSelectedDetent == nil { - initialSelectedDetent = value.selectedDetent - } - sheetConfig = value translation = value.translation @@ -189,11 +177,43 @@ struct SheetPlus: ViewModifier .onPreferenceChange(SheetPlusIndicatorKey.self) { value in showDragIndicator = value } -// .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in -// allowBackgroundInteraction = value -// } + .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in + backgroundInteractionDetentLimit = value + } .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in isInteractiveDismissDisabled = value } + .onPreferenceChange(SheetPlusPresentationCornerRadiusKey.self) { value in + cornerRadius = value + } + .onPreferenceChange(SheetPlusPresentationBackgroundKey.self) { value in + background = value + } + } + + func onDragEnded(with value: DragGesture.Value) { + // Reset the distance on release so we start with a + // clean translation next time + newValue = 0 + + // Calculate velocity based on pt/s so it matches the UIPanGesture + let distance: CGFloat = value.translation.height + let time: CGFloat = startTime != nil ? value.time.timeIntervalSince(startTime!.time) : 0 + + let yVelocity: CGFloat = -1 * ((distance / time) / 1000) + startTime = nil + + if let result = snapBottomSheet( + translation, + detents, + yVelocity, + isInteractiveDismissDisabled + ) { + translation = result.size + + if result.size != .zero { + sheetConfig?.selectedDetent = result + } + } } } diff --git a/Sources/BottomSheet/BottomSheetLegacy.swift b/Sources/BottomSheet/BottomSheetLegacy.swift new file mode 100644 index 0000000..b7e0e7b --- /dev/null +++ b/Sources/BottomSheet/BottomSheetLegacy.swift @@ -0,0 +1,199 @@ +// +// BottomSheet.swift +// +// +// Created by Wouter van de Kamp on 26/11/2022. +// + +import SwiftUI + +struct SheetPlusLegacy: ViewModifier { + @Binding private var isPresented: Bool + + @State private var translation: CGFloat = 0 + @State private var sheetConfig: SheetPlusConfig? + @State private var showDragIndicator: VisibilityPlus? + @State private var allowBackgroundInteraction: PresentationBackgroundInteractionPlus? + @State private var isInteractiveDismissDisabled = true + + @State private var newValue = 0.0 + @State private var startTime: DragGesture.Value? + + @State private var detents: Set = [] + @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) + + @State private var initialSelectedDetent: PresentationDetent? = nil + @State private var isDismissed = false + + let mainContent: MContent + let headerContent: HContent + let animationCurve: SheetAnimation + let onDismiss: () -> Void + let onDrag: (CGFloat) -> Void + let background: Background + + init( + isPresented: Binding, + animationCurve: SheetAnimation, + background: Background, + onDismiss: @escaping () -> Void, + onDrag: @escaping (CGFloat) -> Void, + @ViewBuilder hcontent: () -> HContent, + @ViewBuilder mcontent: () -> MContent + ) { + self._isPresented = isPresented + + self.animationCurve = animationCurve + self.background = background + self.onDismiss = onDismiss + self.onDrag = onDrag + + self.headerContent = hcontent() + self.mainContent = mcontent() + } + + func body(content: Content) -> some View { + ZStack() { + content +// .allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true) + .onChange(of: isPresented) { newValue in + guard let initialSelectedDetent = initialSelectedDetent else { return } + + if newValue == true { + isDismissed = false + sheetConfig?.selectedDetent = initialSelectedDetent + } + + if newValue == false { + translation = 0 + sheetConfig?.selectedDetent = PresentationDetent.height(.zero) + } + } + + if !isDismissed { + GeometryReader { geometry in + VStack(spacing: 0) { + // If / else statement here breaks the animation from the bottom level + // Might want to see if we can refactor the top level animation a bit + DragIndicator( + translation: $translation, + detents: detents + ) + .frame(height: showDragIndicator == .visible ? 22 : 0) + .opacity(showDragIndicator == .visible ? 1 : 0) + + headerContent + .contentShape(Rectangle()) + .gesture( + DragGesture(coordinateSpace: .global) + .onChanged { value in + translation -= value.location.y - value.startLocation.y - newValue + newValue = value.location.y - value.startLocation.y + + if startTime == nil { + startTime = value + } + } + .onEnded { value in + // Reset the distance on release so we start with a + // clean translation next time + newValue = 0 + + // Calculate velocity based on pt/s so it matches the UIPanGesture + let distance: CGFloat = value.translation.height + let time: CGFloat = value.time.timeIntervalSince(startTime!.time) + + let yVelocity: CGFloat = -1 * ((distance / time) / 1000) + startTime = nil + + if let result = snapBottomSheet( + translation, + detents, + yVelocity, + isInteractiveDismissDisabled + ) { + translation = result.size + sheetConfig?.selectedDetent = result + } + } + ) + + UIScrollViewWrapper( + translation: $translation, + preferenceKey: $sheetConfig, + isInteractiveDismissDisabled: $isInteractiveDismissDisabled, + limits: limits, + detents: detents + ) { + mainContent + .frame(width: geometry.size.width) + } + } + .background(background) + .frame(height: + (limits.max - geometry.safeAreaInsets.top) > 0 + ? limits.max - geometry.safeAreaInsets.top + : limits.max + ) + .onChange(of: translation) { newValue in + // Small little hack to make the iOS scroll behaviour work smoothly + if limits.max == 0 { return } + + let minValue = isInteractiveDismissDisabled && isPresented ? limits.min : 0 + translation = min(limits.max, max(newValue, minValue)) + +// currentGlobalTranslation = translation + } + .onAnimationChange(of: translation) { value in + onDrag(value > 0 ? value : 0) + + if value <= 0 && sheetConfig?.selectedDetent == .height(.zero) { + isDismissed = true + isPresented = false + } + } + .offset(y: UIScreen.main.bounds.height - translation) + .onDisappear { + onDismiss() + } + .animation( + .interpolatingSpring( + mass: animationCurve.mass, + stiffness: animationCurve.stiffness, + damping: animationCurve.damping + ) + ) + } + .edgesIgnoringSafeArea([.bottom]) + .transition(.move(edge: .bottom)) + } + } + .onPreferenceChange(SheetPlusKey.self) { value in + /// Quick hack to prevent the scrollview from resetting the height when keyboard shows up. + /// Replace if the root cause has been located. + if value.detents.count == 0 { + isPresented = false + return + } + + if initialSelectedDetent == nil { + initialSelectedDetent = value.selectedDetent + } + + sheetConfig = value + translation = value.translation + + detents = value.detents + limits = detentLimits(detents: detents) + } + .onPreferenceChange(SheetPlusIndicatorKey.self) { value in + showDragIndicator = value + } +// .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in +// allowBackgroundInteraction = value +// } + .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in + isInteractiveDismissDisabled = value + } + } +} diff --git a/Sources/BottomSheet/BottomSheetV2.swift b/Sources/BottomSheet/BottomSheetV2.swift deleted file mode 100644 index 8bedecd..0000000 --- a/Sources/BottomSheet/BottomSheetV2.swift +++ /dev/null @@ -1,232 +0,0 @@ -// -// SwiftUIView.swift -// -// -// Created by Wouter van de Kamp on 06/01/2024. -// - -import SwiftUI - -struct SheetPlusV2: ViewModifier { - @Binding private var isPresented: Bool - - @State private var translation: CGFloat = 0 - @State private var newValue = 0.0 - @State private var startTime: DragGesture.Value? - - @State private var sheetConfig: SheetPlusConfig? - @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) - - @State private var detents: Set = [] - - @State private var showDragIndicator: VisibilityPlus? - @State private var cornerRadius: CGFloat? - @State private var background: EquatableBackground? - @State private var isInteractiveDismissDisabled = true - - @State private var backgroundInteractionDetentLimit: PresentationDetent? - @State private var isBackgroundInteractionEnabled = true - - @State private var viewWillPresent = false - - let animationCurve: SheetAnimation - let onDrag: (CGFloat) -> Void - let mainContent: MContent - let headerContent: HContent - let onDismiss: () -> Void - - init( - isPresented: Binding, - animationCurve: SheetAnimation, - onDismiss: @escaping () -> Void, - onDrag: @escaping (CGFloat) -> Void = { _ in }, - @ViewBuilder hcontent: () -> HContent, - @ViewBuilder mcontent: () -> MContent - ) { - self._isPresented = isPresented - self.animationCurve = animationCurve - - self.onDismiss = onDismiss - self.onDrag = onDrag - - self.headerContent = hcontent() - self.mainContent = mcontent() - } - - func isInteractionEnabled() -> Bool { - return true - } - - func body(content: Content) -> some View { - ZStack { - content - .allowsHitTesting(!isPresented || isBackgroundInteractionEnabled) - .zIndex(1) - .onChange(of: isPresented) { value in - if (value == true) { - viewWillPresent = value - } else { - translation = .zero - } - } - - if viewWillPresent { - // VStack to stick the sheet to the bottom - VStack(spacing: 0) { - Spacer() - - // VStack to keep the drag indicator and header at the top of the card - VStack(spacing: 0) { - - // Holds the background so it animates - VStack(spacing: 0) { - - // Holds the header customization - VStack(spacing: 0) { - if showDragIndicator == .visible { - DragIndicator( - translation: $translation, - detents: detents - ) - } - - headerContent - .contentShape(Rectangle()) - } - .gesture( - DragGesture(coordinateSpace: .global) - .onChanged { value in - translation -= value.location.y - value.startLocation.y - newValue - newValue = value.location.y - value.startLocation.y - - if startTime == nil { - startTime = value - } - } - .onEnded { value in - onDragEnded(with: value) - } - ) - - Spacer() - - UIScrollViewWrapper( - translation: $translation, - preferenceKey: $sheetConfig, - isInteractiveDismissDisabled: $isInteractiveDismissDisabled, - limits: limits, - detents: detents - ) { - HStack { - Spacer() - mainContent - Spacer() - } - } - - Spacer() - } - - } - .background( - ZStack() { - background?.view - .cornerRadius(cornerRadius ?? 0) - } - ) - .frame( - width: UIScreen.main.bounds.width, - height: translation - ) - .clipped() - .onChange(of: translation) { value in - let minLimit = isInteractiveDismissDisabled && isPresented ? limits.min : .zero - translation = min(limits.max, max(value, minLimit)) - - if let interactionDetent = backgroundInteractionDetentLimit { - isBackgroundInteractionEnabled = translation < interactionDetent.size - } - } - .onAnimationChange(of: translation) { value in - onDrag(value > .zero ? value : .zero) - - if (translation == 0 && value == .zero) { - isPresented = false - viewWillPresent = false - } - } - .onDisappear { - onDismiss() - } - .animation( - .interpolatingSpring( - mass: animationCurve.mass, - stiffness: animationCurve.stiffness, - damping: animationCurve.damping - ) - ) - } - .edgesIgnoringSafeArea(.bottom) - .zIndex(2) - } - } - .onPreferenceChange(SheetPlusKey.self) { value in - sheetConfig = value - translation = value.translation - - detents = value.detents - limits = detentLimits(detents: detents) - } - .onPreferenceChange(SheetPlusIndicatorKey.self) { value in - showDragIndicator = value - } - .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in - // Custom assignment for now until I figure out a way to use SPBIK with a struct. - backgroundInteractionDetentLimit = PresentationBackgroundInteractionPlus.detent - - if value == .automatic || value == .enabled { - isBackgroundInteractionEnabled = true - return - } - - isBackgroundInteractionEnabled = false - - print(isBackgroundInteractionEnabled) - } - .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in - isInteractiveDismissDisabled = value - } - .onPreferenceChange(SheetPlusPresentationCornerRadiusKey.self) { value in - cornerRadius = value - } - .onPreferenceChange(SheetPlusPresentationBackgroundKey.self) { value in - background = value - } - } - - func onDragEnded(with value: DragGesture.Value) { - // Reset the distance on release so we start with a - // clean translation next time - newValue = 0 - - // Calculate velocity based on pt/s so it matches the UIPanGesture - let distance: CGFloat = value.translation.height - let time: CGFloat = startTime != nil ? value.time.timeIntervalSince(startTime!.time) : 0 - - let yVelocity: CGFloat = -1 * ((distance / time) / 1000) - startTime = nil - - if let result = snapBottomSheet( - translation, - detents, - yVelocity, - isInteractiveDismissDisabled - ) { - translation = result.size - - if result.size != .zero { - sheetConfig?.selectedDetent = result - } - } - } -} diff --git a/Sources/BottomSheet/Detents/Detents.swift b/Sources/BottomSheet/Detents/Detents.swift index e1b7f6d..f488949 100644 --- a/Sources/BottomSheet/Detents/Detents.swift +++ b/Sources/BottomSheet/Detents/Detents.swift @@ -17,7 +17,7 @@ import SwiftUI - `height`: A constant height. */ -public enum PresentationDetent: Hashable { +public enum PresentationDetent: Hashable, Sendable { /** The system detent for a sheet that’s approximately a quarter height of the screen, and is inactive in compact height. */ diff --git a/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift b/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift index 00f3399..6abadb8 100644 --- a/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift +++ b/Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift @@ -7,24 +7,32 @@ import SwiftUI -public enum PresentationBackgroundInteractionPlus: Equatable { - case automatic - case disabled - case enabled +public struct PresentationBackgroundInteractionPlus: Hashable, Sendable { + internal enum Kind: Hashable, Sendable { + case automatic + case disabled + case enabled(upThrough: PresentationDetent?) + } + + internal let kind: Kind + + internal init(kind: Kind) { + self.kind = kind + } - internal static var detent: PresentationDetent? - internal static var enabledUpThrough: PresentationBackgroundInteractionPlus = .enabledUpThrough + public static let automatic = Self.init(kind: .automatic) + public static let enabled = Self.init(kind: .enabled(upThrough: nil)) + public static let disabled = Self.init(kind: .disabled) - public static func enabled(upThrough detent: PresentationDetent) -> PresentationBackgroundInteractionPlus { - self.detent = detent - return enabledUpThrough + public static func enabled(upThrough detent: PresentationDetent) -> Self { + .init(kind: .enabled(upThrough: detent)) } } struct SheetPlusBackgroundInteractionKey: PreferenceKey { - static var defaultValue: PresentationBackgroundInteractionPlus = .automatic + static var defaultValue: CGFloat? = nil - static func reduce(value: inout PresentationBackgroundInteractionPlus, nextValue: () -> PresentationBackgroundInteractionPlus) { + static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { value = nextValue() } } diff --git a/Sources/BottomSheet/View Modifiers/View+BackgroundInteraction.swift b/Sources/BottomSheet/View Modifiers/View+BackgroundInteraction.swift index 2ba48cb..36dd00b 100644 --- a/Sources/BottomSheet/View Modifiers/View+BackgroundInteraction.swift +++ b/Sources/BottomSheet/View Modifiers/View+BackgroundInteraction.swift @@ -11,9 +11,15 @@ extension View { public func presentationBackgroundInteractionPlus( _ interaction: PresentationBackgroundInteractionPlus ) -> some View { - return self.preference( - key: SheetPlusBackgroundInteractionKey.self, - value: interaction - ) + return transformPreference(SheetPlusBackgroundInteractionKey.self) { value in + switch interaction.kind { + case .automatic: + value = nil + case .disabled: + value = 0 + case .enabled(let detent): + value = detent?.size ?? 0 + } + } } } diff --git a/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift b/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift index 08e1a10..6f80406 100644 --- a/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift +++ b/Sources/BottomSheet/View Modifiers/View+SheetPlus.swift @@ -8,14 +8,13 @@ import SwiftUI extension View { - public func sheetPlus( + public func sheetPlus( isPresented: Binding, animationCurve: SheetAnimation = SheetAnimation( mass: SheetAnimationDefaults.mass, stiffness: SheetAnimationDefaults.stiffness, damping: SheetAnimationDefaults.damping ), - background: Background = Color(UIColor.systemBackground), onDismiss: @escaping () -> Void = {}, onDrag: @escaping (CGFloat) -> Void = { _ in }, header: () -> HContent = { EmptyView() }, @@ -23,31 +22,6 @@ extension View { ) -> some View { modifier( SheetPlus( - isPresented: isPresented, - animationCurve: animationCurve, - background: background, - onDismiss: onDismiss, - onDrag: onDrag, - hcontent: header, - mcontent: main - ) - ) - } - - public func sheetPlusV2( - isPresented: Binding, - animationCurve: SheetAnimation = SheetAnimation( - mass: SheetAnimationDefaults.mass, - stiffness: SheetAnimationDefaults.stiffness, - damping: SheetAnimationDefaults.damping - ), - onDismiss: @escaping () -> Void = {}, - onDrag: @escaping (CGFloat) -> Void = { _ in }, - header: () -> HContent = { EmptyView() }, - main: () -> MContent - ) -> some View { - modifier( - SheetPlusV2( isPresented: isPresented, animationCurve: animationCurve, onDismiss: onDismiss, From 1a078fdd95cd97e1392910c7077e55d759776268 Mon Sep 17 00:00:00 2001 From: Wouter Date: Tue, 4 Jun 2024 23:54:43 +0200 Subject: [PATCH 10/10] chore: remove legacy files --- .../project.pbxproj | 4 - .../BottomSheetExample/SheetV2Example.swift | 85 -------- Sources/BottomSheet/BottomSheetLegacy.swift | 199 ------------------ 3 files changed, 288 deletions(-) delete mode 100644 Example/BottomSheetExample/SheetV2Example.swift delete mode 100644 Sources/BottomSheet/BottomSheetLegacy.swift diff --git a/Example/BottomSheetExample.xcodeproj/project.pbxproj b/Example/BottomSheetExample.xcodeproj/project.pbxproj index 8d7bcb5..10b085a 100644 --- a/Example/BottomSheetExample.xcodeproj/project.pbxproj +++ b/Example/BottomSheetExample.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 6C0359AC2B496FB90012B90A /* SheetV2Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0359AB2B496FB90012B90A /* SheetV2Example.swift */; }; 6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */; }; 6C0BD47127DA862D000AF3CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47027DA862D000AF3CD /* Assets.xcassets */; }; 6C0BD47427DA862D000AF3CD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */; }; @@ -19,7 +18,6 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 6C0359AB2B496FB90012B90A /* SheetV2Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetV2Example.swift; sourceTree = ""; }; 6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BottomSheetExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetExampleApp.swift; sourceTree = ""; }; 6C0BD47027DA862D000AF3CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -65,7 +63,6 @@ isa = PBXGroup; children = ( 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */, - 6C0359AB2B496FB90012B90A /* SheetV2Example.swift */, 6C763146283D774500463709 /* ExampleOverview.swift */, 6CF78516293D3703000E6581 /* Apple Applications */, 6C8CBF1B2AED12C60007E10E /* Examples */, @@ -213,7 +210,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6C0359AC2B496FB90012B90A /* SheetV2Example.swift in Sources */, 6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */, 6C8CBF1D2AED12E00007E10E /* StaticScrollViewExample.swift in Sources */, 6CDF5A0D27ED33C7004609F4 /* CornerRadius.swift in Sources */, diff --git a/Example/BottomSheetExample/SheetV2Example.swift b/Example/BottomSheetExample/SheetV2Example.swift deleted file mode 100644 index b6e9116..0000000 --- a/Example/BottomSheetExample/SheetV2Example.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// SheetV2Example.swift -// BottomSheetExample -// -// Created by Wouter van de Kamp on 06/01/2024. -// - -import SwiftUI -import BottomSheet - -class SheetSettingsV2: ObservableObject { - @Published var isPresented = false - @Published var selectedDetent: BottomSheet.PresentationDetent = .medium -} - -struct NavigationViewV2: View { - @State var text = "Text" - @StateObject var settings = SheetSettings() - - var views: [(label: String, view: AnyView)] = [ - (label: "V2 example", view: AnyView(SheetV2Example())) - ] - - var body: some View { - NavigationView { - List(views.indices, id: \.self) { index in - NavigationLink(destination: views[index].view) { - Text(views[index].label) - } - } - .onAppear { - settings.isPresented = false - } - } - .environmentObject(settings) - .sheetPlus( - isPresented: $settings.isPresented, - header: { - Text("Test") - }, - main: { - VStack { - Spacer() - Text("Main content") - Spacer() - } - .presentationDetentsPlus( - [ - PresentationDetent.fraction(0.1), - PresentationDetent.fraction(0.3), - PresentationDetent.fraction(0.8) - ], - selection: $settings.selectedDetent - ) - .presentationBackgroundPlus { - Color.yellow - } - .presentationBackgroundInteractionPlus(.automatic) - .presentationDragIndicatorPlus(.visible) - .presentationCornerRadiusPlus(cornerRadius: 12) - .interactiveDismissDisabledPlus(false) - } - ) - } -} - -struct SheetV2Example: View { - @EnvironmentObject var settings: SheetSettings - - var body: some View { - VStack { - Text("Hello, World!") - - Text("Is Presented, \(settings.isPresented ? "True" : "False")") - - Button("Test") { - settings.isPresented = !settings.isPresented - } - } - } -} - -#Preview { - SheetV2Example() -} diff --git a/Sources/BottomSheet/BottomSheetLegacy.swift b/Sources/BottomSheet/BottomSheetLegacy.swift deleted file mode 100644 index b7e0e7b..0000000 --- a/Sources/BottomSheet/BottomSheetLegacy.swift +++ /dev/null @@ -1,199 +0,0 @@ -// -// BottomSheet.swift -// -// -// Created by Wouter van de Kamp on 26/11/2022. -// - -import SwiftUI - -struct SheetPlusLegacy: ViewModifier { - @Binding private var isPresented: Bool - - @State private var translation: CGFloat = 0 - @State private var sheetConfig: SheetPlusConfig? - @State private var showDragIndicator: VisibilityPlus? - @State private var allowBackgroundInteraction: PresentationBackgroundInteractionPlus? - @State private var isInteractiveDismissDisabled = true - - @State private var newValue = 0.0 - @State private var startTime: DragGesture.Value? - - @State private var detents: Set = [] - @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) - - @State private var initialSelectedDetent: PresentationDetent? = nil - @State private var isDismissed = false - - let mainContent: MContent - let headerContent: HContent - let animationCurve: SheetAnimation - let onDismiss: () -> Void - let onDrag: (CGFloat) -> Void - let background: Background - - init( - isPresented: Binding, - animationCurve: SheetAnimation, - background: Background, - onDismiss: @escaping () -> Void, - onDrag: @escaping (CGFloat) -> Void, - @ViewBuilder hcontent: () -> HContent, - @ViewBuilder mcontent: () -> MContent - ) { - self._isPresented = isPresented - - self.animationCurve = animationCurve - self.background = background - self.onDismiss = onDismiss - self.onDrag = onDrag - - self.headerContent = hcontent() - self.mainContent = mcontent() - } - - func body(content: Content) -> some View { - ZStack() { - content -// .allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true) - .onChange(of: isPresented) { newValue in - guard let initialSelectedDetent = initialSelectedDetent else { return } - - if newValue == true { - isDismissed = false - sheetConfig?.selectedDetent = initialSelectedDetent - } - - if newValue == false { - translation = 0 - sheetConfig?.selectedDetent = PresentationDetent.height(.zero) - } - } - - if !isDismissed { - GeometryReader { geometry in - VStack(spacing: 0) { - // If / else statement here breaks the animation from the bottom level - // Might want to see if we can refactor the top level animation a bit - DragIndicator( - translation: $translation, - detents: detents - ) - .frame(height: showDragIndicator == .visible ? 22 : 0) - .opacity(showDragIndicator == .visible ? 1 : 0) - - headerContent - .contentShape(Rectangle()) - .gesture( - DragGesture(coordinateSpace: .global) - .onChanged { value in - translation -= value.location.y - value.startLocation.y - newValue - newValue = value.location.y - value.startLocation.y - - if startTime == nil { - startTime = value - } - } - .onEnded { value in - // Reset the distance on release so we start with a - // clean translation next time - newValue = 0 - - // Calculate velocity based on pt/s so it matches the UIPanGesture - let distance: CGFloat = value.translation.height - let time: CGFloat = value.time.timeIntervalSince(startTime!.time) - - let yVelocity: CGFloat = -1 * ((distance / time) / 1000) - startTime = nil - - if let result = snapBottomSheet( - translation, - detents, - yVelocity, - isInteractiveDismissDisabled - ) { - translation = result.size - sheetConfig?.selectedDetent = result - } - } - ) - - UIScrollViewWrapper( - translation: $translation, - preferenceKey: $sheetConfig, - isInteractiveDismissDisabled: $isInteractiveDismissDisabled, - limits: limits, - detents: detents - ) { - mainContent - .frame(width: geometry.size.width) - } - } - .background(background) - .frame(height: - (limits.max - geometry.safeAreaInsets.top) > 0 - ? limits.max - geometry.safeAreaInsets.top - : limits.max - ) - .onChange(of: translation) { newValue in - // Small little hack to make the iOS scroll behaviour work smoothly - if limits.max == 0 { return } - - let minValue = isInteractiveDismissDisabled && isPresented ? limits.min : 0 - translation = min(limits.max, max(newValue, minValue)) - -// currentGlobalTranslation = translation - } - .onAnimationChange(of: translation) { value in - onDrag(value > 0 ? value : 0) - - if value <= 0 && sheetConfig?.selectedDetent == .height(.zero) { - isDismissed = true - isPresented = false - } - } - .offset(y: UIScreen.main.bounds.height - translation) - .onDisappear { - onDismiss() - } - .animation( - .interpolatingSpring( - mass: animationCurve.mass, - stiffness: animationCurve.stiffness, - damping: animationCurve.damping - ) - ) - } - .edgesIgnoringSafeArea([.bottom]) - .transition(.move(edge: .bottom)) - } - } - .onPreferenceChange(SheetPlusKey.self) { value in - /// Quick hack to prevent the scrollview from resetting the height when keyboard shows up. - /// Replace if the root cause has been located. - if value.detents.count == 0 { - isPresented = false - return - } - - if initialSelectedDetent == nil { - initialSelectedDetent = value.selectedDetent - } - - sheetConfig = value - translation = value.translation - - detents = value.detents - limits = detentLimits(detents: detents) - } - .onPreferenceChange(SheetPlusIndicatorKey.self) { value in - showDragIndicator = value - } -// .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in -// allowBackgroundInteraction = value -// } - .onPreferenceChange(SheetPlusInteractiveDismissDisabledKey.self) { value in - isInteractiveDismissDisabled = value - } - } -}