可拖拽 Bottom Sheet View Controller
Posted 北冥鱼_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了可拖拽 Bottom Sheet View Controller相关的知识,希望对你有一定的参考价值。
当我们想弹出一个预览视图,bottom sheet modal view controller 非常实用。在 ios 中,长按拖拽手势可以让 controller 上滑或者向下消失。
实现原理是,通过监听拖拽事件,动态改变 view 之间的 auto layout 约束,并加上少许动画。
下面看源码:
第一个页面 ViewController.swift:
import UIKit
class ViewController: UIViewController
// Defined UI views
lazy var titleLabel: UILabel =
let label = UILabel()
label.text = "百字令·偶忆"
label.font = .boldSystemFont(ofSize: 32)
return label
()
lazy var textView: UITextView =
let textView = UITextView(frame: .zero)
textView.font = UIFont.systemFont(ofSize: 22)
textView.isEditable = false
textView.text = "横街南巷,记钿车小小,翠帘徐揭。绿酒分曹人散后,心事低徊潜说。莲子湖头,枇杷花下,绾就同心结。明珠未斛,朔风千里催别。\\n同是沦落天涯,青青柳色,争忍先攀折。红浪香温围夜玉,堕我怀中明月。暮雨空归,秋河不动,虬箭丁丁咽。十年一梦,鬓丝今已如雪。 "
return textView
()
lazy var registerButton: UIButton =
let button = UIButton()
button.setTitle("Get Started", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = view.tintColor
button.layer.cornerRadius = 8
button.clipsToBounds = true
return button
()
lazy var containerStackView: UIStackView =
let spacer = UIView()
let stackView = UIStackView(arrangedSubviews: [titleLabel, textView, spacer, registerButton])
stackView.axis = .vertical
stackView.spacing = 16.0
return stackView
()
override func viewDidLoad()
super.viewDidLoad()
setupView()
setupConstraints()
// 3. Add action
registerButton.addTarget(self, action: #selector(presentModalController), for: .touchUpInside)
func setupView()
// cosmetics
view.backgroundColor = .systemBackground
// Add subviews and set constraints
func setupConstraints()
view.addSubview(containerStackView)
containerStackView.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.safeAreaLayoutGuide
// Call .activate method to enable the defined constraints
NSLayoutConstraint.activate([
// Set containerStackView edges to superview with 24 spacing
containerStackView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 24),
containerStackView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -24),
containerStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 24),
containerStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -24),
// Set button height
registerButton.heightAnchor.constraint(equalToConstant: 50)
])
// To be updated
@objc func presentModalController()
let vc = CustomModalViewController()
vc.modalPresentationStyle = .overCurrentContext
// Keep animated value as false
// Custom Modal presentation animation will be handled in VC itself
self.present(vc, animated: false)
第二个页面 CustomModalViewController.swift:
import UIKit
class CustomModalViewController: UIViewController
// define lazy views
lazy var titleLabel: UILabel =
let label = UILabel()
label.text = "Get Started"
label.font = .boldSystemFont(ofSize: 20)
return label
()
lazy var notesLabel: UILabel =
let label = UILabel()
label.text = "横街南巷,记钿车小小,翠帘徐揭。绿酒分曹人散后,心事低徊潜说。莲子湖头,枇杷花下,绾就同心结。明珠未斛,朔风千里催别。\\n同是沦落天涯,青青柳色,争忍先攀折。红浪香温围夜玉,堕我怀中明月。暮雨空归,秋河不动,虬箭丁丁咽。十年一梦,鬓丝今已如雪。 "
label.font = .systemFont(ofSize: 16)
label.textColor = .darkGray
label.numberOfLines = 0
return label
()
lazy var contentStackView: UIStackView =
let spacer = UIView()
let stackView = UIStackView(arrangedSubviews: [titleLabel, notesLabel, spacer])
stackView.axis = .vertical
stackView.spacing = 12.0
return stackView
()
lazy var containerView: UIView =
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 16
view.clipsToBounds = true
return view
()
let maxDimmedAlpha: CGFloat = 0.6
lazy var dimmedView: UIView =
let view = UIView()
view.backgroundColor = .black
view.alpha = maxDimmedAlpha
return view
()
let defaultHeight: CGFloat = 300
let dismissibleHeight: CGFloat = 200
let maximumContainerHeight: CGFloat = UIScreen.main.bounds.height - 64
// keep updated with new height
var currentContainerHeight: CGFloat = 300
// Dynamic container constraint
var containerViewHeightConstraint: NSLayoutConstraint?
var containerViewBottomConstraint: NSLayoutConstraint?
override func viewDidLoad()
super.viewDidLoad()
setupView()
setupConstraints()
// tap gesture on dimmed view to dismiss
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleCloseAction))
dimmedView.addGestureRecognizer(tapGesture)
setupPanGesture()
@objc func handleCloseAction()
animateDismissView()
override func viewDidAppear(_ animated: Bool)
super.viewDidAppear(animated)
animateShowDimmedView()
animatePresentContainer()
func setupView()
view.backgroundColor = .clear
func setupConstraints()
// Add subviews
view.addSubview(dimmedView)
view.addSubview(containerView)
dimmedView.translatesAutoresizingMaskIntoConstraints = false
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(contentStackView)
contentStackView.translatesAutoresizingMaskIntoConstraints = false
// Set static constraints
NSLayoutConstraint.activate([
// set dimmedView edges to superview
dimmedView.topAnchor.constraint(equalTo: view.topAnchor),
dimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
dimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
dimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// set container static constraint (trailing & leading)
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// content stackView
contentStackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 32),
contentStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20),
contentStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),
contentStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),
])
// Set dynamic constraints
// First, set container to default height
// after panning, the height can expand
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: defaultHeight)
// By setting the height to default height, the container will be hide below the bottom anchor view
// Later, will bring it up by set it to 0
// set the constant to default height to bring it down again
containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: defaultHeight)
// Activate constraints
containerViewHeightConstraint?.isActive = true
containerViewBottomConstraint?.isActive = true
@objc func handlePanGesture(gesture: UIPanGestureRecognizer)
let translation = gesture.translation(in: view)
// drag to top will be minus value and vice versa
print("Pan gesture y offset: \\(translation.y)")
// get drag direction
let isDraggingDown = translation.y > 0
print("Dragging direction: \\(isDraggingDown ? "going down" : "going up")")
// New height is based on value of dragging plus current container height
let newHeight = currentContainerHeight - translation.y
// Handle based on gesture state
switch gesture.state
case .changed:
// This state will occur when user is dragging
if newHeight < maximumContainerHeight
// Keep updating the height constraint
containerViewHeightConstraint?.constant = newHeight
// refresh layout
view.layoutIfNeeded()
case .ended:
// This happens when user stop drag,
// so we will get the last height of container
// Condition 1: If new height is below min, dismiss controller
if newHeight < dismissibleHeight
self.animateDismissView()
else if newHeight < defaultHeight
// Condition 2: If new height is below default, animate back to default
animateContainerHeight(defaultHeight)
else if newHeight < maximumContainerHeight && isDraggingDown
// Condition 3: If new height is below max and going down, set to default height
animateContainerHeight(defaultHeight)
else if newHeight > defaultHeight && !isDraggingDown
// Condition 4: If new height is below max and going up, set to max height at top
animateContainerHeight(maximumContainerHeight)
default:
break
func setupPanGesture()
// add pan gesture recognizer to the view controller's view (the whole screen)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(gesture:)))
// change to false to immediately listen on gesture movement
panGesture.delaysTouchesBegan = false
panGesture.delaysTouchesEnded = false
view.addGestureRecognizer(panGesture)
func animateContainerHeight(_ height: CGFloat)
UIView.animate(withDuration: 0.4)
// Update container height
self.containerViewHeightConstraint?.constant = height
// Call this to trigger refresh constraint
self.view.layoutIfNeeded()
// Save current height
currentContainerHeight = height
func animatePresentContainer()
// Update bottom constraint in animation block
UIView.animate(withDuration: 0.3)
self.containerViewBottomConstraint?.constant = 0
// Call this to trigger refresh constraint
self.view.layoutIfNeeded()
func animateShowDimmedView()
dimmedView.alpha = 0
UIView.animate(withDuration: 0.4)
self.dimmedView.alpha = self.maxDimmedAlpha
func animateDismissView()
// hide blur view
dimmedView.alpha = maxDimmedAlpha
UIView.animate(withDuration: 0.4)
self.dimmedView.alpha = 0
completion: _ in
// once done, dismiss without animation
self.dismiss(animated: false)
// hide main view by updating bottom constraint in animation block
UIView.animate(withDuration: 0.3)
self.containerViewBottomConstraint?.constant = self.defaultHeight
// call this to trigger refresh constraint
self.view.layoutIfNeeded()
参考:
源码下载:CustomModalVC
以上是关于可拖拽 Bottom Sheet View Controller的主要内容,如果未能解决你的问题,请参考以下文章
可拖拽 Bottom Sheet View Controller