// | |
// BottomSheetView.swift | |
// | |
// Created by Majid Jabrayilov | |
// Copyright © 2019 Majid Jabrayilov. All rights reserved. | |
// | |
import SwiftUI | |
fileprivate enum Constants { | |
static let radius: CGFloat = 16 | |
static let indicatorHeight: CGFloat = 6 | |
static let indicatorWidth: CGFloat = 60 | |
static let snapRatio: CGFloat = 0.25 | |
static let minHeightRatio: CGFloat = 0.3 | |
} | |
struct BottomSheetView<Content: View>: View { | |
@Binding var isOpen: Bool | |
let maxHeight: CGFloat | |
let minHeight: CGFloat | |
let content: Content | |
@GestureState private var translation: CGFloat = 0 | |
private var offset: CGFloat { | |
isOpen ? 0 : maxHeight - minHeight | |
} | |
private var indicator: some View { | |
RoundedRectangle(cornerRadius: Constants.radius) | |
.fill(Color.secondary) | |
.frame( | |
width: Constants.indicatorWidth, | |
height: Constants.indicatorHeight | |
).onTapGesture { | |
self.isOpen.toggle() | |
} | |
} | |
init(isOpen: Binding<Bool>, maxHeight: CGFloat, @ViewBuilder content: () -> Content) { | |
self.minHeight = maxHeight * Constants.minHeightRatio | |
self.maxHeight = maxHeight | |
self.content = content() | |
self._isOpen = isOpen | |
} | |
var body: some View { | |
GeometryReader { geometry in | |
VStack(spacing: 0) { | |
self.indicator.padding() | |
self.content | |
} | |
.frame(width: geometry.size.width, height: self.maxHeight, alignment: .top) | |
.background(Color(.secondarySystemBackground)) | |
.cornerRadius(Constants.radius) | |
.frame(height: geometry.size.height, alignment: .bottom) | |
.offset(y: max(self.offset + self.translation, 0)) | |
.animation(.interactiveSpring()) | |
.gesture( | |
DragGesture().updating(self.$translation) { value, state, _ in | |
state = value.translation.height | |
}.onEnded { value in | |
let snapDistance = self.maxHeight * Constants.snapRatio | |
guard abs(value.translation.height) > snapDistance else { | |
return | |
} | |
self.isOpen = value.translation.height < 0 | |
} | |
) | |
} | |
} | |
} | |
struct BottomSheetView_Previews: PreviewProvider { | |
static var previews: some View { | |
BottomSheetView(isOpen: .constant(false), maxHeight: 600) { | |
Rectangle().fill(Color.red) | |
}.edgesIgnoringSafeArea(.all) | |
} | |
} |
This comment has been minimized.
This comment has been minimized.
When I call this from a Button the below the result. The blank screen is a sheet with your sheet inside. This is not the expected. .sheet(isPresented: self.$showingActionsheet) { |
This comment has been minimized.
This comment has been minimized.
@JasonGreenAmerica you don't need to use .sheet to present BottomSheet. |
This comment has been minimized.
This comment has been minimized.
It’s this just hidden view?
…________________________________
From: Majid Jabrayilov <notifications@github.com>
Sent: Tuesday, April 28, 2020 7:49:44 AM
To: mecid <mecid@noreply.github.com>
Cc: Green America Technology <jason.amos@greenamericatechnology.com>; Mention <mention@noreply.github.com>
Subject: Re: mecid/BottomSheetView.swift
@mecid commented on this gist.
________________________________
@JasonGreenAmerica<https://github.com/JasonGreenAmerica> you don't need to use .sheet to present BottomSheet.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub<https://gist.github.com/78eab34d05498d6c60ae0f162bfd81ee#gistcomment-3273274>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/ADKS446Y7P3X7L6PPRZAV7LRO3UIRANCNFSM4KBZ5Z3Q>.
|
This comment has been minimized.
This comment has been minimized.
@JasonGreenAmerica take a look at my post, there is the usage example in the end. |
This comment has been minimized.
This comment has been minimized.
Hey @mecid, love this gist. Thanks so much for sharing this. In my use cases, the implicit animation dictates all views with explicit animation on the sheet. So I updated it to use explicit animation. I really wanted to contribute to this gist. But looks like I can't so I've forked and modified it here https://gist.github.com/landtanin/dc8bc52f77345fd078deb64f91393008. Just want to let you know and sort of contribute back. Thanks again! |
This comment has been minimized.
This comment has been minimized.
@landtanin Thanks for your contribution 🙏🏻 |
This comment has been minimized.
This comment has been minimized.
@mecid Thank you so much for making this gist available! But when I put a |
This comment has been minimized.
This comment has been minimized.
Hi, I can't figure out how to add content to the slide. From the looks of it, I need to give 'content' a body as 'content' has restrains to only show content on the slide. |
This comment has been minimized.
This comment has been minimized.
Nevermind, think I've worked it out. I've basically made another var like indicator which'll have my custom view in it. |
This comment has been minimized.
This comment has been minimized.
@mxgc I'm not sure there is a way to control it for now. |
This comment has been minimized.
This comment has been minimized.
@mxgc I was able to get it working with a Slider. Basically we need to track
|
This comment has been minimized.
This comment has been minimized.
How to open bottom sheet on button click? |
This comment has been minimized.
This comment has been minimized.
@javariahazoor toggle your isOpen boolean. |
This comment has been minimized.
This comment has been minimized.
Could you provide an example of how to toggle isOpen Boolen from another swift file? |
This comment has been minimized.
This comment has been minimized.
@javariahazoor How u success to make it? |
This comment has been minimized.
This comment has been minimized.
@matarali since it is a binding you simply change the property. If it needs to be changed in another file then likely the property will be inside an ObservableObject. |
This comment has been minimized.
This comment has been minimized.
Fantastic! Thank you. |
This comment has been minimized.
This comment has been minimized.
How do I inset the main content view to counteract edgesIgnoringSafeArea? I tried adding padding based on the geometry data:
|
This comment has been minimized.
This comment has been minimized.
I solved this. I wrapped an extra GeometryReader (ugh?) and used its safeAreaInsets for the main content padding. The inner GeometryReader seems to mainly serve the purpose of being a greedy view to use up all the available space, and it ignores the safe area. So: GR1 reads the safe area. GR2 Ignores the safe area. VStack{}.padding(geometry.safeAreaInsets) re-honors the GR1 safe area.
|
This comment has been minimized.
Thank you so much for sharing this Majid. I have used it on a little App that is the last Project (Day 99) as part of Paul Hudsons 100 Days of SwiftUI. it works an absolute treat though I made a a couple of changes to customise it for my needs.
