diff --git a/Example/PartialSheetExample.xcodeproj/project.pbxproj b/Example/PartialSheetExample.xcodeproj/project.pbxproj index 557d1ac..eccedbb 100644 --- a/Example/PartialSheetExample.xcodeproj/project.pbxproj +++ b/Example/PartialSheetExample.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 3B9841FC24E880870052A996 /* PickerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B9841FA24E880870052A996 /* PickerExample.swift */; }; 3B9841FD24E880870052A996 /* DatePickerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B9841FB24E880870052A996 /* DatePickerExample.swift */; }; 3BF874DF24E6F4DE004F4550 /* BlurredSheetExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF874DE24E6F4DE004F4550 /* BlurredSheetExample.swift */; }; + 9C721F222519347C007E46D4 /* AnimationContentExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C721F212519347C007E46D4 /* AnimationContentExample.swift */; }; F8E410DE25015F710064D3A6 /* ViewModifierExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E410DD25015F710064D3A6 /* ViewModifierExample.swift */; }; /* End PBXBuildFile section */ @@ -52,6 +53,7 @@ 3B9841FA24E880870052A996 /* PickerExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerExample.swift; sourceTree = ""; }; 3B9841FB24E880870052A996 /* DatePickerExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerExample.swift; sourceTree = ""; }; 3BF874DE24E6F4DE004F4550 /* BlurredSheetExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurredSheetExample.swift; sourceTree = ""; }; + 9C721F212519347C007E46D4 /* AnimationContentExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationContentExample.swift; sourceTree = ""; }; F8E410DD25015F710064D3A6 /* ViewModifierExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifierExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -77,6 +79,7 @@ 0174F416245962B80053C454 /* PushNavigationExample.swift */, 3BF874DE24E6F4DE004F4550 /* BlurredSheetExample.swift */, F8E410DD25015F710064D3A6 /* ViewModifierExample.swift */, + 9C721F212519347C007E46D4 /* AnimationContentExample.swift */, ); path = Examples; sourceTree = ""; @@ -212,6 +215,7 @@ 0174F417245962B80053C454 /* PushNavigationExample.swift in Sources */, 3BF874DF24E6F4DE004F4550 /* BlurredSheetExample.swift in Sources */, 01A013962458E4C000D0F5DD /* TextfieldExample.swift in Sources */, + 9C721F222519347C007E46D4 /* AnimationContentExample.swift in Sources */, 01A013942458E4A900D0F5DD /* NormalExample.swift in Sources */, 1F177A6E23E1ECC3006F59D0 /* AppDelegate.swift in Sources */, 01477D8B2458E928007AE720 /* PartialSheetManager.swift in Sources */, diff --git a/Example/PartialSheetExample/ContentView.swift b/Example/PartialSheetExample/ContentView.swift index 8bb0777..da36392 100644 --- a/Example/PartialSheetExample/ContentView.swift +++ b/Example/PartialSheetExample/ContentView.swift @@ -19,7 +19,7 @@ struct ContentView: View { Text(""" Hi, this is the Partial Sheet modifier. - On iPhone devices it allows you to dispaly a totally custom sheet with a relative height based on his content. + On iPhone devices it allows you to display a totally custom sheet with a relative height based on its content. In this way the sheet will cover the screen only for the space it will need. On iPad and Mac devices it will present a normal .sheet view. @@ -31,27 +31,21 @@ struct ContentView: View { .padding() List { - NavigationLink( destination: NormalExample(), label: {Text("Normal Example") - }) NavigationLink( destination: TextfieldExample(), - label: { - Text("Textfield Example") - + label: {Text("Textfield Example") }) NavigationLink( destination: ListExample(), label: {Text("List Example") - }) NavigationLink( destination: PushNavigationExample(), label: {Text("Push Navigation Example") - }) NavigationLink( destination: DatePickerExample(), @@ -69,6 +63,10 @@ struct ContentView: View { destination: ViewModifierExample(), label: {Text("ViewModifier Example") }) + NavigationLink( + destination: AnimationContentExample(), + label: {Text("AnimationContent Example") + }) } Spacer() Spacer() diff --git a/Example/PartialSheetExample/Examples/AnimationContentExample.swift b/Example/PartialSheetExample/Examples/AnimationContentExample.swift new file mode 100644 index 0000000..de200af --- /dev/null +++ b/Example/PartialSheetExample/Examples/AnimationContentExample.swift @@ -0,0 +1,78 @@ +// +// AnimationContentExample.swift +// PartialSheetExample +// +// Created by Eliott Robson on 21/09/2020. +// Copyright © 2020 Swift. All rights reserved. +// + +import SwiftUI + +struct AnimationContentExample: View { + @EnvironmentObject var partialSheet: PartialSheetManager + + var body: some View { + VStack { + Button( + action: { + self.partialSheet.showPartialSheet { + AnimationSheetView() + } + }, + label: { + Text("Show Partial Sheet") + } + ) + } + } +} + +struct AnimationSheetView: View { + + @State private var explicitScale: CGFloat = 1 + + @State private var implicitScale: CGFloat = 1 + + @State private var noScale: CGFloat = 1 + + var body: some View { + VStack { + Text("Tap to animate explicitly") + .padding() + .background(Color.green) + .cornerRadius(5) + .scaleEffect(explicitScale) + .onTapGesture { + withAnimation { + explicitScale = CGFloat.random(in: 0.5..<1.5) + } + } + + Text("Tap to animate implicitly") + .padding() + .background(Color.orange) + .cornerRadius(5) + .scaleEffect(implicitScale) + .animation(.default) + .onTapGesture { + implicitScale = CGFloat.random(in: 0.5..<1.5) + } + + Text("Tap to change with no animation") + .padding() + .background(Color.red) + .cornerRadius(5) + .scaleEffect(noScale) + .onTapGesture { + noScale = CGFloat.random(in: 0.5..<1.5) + } + } + } +} + +struct AnimationContentExample_Previews: PreviewProvider { + static var previews: some View { + AnimationContentExample() + .environmentObject(PartialSheetManager()) + } +} diff --git a/Sources/PartialSheet/PartialSheetManager.swift b/Sources/PartialSheet/PartialSheetManager.swift index c47001a..9df0908 100644 --- a/Sources/PartialSheet/PartialSheetManager.swift +++ b/Sources/PartialSheet/PartialSheetManager.swift @@ -51,7 +51,11 @@ public class PartialSheetManager: ObservableObject { public func showPartialSheet(_ onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> T) where T: View { self.content = AnyView(content()) self.onDismiss = onDismiss - self.isPresented = true + DispatchQueue.main.async { + withAnimation { + self.isPresented = true + } + } } /** @@ -68,7 +72,9 @@ public class PartialSheetManager: ObservableObject { self.onDismiss = onDismiss } if let isPresented = isPresented { - self.isPresented = isPresented + withAnimation { + self.isPresented = isPresented + } } } diff --git a/Sources/PartialSheet/PartialSheetViewModifier.swift b/Sources/PartialSheet/PartialSheetViewModifier.swift index d3aac39..3a8a4a1 100644 --- a/Sources/PartialSheet/PartialSheetViewModifier.swift +++ b/Sources/PartialSheet/PartialSheetViewModifier.swift @@ -23,7 +23,6 @@ struct PartialSheet: ViewModifier { /// The rect containing the presenter @State private var presenterContentRect: CGRect = .zero - /// The rect containing the sheet content @State private var sheetContentRect: CGRect = .zero @@ -31,6 +30,9 @@ struct PartialSheet: ViewModifier { /// The offset for keyboard height @State private var offset: CGFloat = 0 + /// The offset for the drag gesture + @State private var dragOffset: CGFloat = 0 + /// The point for the top anchor private var topAnchor: CGFloat { return max(presenterContentRect.height + @@ -60,19 +62,16 @@ struct PartialSheet: ViewModifier { private var sheetPosition: CGFloat { if self.manager.isPresented { let topInset = UIApplication.shared.windows.first?.safeAreaInsets.top ?? 20.0 // 20.0 = To make sure we dont go under statusbar on screens without safe area inset - let position = self.topAnchor + self.dragState.translation.height - self.offset + let position = self.topAnchor + self.dragOffset - self.offset if position < topInset { return topInset } return position } else { - return self.bottomAnchor - self.dragState.translation.height + return self.bottomAnchor - self.dragOffset } } - - /// The Gesture State for the drag gesture - @GestureState private var dragState = DragState.inactive /// Background of sheet private var background: AnyView { @@ -216,7 +215,6 @@ extension PartialSheet { Color.clear.preference(key: SheetPreferenceKey.self, value: [PreferenceData(bounds: proxy.frame(in: .global))]) } ) - .animation(nil) } Spacer() } @@ -228,8 +226,6 @@ extension PartialSheet { .cornerRadius(style.cornerRadius) .shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0) .offset(y: self.sheetPosition) - .animation(self.dragState.isDragging ? - nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0)) .gesture(drag) } } @@ -240,56 +236,68 @@ extension PartialSheet { extension PartialSheet { /// Create a new **DragGesture** with *updating* and *onEndend* func - private func dragGesture() -> _EndedGesture> { + private func dragGesture() -> _EndedGesture<_ChangedGesture> { DragGesture(minimumDistance: 30, coordinateSpace: .local) - .updating($dragState) { drag, state, _ in - self.dismissKeyboard() - let yOffset = drag.translation.height - let threshold = CGFloat(-50) - let stiffness = CGFloat(0.3) - if yOffset > threshold { - state = .dragging(translation: drag.translation) - } else if - // if above threshold and belove ScreenHeight make it elastic - -yOffset + self.sheetContentRect.height < - UIScreen.main.bounds.height + self.handlerSectionHeight - { - let distance = yOffset - threshold - let translationHeight = threshold + (distance * stiffness) - state = .dragging(translation: CGSize(width: drag.translation.width, height: translationHeight)) - } + .onChanged(onDragChanged) + .onEnded(onDragEnded) + } + + private func onDragChanged(drag: DragGesture.Value) { + self.dismissKeyboard() + let yOffset = drag.translation.height + let threshold = CGFloat(-50) + let stiffness = CGFloat(0.3) + if yOffset > threshold { + dragOffset = drag.translation.height + } else if + // if above threshold and belove ScreenHeight make it elastic + -yOffset + self.sheetContentRect.height < + UIScreen.main.bounds.height + self.handlerSectionHeight + { + let distance = yOffset - threshold + let translationHeight = threshold + (distance * stiffness) + dragOffset = translationHeight } - .onEnded(onDragEnded) } /// The method called when the drag ends. It moves the sheet in the correct position based on the last drag gesture private func onDragEnded(drag: DragGesture.Value) { /// The drag direction let verticalDirection = drag.predictedEndLocation.y - drag.location.y - /// The current sheet position - let cardTopEdgeLocation = topAnchor + drag.translation.height - - // Get the closest anchor point based on the current position of the sheet - let closestPosition: CGFloat - - if (cardTopEdgeLocation - topAnchor) < (bottomAnchor - cardTopEdgeLocation) { - closestPosition = topAnchor - } else { - closestPosition = bottomAnchor - } // Set the correct anchor point based on the vertical direction of the drag if verticalDirection > 1 { DispatchQueue.main.async { - self.manager.isPresented = false - self.manager.onDismiss?() + withAnimation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0)) { + dragOffset = 0 + self.manager.isPresented = false + self.manager.onDismiss?() + } } } else if verticalDirection < 0 { - self.manager.isPresented = true + withAnimation { + dragOffset = 0 + self.manager.isPresented = true + } } else { - self.manager.isPresented = (closestPosition == topAnchor) - if !manager.isPresented { - manager.onDismiss?() + /// The current sheet position + let cardTopEdgeLocation = topAnchor + drag.translation.height + + // Get the closest anchor point based on the current position of the sheet + let closestPosition: CGFloat + + if (cardTopEdgeLocation - topAnchor) < (bottomAnchor - cardTopEdgeLocation) { + closestPosition = topAnchor + } else { + closestPosition = bottomAnchor + } + + withAnimation { + dragOffset = 0 + self.manager.isPresented = (closestPosition == topAnchor) + if !manager.isPresented { + manager.onDismiss?() + } } } } @@ -304,14 +312,18 @@ extension PartialSheet { if let rect: CGRect = notification.userInfo![endFrame] as? CGRect { let height = rect.height let bottomInset = UIApplication.shared.windows.first?.safeAreaInsets.bottom - self.offset = height - (bottomInset ?? 0) + withAnimation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0)) { + self.offset = height - (bottomInset ?? 0) + } } } /// Remove the keyboard offset private func keyboardHide(notification: Notification) { DispatchQueue.main.async { - self.offset = 0 + withAnimation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0)) { + self.offset = 0 + } } }