Issue #301

Make it more composable using UIViewController subclass and ThroughView to pass hit events to underlying views.

class PanViewController: UIViewController {
    var animator = UIViewPropertyAnimator(duration: 0, curve: .easeOut)
    lazy var panGR = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_: )))
    let slideView = UIView()
    let gripView = UIView()
    var options: Options = Options()
    var didHide: (() -> Void)?
    let pullDownVelocity: CGFloat = 70

    class Options {
        var contentView: UIView = UIView()
        var percentHeight: CGFloat = 0.24
    }

    override func loadView() {
        view = ThroughView()
        view.translatesAutoresizingMaskIntoConstraints = false
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
    }

    func setup() {
        slideView.layer.cornerRadius = 10
        slideView.clipsToBounds = true

        gripView.backgroundColor = UIColor.yellow
        gripView.layer.cornerRadius = 1

        view.addSubview(slideView)
        slideView.addSubview(gripView)
        slideView.addGestureRecognizer(panGR)

        NSLayoutConstraint.on([
            slideView.leftAnchor.constraint(equalTo: view.leftAnchor),
            slideView.rightAnchor.constraint(equalTo: view.rightAnchor),
            slideView.heightAnchor.constraint(equalTo: view.heightAnchor),
            slideView.topAnchor.constraint(equalTo: view.bottomAnchor)
        ])

        NSLayoutConstraint.on([
            gripView.centerXAnchor.constraint(equalTo: slideView.centerXAnchor),
            gripView.topAnchor.constraint(equalTo: slideView.topAnchor, constant: 16),
            gripView.widthAnchor.constraint(equalToConstant: 30),
            gripView.heightAnchor.constraint(equalToConstant: 2)
        ])
    }

    func apply(options: Options) {
        self.options.contentView.removeFromSuperview()
        slideView.insertSubview(options.contentView, at: 0)

        NSLayoutConstraint.on([
            options.contentView.leftAnchor.constraint(equalTo: slideView.leftAnchor),
            options.contentView.rightAnchor.constraint(equalTo: slideView.rightAnchor),
            options.contentView.topAnchor.constraint(equalTo: slideView.topAnchor),
            options.contentView.heightAnchor.constraint(equalTo: slideView.heightAnchor, multiplier: options.percentHeight)
        ])

        self.options = options
    }

    @objc func handlePan(_ gr: UIPanGestureRecognizer) {
        switch gr.state {
        case .began:
            break
        case .changed:
            break
        case .ended:
            let velocity = gr.velocity(in: slideView)
            if velocity.y > pullDownVelocity {
                hide()
            }
        default:
            break
        }
    }

    func show() {
        guard let parentView = view.superview else {
            return
        }

        animator = self.makeAnimator()
        animator.addAnimations {
            self.slideView.transform = CGAffineTransform(
                translationX: 0,
                y: -parentView.bounds.height * self.options.percentHeight - parentView.safeAreaInsets.bottom
            )
        }

        animator.startAnimation()
    }

    func hide() {
        animator = self.makeAnimator()
        animator.addAnimations {
            self.slideView.transform = CGAffineTransform.identity
        }

        animator.addCompletion({ _ in
            self.didHide?()
        })

        animator.startAnimation()
    }

    func makeAnimator() -> UIViewPropertyAnimator {
        return UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1.0)
    }
}

class ThroughView: UIView {
    override func didMoveToSuperview() {
        super.didMoveToSuperview()

        guard let superview = superview else {
            return
        }

        NSLayoutConstraint.on([pinEdges(view: superview)])
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard let slideView = subviews.first else {
            return false
        }

        return slideView.hitTest(convert(point, to: slideView), with: event) != nil
    }
}