SriTeja Chilakamarri
SriTeja Chilakamarri

Reputation: 2703

How to build a context menu like Facebook / Slack on iOS?

I was just looking at Context menu of Facebook and or slack and wanted to create something similar in my App.

I have tried two methods.

First method. Having a in View Table View and sliding it from bottom to create as if it is animated on to the view. But the problem with this is that The navigation controller and Tab bar controller are not hidden and a white patch is shown over the Black (Alpha 30 %).

enter image description here

The second method I tried was showing a new View controller over the current view controller and presenting as a Modal presentation.

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

This works okay but the method is too slow as I have to work with lot of Notifications (To send selected index to my main View and then perform action). It is painfully slow.

Could anyone help me with how I can improve the implementation so that I can get the Action sheet similar to Facebook which is smooth and very very fluid

enter image description here

Upvotes: 0

Views: 1664

Answers (3)

Aalaa
Aalaa

Reputation: 57

Using UIPresentationController and UIPanGestureRecognizer

1- create BottomMenu presentation Controller which will handle the height of your View Controller and blur

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- create Your 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- make the viewController that contain the button that will present your menu conforms to 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- set the transitioning delegate to self and present your custom presentation Controller

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

check this PanGesture Slidable View article

Upvotes: 1

okcoker
okcoker

Reputation: 1339

Since you mentioned Slack, they actually have open sourced their bottom sheet implementation, PanModal.

Upvotes: 1

Nahid Raihan
Nahid Raihan

Reputation: 1017

Check this example : Bottom pop Up Currently I am using this in my app and it's work fine.

Upvotes: 1

Related Questions