请稍侯

一个基于viewcontroll

12 March 2024

一个基于ViewController addChild方式实现的通用 EmbedPanelView

import Foundation
import UIKit

class EmbedPanelView: UIView {

    private var defaultHeight = UIScreen.main.bounds.height * 0.618
    //private let maxHeight = UIScreen.main.bounds.height * (1.0 - pow((1.0 - 0.618), 2))
    private let maxHeight = UIScreen.main.bounds.height - 54.0

    var getParentViewController: (() -> LiveRoomBaseViewController?)?
    var getChildViewController: (() -> UIViewController?)?

    var expandHandler: ((_ isExpand: Bool) -> Void)?

    private var subViewsConfiged: Bool = false

    private weak var childViewController: UIViewController?

    private lazy var backgroundControl: UIControl = {
        let control = UIControl(frame: .zero)
        control.backgroundColor = .clear
        control.addTarget(self, action: #selector(dismissEmbedVC), for: .touchUpInside)
        return control
    } ()

    private lazy var containerView: UIView = {
        let view = UIView(frame: .zero)
        view.backgroundColor = .white
        view.layer.cornerRadius = 16.pt
        view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        view.clipsToBounds = true
        return view
    } ()

    private lazy var headerView: UIView = {
        let view = UIView(frame: .zero)
        view.clipsToBounds = true
        return view
    }()

    private lazy var contentView: UIView = {
        let view = UIView(frame: .zero)
        view.backgroundColor = .clear
        view.clipsToBounds = true
        return view
    }()

    //MARK: default header subviews
    private lazy var titleLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.backgroundColor = .white
        label.textColor = UIColor.darkText
        label.font = UIFont.systemFont(ofSize: 16.0, weight: .bold)
        label.numberOfLines = 1
        label.lineBreakMode = .byTruncatingTail
        label.textAlignment = .left
        label.text = "라이브 안내사항".localized
        return label
    } ()

    private lazy var closeButton: UIButton = {
        let button = UIButton(type: .custom)
        button.backgroundColor = .white
        button.setImage(UIImage.live.imageAsset(named: "iconClose2Outline20"), for: .normal)
        button.addTarget(self, action: #selector(dismissEmbedVC), for: .touchUpInside)
        return button
    } ()

    private lazy var upperDivider: RDSDivider = {
        let divider = RDSDivider(type: .horizontal, size: .small)
        divider.tintColor = UIColor.clear
        divider.backgroundColor = UIColor.clear
        return divider
    }()

    //MARK: - method
    //required init?(coder aDecoder: NSCoder) {
    //    super.init(coder: aDecoder)
    //}

    init(enableExpand: Bool = false) {
        super.init(frame: .zero)
        if enableExpand {
            containerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
        }
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    private func setupSubviews(customHeaderHandler:((_ headerView: UIView) -> Void)? = nil){
        guard !self.subViewsConfiged else { return }
        self.subViewsConfiged = true

        self.configBasePanelView()

        if let headerHandler = customHeaderHandler {
            headerHandler(self.headerView)

        }else { //default
            self.headerView.addSubview(self.titleLabel)
            self.headerView.addSubview(self.closeButton)
            self.headerView.addSubview(self.upperDivider)

            self.titleLabel.snp.makeConstraints { (make) in
                make.leading.equalToSuperview().offset(16.pt)
                make.top.equalToSuperview().offset(16.pt)
            }
            self.closeButton.snp.makeConstraints { (make) in
                make.size.equalTo(20.pt)
                make.trailing.equalToSuperview().offset(-16.pt)
                make.centerY.equalTo(self.titleLabel)
            }
            self.upperDivider.snp.makeConstraints { (make) in
                make.leading.trailing.equalToSuperview()
                make.top.equalToSuperview().offset((44-1).pt)
            }
        }
    }

    private func configBasePanelView() {
        self.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        self.addSubview(self.backgroundControl)
        self.addSubview(self.containerView)
        self.containerView.addSubview(self.headerView)
        self.containerView.addSubview(self.contentView)

        self.backgroundControl.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }
        self.containerView.snp.makeConstraints { (make) in
            make.leading.trailing.equalToSuperview()
            make.top.bottom.equalTo(self.snp.bottom)
        }
        self.headerView.snp.makeConstraints { (make) in
            make.top.leading.trailing.equalToSuperview()
            make.height.equalTo(44.pt)
        }
        self.contentView.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
            make.top.equalTo(self.headerView.snp.bottom).offset(1.0)
        }
    }


    func showEmbedVC(customHeaderHandler:((_ headerView: UIView) -> Void)? = nil) {
        guard let parentVC = self.getParentViewController?(),
              let childVC = self.getChildViewController?() else { return }

        self.childViewController = childVC

        //add panelView (self)
        parentVC.view.addSubview(self)
        self.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }

        self.setupSubviews(customHeaderHandler: customHeaderHandler)

        //add child viewController
        parentVC.addChild(childVC)
        childVC.willMove(toParent: parentVC)
        self.contentView.addSubview(childVC.view)
        childVC.view.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        childVC.didMove(toParent: parentVC)

        parentVC.view.layoutIfNeeded()

        UIView.animate(withDuration: 0.25) { [weak self] in
            guard let self = self else { return }
            self.containerView.snp.remakeConstraints { (make) in
                make.leading.trailing.bottom.equalToSuperview()
                make.height.equalTo(self.defaultHeight)
            }
            self.layoutIfNeeded()
        } completion: { finished in
            if finished { /* do nothing */ }
        }
        self.getParentViewController?()?.roomService.autoShowBottomSheetInterrupt.popupInterrupt(for: self)
    }
    
    @objc func dismissEmbedVC() {
        UIView.animate(withDuration: 0.25) { [weak self] in
            guard let self = self else { return }
            self.containerView.snp.remakeConstraints { (make) in
                make.leading.trailing.equalToSuperview()
                make.top.bottom.equalTo(self.snp.bottom)
            }
            self.layoutIfNeeded()
        } completion: { [weak self] _ in
            guard let self = self else { return }
            if let childVC = self.childViewController,
                childVC.parent != nil {
                childVC.willMove(toParent: nil)
                childVC.view.removeFromSuperview()
                childVC.removeFromParent()
                childVC.didMove(toParent: nil)
            }
            self.getParentViewController?()?.roomService.autoShowBottomSheetInterrupt.popupInterruptCancel(for: self)
            self.removeFromSuperview()
        }
    }

    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
        }
    }

    func showDefaultHeight(animated: Bool) {
        containerView.snp.updateConstraints { make in
            make.height.equalTo(defaultHeight)
        }

        if animated {
            UIView.animate(withDuration: 0.3) {
                self.layoutIfNeeded()
            } completion: { [weak self] _ in
                self?.expandHandler?(false)
            }
        } else {
            layoutIfNeeded()
            self.expandHandler?(false)
        }
    }

    func showMaxHeight(animated: Bool, completion: ((_ isDefaultHeight: Bool) -> Void)? = nil) {
        containerView.snp.updateConstraints { make in
            make.height.equalTo(maxHeight)
        }

        if animated {
            UIView.animate(withDuration: 0.3) {[weak self] in
                self?.layoutIfNeeded()
            } completion: { [weak self] _ in
                self?.expandHandler?(true)
            }
        } else {
            layoutIfNeeded()
            self.expandHandler?(true)
        }
    }
}