0

I created a floating action button which i would like to dismiss all the action button when a user either taps the FAB when it's open or when the user taps anywhere on the screen and FAB is open. It is also important to note that the FAB is being displayed over a tableview and i want to retain the ability to select tableview cells.

In my implementation of the FAB i added a target to the FAB button which i use to open and close the FAB and i also implemented a tapGesture on the viewController with the tableview such that when a tap gesture is invoked i can close the FAB if open.

To make this work i did a bit of research and found out that i have to set

tap.cancelsTouchesInView = false

so that the tableView events continue working. However the side effect is that when i tap on the fab to close it two events are fired one from the tapGesture of the FAB and another from the button target which results in the FAB not closing when u tap on it if its open.

Is there a more elegant way of making the FAB be able to close when tapped whilst its open and also have the tap Gesture on the viewController close the fab when its open and a user taps anywhere on the screen.

Here is some of my code:

ViewController:

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tap = UITapGestureRecognizer(target: self,
                                         action: #selector(self.dismissActionButtons(_:)))
        self.view.addGestureRecognizer(tap)
        
        tap.cancelsTouchesInView = false
        self.floatingActionButton.isHidden = true
    }

    @objc func dismissActionButtons(_ sender: UIButton) {
        if !floatingActionButton.actionButtonsCarousel.isHidden {
            self.floatingActionButton.animateActionButtonsDisappering()
        }
    }

Custom FAB View :

    
    override init(frame: CGRect) {
        super.init(frame: .zero)
        
        actionButtonsCarousel.isHidden = true
        self.translatesAutoresizingMaskIntoConstraints = false
        
        self.mainButton.addTarget(self,
                                   action: #selector(ACTFAB.fabButtonAction(_:)),
                                   for: .touchUpInside)
        
    }
    @objc func fabButtonAction(_ sender: UIButton) {
        
        if self.actionButtonsCarousel.isHidden {
            self.animateActionButtonsAppering()
        } else {
            self.animateActionButtonsDisappering()
        }
    }
    
    func animateActionButtonsAppering() {
        self.actionButtonsCarousel.alpha = 0.0
        self.actionButtonsCarousel.isHidden = false
        
        UIView.transition(with: self, duration: 0.5, options: .preferredFramesPerSecond60, animations: {
            self.actionButtonsCarousel.alpha = 1.0
        })
        self.mainButton.setImage(UIImage(named: "fab-open-icon"), for: .normal)
    }
    
    func animateActionButtonsDisappering() {
        self.actionButtonsCarousel.alpha = 1.0
        self.actionButtonsCarousel.isHidden = true
        
        UIView.transition(with: self, duration: 0.3, options: .transitionCrossDissolve, animations: {
            self.actionButtonsCarousel.alpha = 0.0
        })
        self.mainButton.setImage(UIImage(named: "fab-closed-icon"), for: .normal)
    }

Two valid scenarios:

1 FAB is open -> click FAB -> FAB closes

2 FAB is open -> click anywhere other than FAB -> FAB closes

Scenario number 1 fails with my current code.

2 Answers 2

1

If I understand your question correctly, the issue is that tapping the FAB causes both the button's action to be fired but also, as you are passing the event through to the underlying viewController, the gestureRecogniser to fire too.

I'm assuming the button action is the primary event, and that when this fires you need to stop the gestureRecogniser. A gestureRecogniser has a .location(in:) method which allows you to get the first tap location (for a tapGestureRecogniser) in terms of any view, and a UIView has a .point(inside: with:) method that checks whether a CGPoint (in terms of its own coordinate space) is inside it bounds. Therefore you should be able to do something like this (from memory and not compiled, so may need some tweaking but hopefully it should get you started):

@objc func dismissActionButtons(_ sender: UIButton) {
  let tapPoint = sender.location(in: customFABview)
  if customFABview.point(inside: tapPoint, with: nil) && 
    !floatingActionButton.actionButtonsCarousel.isHidden {
            self.floatingActionButton.animateActionButtonsDisappering()
  }
}
Sign up to request clarification or add additional context in comments.

1 Comment

You the GOAT @flanker thank you it worked however i made a small change
0

Continuing with @flanker's answer i created a boolean in the fab to check if the event was coming from tapGesture then i added it to the check conditional statement as follows:

    var isComingFromGestureEvent: Bool = false

    @objc func fabButtonAction(_ sender: UIButton) {

        if self.actionButtonsCarousel.isHidden && !isComingFromGestureEvent {
            self.animateActionButtonsAppering()
        } else {
            self.animateActionButtonsDisappering()
        }
    }

In the ViewController i then just used flanker's answer to set the boolean state as follows :

    var locationInView: CGPoint = CGPoint(x: 0, y: 0)

    override func viewDidLoad() {
        super.viewDidLoad()

        let tap = UITapGestureRecognizer(target: self,
                                         action: #selector(self.dismissActionButtons(_:)))
        self.view.addGestureRecognizer(tap)

        pointInView = tap.location(in: floatingActionButton)

        tap.cancelsTouchesInView = false
        self.floatingActionButton.isHidden = true
    }

    @objc func dismissActionButtons(_ sender: UIButton) {

        let tapPoint = pointInView
        if floatingActionButton.point(inside: tapPoint, with: nil) &&
          !floatingActionButton.actionButtonsCarousel.isHidden {

            self.floatingActionButton.isComingFromGestureEvent = true
            self.floatingActionButton.animateActionButtonsDisappering()

        } else {

            self.floatingActionButton.isComingFromGestureEvent = false

        }
    }

1 Comment

Does that work, because I don't think it should?! You only get the location from the tap event when it happens, not from the class, so calculating it in viewDidLoad before the tap event would be meaningless as I'm pretty sure it will just return a default location of (0,0) which will always be true.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.