iOS/SwiftUI

[iOS / SwiftUI] Bottom Sheet Drawer Use with Gesture

TDCIAN 2022. 1. 7. 17:13

Hi! this is TDCIAN!

 

Let's find out about how to make bottom sheet drawer that move with gesture!

 

Look at these codes!

 

(1) Home.swift

 

import SwiftUI
struct Home: View {
// Search text binding value
@State var searchText: String = ""
// Gesture properties
@State var offset: CGFloat = 0
@State var lastOffset: CGFloat = 0
@GestureState var gestureOffset: CGFloat = 0
var body: some View {
ZStack {
// For getting frame for image
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: frame.width, height: frame.height)
}
.blur(radius: getBlurRadius()) // BlurRadius change depends on offset
.ignoresSafeArea()
// For getting height for drag gesture
GeometryReader { proxy -> AnyView in
let height = proxy.frame(in: .global).height
return AnyView(
ZStack {
// Bottom Sheet
BlurView(style: .systemThinMaterialDark)
.clipShape(CustomCorner(corners: [.topLeft, .topRight], radius: 30))
VStack {
VStack {
Capsule()
.fill(Color.white)
.frame(width: 60, height: 4)
TextField("Search", text: $searchText)
.padding(.vertical, 10)
.padding(.horizontal)
.background(BlurView(style: .dark))
.cornerRadius(10)
.colorScheme(.dark)
.padding(.top, 10)
}
.frame(height: 100)
.border(Color.red, width: 1)
// ScrollView content
ScrollView(.vertical, showsIndicators: false, content: {
BottomContent()
})
.border(Color.blue, width: 1)
}
.padding(.horizontal)
.frame(maxHeight: .infinity, alignment: .top)
}
.border(Color.green, width: 1)
.offset(y: height - 100)
.offset(y: -offset > 0 ? -offset <= (height - 100) ? offset : -(height - 100) : 0)
.gesture(DragGesture().updating($gestureOffset, body: { value, state, transcation in
state = value.translation.height
onChange()
}).onEnded({ value in
let maxHeight = height - 100
withAnimation {
// (1) Mid
if -offset > 100 && -offset < maxHeight / 2 {
// * Test each case!
offset = -(maxHeight / 4)
// offset = -(maxHeight / 3)
// offset = -(maxHeight / 2)
// offset = -(maxHeight / 1.5)
// offset = -(maxHeight / 1.25)
// (2) Top
} else if -offset > maxHeight / 2 {
offset = -maxHeight
// (3) Bottom
} else {
offset = 0
}
}
// Storing last offset, so that the gesture can continue from the last position
lastOffset = offset
}))
)
}
.ignoresSafeArea(.all, edges: .bottom)
}
}
func onChange() {
DispatchQueue.main.async {
self.offset = gestureOffset + lastOffset
}
}
// Blur radius for background
func getBlurRadius() -> CGFloat {
let progress = -offset / (UIScreen.main.bounds.height - 100)
return progress * 30
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
}
}
struct BottomContent: View {
var body: some View {
VStack {
HStack {
Text("Favorite")
.fontWeight(.bold)
.foregroundColor(.white)
Spacer()
Button(action: {
}, label: {
Text("See All")
.fontWeight(.bold)
.foregroundColor(.gray)
})
}
.padding(.top, 20)
Divider()
.background(Color.white)
ScrollView(.horizontal, showsIndicators: false, content: {
HStack(spacing: 15) {
VStack(spacing: 8) {
Button(action: {
}, label: {
Image(systemName: "house.fill")
.font(.title)
.frame(width: 65, height: 65)
.background(BlurView(style: .dark))
.clipShape(Circle())
})
Text("Home")
.foregroundColor(Color.white)
}
VStack(spacing: 8) {
Button(action: {
}, label: {
Image(systemName: "briefcase.fill")
.font(.title)
.frame(width: 65, height: 65)
.background(BlurView(style: .dark))
.clipShape(Circle())
})
Text("Work")
.foregroundColor(Color.white)
}
VStack(spacing: 8) {
Button(action: {
}, label: {
Image(systemName: "plus")
.font(.title)
.frame(width: 65, height: 65)
.background(BlurView(style: .dark))
.clipShape(Circle())
})
Text("Add")
.foregroundColor(Color.white)
}
}
})
.padding(.top)
HStack {
Text("Editor's Pick")
.fontWeight(.bold)
.foregroundColor(.white)
Spacer()
Button(action: {
}, label: {
Text("See All")
.fontWeight(.bold)
.foregroundColor(.gray)
})
}
.padding(.top, 25)
Divider()
.background(Color.white)
ForEach(1...6, id: \.self) { index in
Image("p\(index)")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width - 30, height: 250)
.cornerRadius(15)
.padding(.top)
}
}
}
}

 

 

(2) BlurView.swift

 

import SwiftUI
struct BlurView: UIViewRepresentable {
var style: UIBlurEffect.Style
func makeUIView(context: Context) -> UIVisualEffectView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: style))
return view
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
}
}

 

 

(3) CustomCorner.swift

 

import SwiftUI
struct CustomCorner: Shape {
var corners: UIRectCorner
var radius: CGFloat
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}

 

 

(4) ContentView.swift

 

import SwiftUI
struct ContentView: View {
var body: some View {
Home()
}
}

 

 

(5) In simulator

 

When you launched app, In Home.swift, find code in 97th line.

 

When you pushed sheet up lower than half -> In Home.swift, 'offset = -(maxHeight / 4)', find code in 86th line.

 

When you pushed sheet up more than half -> In Home.swift, find code in 93th line.

 

 

Full source code: https://github.com/TDCIAN/SwiftUIBottomSheetDrawer

 

GitHub - TDCIAN/SwiftUIBottomSheetDrawer: based on: https://www.youtube.com/watch?v=CyMtjSspJZA

based on: https://www.youtube.com/watch?v=CyMtjSspJZA - GitHub - TDCIAN/SwiftUIBottomSheetDrawer: based on: https://www.youtube.com/watch?v=CyMtjSspJZA

github.com

 

Reference: https://www.youtube.com/watch?v=CyMtjSspJZA 

 

Images Reference: https://unsplash.com/

 

Beautiful Free Images & Pictures | Unsplash

Beautiful, free images and photos that you can download and use for any project. Better than any royalty free or stock photos.

unsplash.com