实现一个自定义的 bottoms
11 September 2023
实现一个自定义的 BottomSheepPopView
class BottomSheetPopView: UIView {
private var contentCornerRadius: CGFloat = 16.0
private var headerHeight = 32.0
private var defaultHeight = UIScreen.main.bounds.height - 120.0
private let maxHeight = UIScreen.main.bounds.height - 36.0
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = contentCornerRadius
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
lazy var headerBar: UIView = {
let view = UIView()
return view
}()
lazy var contentView: UIView = {
let view = UIView()
return view
}()
private var bottomConstraint: Constraint?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
init(enableExpand: Bool = false, blockOuterScroll: Bool = true,
defaultHeight: CGFloat = 0, headerHeight: CGFloat = 32.0, cornerRadius: CGFloat = 16.0) {
if defaultHeight > 0 {
self.defaultHeight = defaultHeight
}
self.headerHeight = headerHeight
self.contentCornerRadius = cornerRadius
super.init(frame: .zero)
setupView()
if enableExpand {
headerBar.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
}
if blockOuterScroll {
containerView.addGestureRecognizer(UIPanGestureRecognizer())
}
}
private func setupView() {
backgroundColor = UIColor.black.withAlphaComponent(0.3)
addSubview(containerView)
containerView.addSubview(headerBar)
containerView.addSubview(contentView)
containerView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.height.equalTo(defaultHeight).constraint
bottomConstraint = make.bottom.equalToSuperview().offset(defaultHeight).constraint
}
headerBar.snp.makeConstraints { make in
make.leading.trailing.top.equalToSuperview()
make.height.equalTo(headerHeight)
}
contentView.snp.makeConstraints { make in
make.top.equalTo(headerBar.snp.bottom)
make.leading.trailing.equalToSuperview()
if UIDevice.current.hasNotch() { //notch top
make.bottom.equalTo(safeAreaLayoutGuide.snp.bottomMargin)
}else {
make.bottom.equalToSuperview().offset(-8)
}
}
}
private var startPointY: CGFloat = 0.0
private var startHeight: CGFloat = 0.0
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: self)
let velocity = gestureRecognizer.velocity(in: self)
switch gestureRecognizer.state {
case .began:
startPointY = translation.y
startHeight = containerView.frame.height
case .changed:
var newHeight = startHeight - translation.y
newHeight = max(defaultHeight, newHeight)
newHeight = min(maxHeight, newHeight)
containerView.snp.updateConstraints { make in
make.height.equalTo(newHeight)
}
case .ended, .cancelled, .failed:
let finalHeight = containerView.frame.height
if finalHeight > maxHeight - 200 && velocity.y < 0 {
showMaxHeight(animated: true)
} else if finalHeight < defaultHeight + 200 && velocity.y > 0 {
showDefaultHeight(animated: true)
}
default:
break
}
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let location = touches.first?.location(in: self),
!containerView.frame.contains(location) || location.y > containerView.frame.maxY {
dismiss()
}
}
private func showDefaultHeight(animated: Bool) {
containerView.snp.updateConstraints { make in
make.height.equalTo(defaultHeight)
}
if animated {
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
} else {
layoutIfNeeded()
}
}
private func showMaxHeight(animated: Bool) {
containerView.snp.updateConstraints { make in
make.height.equalTo(maxHeight)
}
if animated {
UIView.animate(withDuration: 0.3) {[weak self] in
self?.layoutIfNeeded()
}
} else {
layoutIfNeeded()
}
}
func present(in view: UIView, withDuration duration: TimeInterval = 0.3, contentHandler:((_ sheetPop: BottomSheetPopView) -> Void)? = nil) {
view.addSubview(self)
self.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentHandler?(self)
self.layoutIfNeeded()
bottomConstraint?.update(offset: 0)
UIView.animate(withDuration: duration) {
self.layoutIfNeeded()
self.containerView.transform = .identity
}
}
func dismiss(withDuration duration: TimeInterval = 0.3) {
UIView.animate(withDuration: duration, animations: { [weak self] in
guard let self = self else { return }
self.containerView.transform = CGAffineTransform(translationX: 0, y: self.containerView.frame.height)
}) { [weak self] _ in
guard let self = self else { return }
self.removeFromSuperview()
}
}
}
