Skip to content

Instantly share code, notes, and snippets.

@jaecheoljung
Last active April 8, 2021 11:54
Show Gist options
  • Save jaecheoljung/0c3e23db87c5f8ffb40efe55081826a7 to your computer and use it in GitHub Desktop.
Save jaecheoljung/0c3e23db87c5f8ffb40efe55081826a7 to your computer and use it in GitHub Desktop.
(Floating Panel || Bottom Sheet || Floating View) in SwiftUI
//
// FloatingPanel.swift
// UITest
//
// Created by USER on 2021/04/07.
//
import SwiftUI
import Introspect
class FloatingPanelViewModel: NSObject, ObservableObject {
@Published var height: CGFloat
@Published var tableOffset: CGPoint
@Published var which: String
var initialOffset: CGPoint
var top: CGFloat
var topThreshold: CGFloat
var middle: CGFloat
var collapse: CGFloat
var tableView: UITableView?
init(height: CGFloat, top: CGFloat, topThreshold: CGFloat, middle: CGFloat, collapse: CGFloat) {
self.height = height
self.which = "?"
self.tableOffset = CGPoint(x: 0, y: 0)
self.initialOffset = CGPoint(x: 0, y: 0)
self.top = top
self.topThreshold = topThreshold
self.middle = middle
self.collapse = collapse
}
@objc func panGesture(_ gesture: UIPanGestureRecognizer) {
guard let tableView = tableView else { return }
tableOffset = tableView.contentOffset
let translation = gesture.translation(in: tableView)
let velocity = gesture.velocity(in: tableView)
let isPanningDown = velocity.y > 0 && tableOffset.y <= 1
let isPanningUp = velocity.y < 0 && height < top
let isPanning = (isPanningDown || isPanningUp) && tableOffset.y < 100
if isPanning {
tableView.setContentOffset(.zero, animated: false)
}
switch gesture.state {
case .began:
initialOffset = tableView.contentOffset
case .changed:
guard isPanning else { break }
height += -translation.y + initialOffset.y
height = min(top, max(0, height))
gesture.setTranslation(initialOffset, in: tableView)
case .ended:
withAnimation {
height = (height < middle ? collapse : (height < topThreshold ? middle : top))
}
default:
break
}
}
}
struct FloatingPanel<T: View>: View {
@ObservedObject var viewModel: FloatingPanelViewModel
var top: CGFloat { viewModel.top }
var topThreshold: CGFloat { viewModel.topThreshold }
var middle: CGFloat { viewModel.middle }
var collapse: CGFloat { viewModel.collapse }
var Content: T
init(height: CGFloat, top: CGFloat, topThreshold: CGFloat, middle: CGFloat, collapse: CGFloat, @ViewBuilder content: () -> T) {
self.viewModel = FloatingPanelViewModel(height: height, top: top, topThreshold: topThreshold, middle: middle, collapse: collapse)
self.Content = content()
}
var body: some View {
let viewGesture = DragGesture()
.onChanged { gesture in
viewModel.tableView?.setContentOffset(.zero, animated: true)
viewModel.height += -gesture.translation.height
viewModel.height = min(top, max(0, viewModel.height))
viewModel.which = "View gesture"
}
.onEnded { _ in
withAnimation {
viewModel.height = (viewModel.height < middle ? collapse : (viewModel.height < topThreshold ? middle : top))
}
}
// let tableGesture = DragGesture()
// .onChanged { gesture in
// // Swipe
// if viewModel.height != top || (viewModel.tableView?.contentOffset.y == 0 && gesture.translation.height > 0) {
// viewModel.height += -gesture.translation.height
// viewModel.height = min(top, max(0, viewModel.height))
// }
// // Scroll
// else if viewModel.isScrollEnabled == false {
// viewModel.tableOffset.y = max(0, -gesture.translation.height)
// }
//
// viewModel.which = "Table gesture"
// }
// .onEnded { _ in
// withAnimation {
// viewModel.height = (viewModel.height < middle ? collapse : (viewModel.height < topThreshold ? middle : top))
// }
// viewModel.isScrollEnabled = viewModel.height == top
// }
return VStack(spacing: 0) {
HStack {
Spacer()
RoundedRectangle(cornerRadius: 100)
.frame(width: 45, height: 5)
Spacer()
}
.frame(height: 13)
.gesture(viewGesture)
Text("Height: " + String(Float(viewModel.height)))
Text("TableOffset: " + String(Float(viewModel.tableOffset.y)))
Text(viewModel.which)
List {
Content
}
.introspectTableView { tableView in
viewModel.tableView = tableView
// tableView.isScrollEnabled = (viewModel.height >= top - 5 )
// tableView.setContentOffset(viewModel.tableOffset, animated: false)
tableView.bounces = false
tableView.panGestureRecognizer.addTarget(viewModel, action: #selector(FloatingPanelViewModel.panGesture))
}
// .gesture(tableGesture)
}
.background(Color.white)
.frame(height: viewModel.height)
}
}
struct FloatingPanel_Previews: PreviewProvider {
static var previews: some View {
GeometryReader { geometry in
ZStack(alignment: .bottom) {
Color.green
FloatingPanel(height: 302,
top: geometry.size.height,
topThreshold: geometry.size.height - 200,
middle: 302,
collapse: 0) {
ForEach(0..<30) { _ in
Color(white: Double.random(in: 0..<1))
.frame(height: 100)
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment