Last active
April 8, 2021 11:54
-
-
Save jaecheoljung/0c3e23db87c5f8ffb40efe55081826a7 to your computer and use it in GitHub Desktop.
(Floating Panel || Bottom Sheet || Floating View) in SwiftUI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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