请稍侯

实现一个自定义的 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()
        }
    }
}