如何在iOS上建立像Facebook / Slack这样的上下文菜单?

时间:2018-11-19 10:32:09

标签: ios swift swift3 contextmenu uiactionsheet

我只是看着Facebook和/或松弛的上下文菜单,想在我的应用中创建类似的内容。

我尝试了两种方法。

第一种方法。拥有一个in View Table视图,并从底部滑动它以创建动画,就像在视图上进行动画处理一样。但这是因为导航控制器和Tab栏控制器没有隐藏,并且在黑色(Alpha 30%)上方显示了白色补丁。

enter image description here

我尝试的第二种方法是在当前视图控制器上显示一个新的View控制器,并显示为Modal presentation

  let vc = CustomActionTableViewController(nibName: "CustomActionTableViewController", bundle: nil)
    vc.modalPresentationStyle = .overFullScreen
    self.present(vc, animated: false, completion: nil)

这可以,但是该方法太慢,因为我必须处理大量通知(将选定的索引发送到主视图然后执行操作)。太慢了。

任何人都可以帮助我改善实施方式,以便获得类似于Facebook的操作表,该操作表非常流畅且非常流畅

enter image description here

3 个答案:

答案 0 :(得分:1)

检查此示例:Bottom pop Up 目前,我正在我的应用程序中使用它,并且可以正常工作。

答案 1 :(得分:1)

自从您提到Slack以来,他们实际上已经公开了其底层工作表PanModal的源代码。

答案 2 :(得分:1)

使用 UIPresentationController 和 UIPanGestureRecognizer

1- 创建 BottomMenu 演示控制器,它将处理您的视图控制器的高度和模糊

class BottomMenuPresentationController: UIPresentationController {

    // MARK: - Properties
        var blurEffectView: UIVisualEffectView?
        var tapGestureRecognizer = UITapGestureRecognizer()

    private var topHeightRatio: Float
    private var bottomHeightRatio: Float

     init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, topHeightRatio: Float, bottomHeightRatio: Float) {
            let blurEffect = UIBlurEffect(style: .systemThickMaterialDark)
            blurEffectView = UIVisualEffectView(effect: blurEffect)

            self.topHeightRatio = topHeightRatio
            self.bottomHeightRatio = bottomHeightRatio
            super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
            blurEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
            self.blurEffectView?.isUserInteractionEnabled = true
            self.blurEffectView?.addGestureRecognizer(tapGestureRecognizer)
        }

        override var frameOfPresentedViewInContainerView: CGRect {
            CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * CGFloat(topHeightRatio)),
                   size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height * CGFloat(bottomHeightRatio)))
        }

        override func presentationTransitionWillBegin() {
            self.blurEffectView?.alpha = 0
            if let blurEffectView = blurEffectView {
            self.containerView?.addSubview(blurEffectView)
            }
            self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (_) in
                self.blurEffectView?.alpha = 0.66
            }, completion: { (_) in })
        }

        override func dismissalTransitionWillBegin() {
            self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (_) in
                    self.blurEffectView?.alpha = 0
                }, completion: { (_) in
                    self.blurEffectView?.removeFromSuperview()
                })
        }

        override func containerViewWillLayoutSubviews() {
            super.containerViewWillLayoutSubviews()
            presentedView!.roundCorners([.topLeft, .topRight], radius: 14)

        }

        override func containerViewDidLayoutSubviews() {
            super.containerViewDidLayoutSubviews()
            presentedView?.frame = frameOfPresentedViewInContainerView
            blurEffectView?.frame = containerView!.bounds
        }

        @objc func dismissController() {
            self.presentedViewController.dismiss(animated: true, completion: nil)
        }
    }

2- 创建您的 ViewController

class BottomMenuVC: UIViewController {

    // MARK: - Instances
    var hasSetPointOrigin = false
    var pointOrigin: CGPoint?

    // MARK: - Properties
    let topDarkLine: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor(hexString: "#E1E1E1")
        view.layer.cornerRadius = 2
        return view
    }()
    let cancelButn: UIButton = {
        let button = UIButton(type: .custom)
        button.setAttributedTitle(NSAttributedString(string: "Cancel", attributes: [NSAttributedString.Key.font: UIFont.LatoMedium(size: 17),
                                                                                    NSAttributedString.Key.foregroundColor: UIColor(hexString: "#515151")
        ]), for: .normal)
        button.backgroundColor = UIColor(hexString: "#F1F3F4")
        button.layer.cornerRadius = 5.0
        button.addTarget(self, action: #selector(cancelButnPressed), for: .touchUpInside)
        return button
    }()

    // MARK: - viewLifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.isUserInteractionEnabled = true
        setupMenuView()
    }


    override func viewDidLayoutSubviews() {
        if !hasSetPointOrigin {
            hasSetPointOrigin = true
            pointOrigin = self.view.frame.origin
        }
    }

    // MARK: - SetupView

    func setupMenuView() {
        self.view.addSubview(topDarkLine)
        self.view.addSubview(cancelButn)

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction(_:)))

        view.addGestureRecognizer(panGesture)

        topDarkLine.constrainHeight(constant: 4)
        topDarkLine.constrainWidth(constant: view.frame.size.width * 0.10)
        topDarkLine.centerXInSuperview()
        topDarkLine.anchor(top: view.topAnchor, leading: nil, bottom: nil, trailing: nil, padding: .init(top: 8, left: 0, bottom: 0, right: 0))


        cancelButn.anchor(top:view.topAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor,
                          padding: .init(top: 16, left: 16, bottom: 0, right: 16))
         cancelButn.constrainHeight(constant: 44)

    }

    // MARK: - Actions

    @objc func panGestureRecognizerAction(_ sender: UIPanGestureRecognizer) {

        let translation = sender.translation(in: view)

        // Not allowing the user to drag the view upward
        guard translation.y >= 0 else { return }

        // setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down in the y-axis
        view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)

        if sender.state == .ended {
            let dragVelocity = sender.velocity(in: view)
            if dragVelocity.y >= 1300 {
                // Velocity fast enough to dismiss the uiview
                self.dismiss(animated: true, completion: nil)
            } else {
                // If the dragging isn’t too fast, resetting the view back to it’s original point
                UIView.animate(withDuration: 0.3) {
                    self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
                }
            }
        }

    }

    @objc func cancelButnPressed() {

        dismiss(animated: true, completion: nil)
    }

}

3- 使包含将显示您的菜单的按钮的 viewController 符合 UIViewControllerTransitioningDelegate

extension viewController: UIViewControllerTransitioningDelegate {

    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {

BottomMenuPresentationController(presentedViewController: presented, presenting: presenting, topHeightRatio: 0.6, bottomHeightRatio: 0.4)

    }

}

4- 将过渡委托设置为 self 并展示您的自定义演示控制器

func showBottomMenu() {
    let menu = BottomMenuVC()
    menu.coordinator = self
    menu.modalPresentationStyle = .custom
    menu.transitioningDelegate = self        
    present(menu, animated: true, completion: nil)
}

查看这篇 PanGesture Slidable View 文章