youareawaitress
youareawaitress

Reputation: 407

How do I use 2 gesture recognizers to receive events simultaneously?

I'm using swipe gestures to navigate between tabs using a fake UITabBarController that hosts 4 UIViewController subclasses inside of a UIScrollView, with UIPanGestureRecognizers on each of them to detect swipes and switch tabs accordingly. The resulting effect is core navigation like what you see on Snapchat. However, one of these screens houses a map view (Mapbox map), which has its own gesture recognizers built-in.

I'd like for the user to be able to swipe on the map itself to navigate to another tab by detecting a swipe from the very right edge of the screen. I've tried adding a UIPanGestureRecognizer to an invisible UIView in this area on the right side, but this provides for a jerky, unanimated tab switch, and it intercepts touch events from the map below.

How can I allow for both gesture recognizers to receive events simultaneously, or filter out some events based upon only the start position of the swipe and its distance and direction?

Code for swiping view controller:

class KUSwipeViewController: EZSwipeController {

    fileprivate var fakeTabBar: UIView!

    fileprivate var tabBarHeight: CGFloat = 50

    override func viewDidLoad() {
        initUI()
    }

    override func setupView() {
        super.setupView()
        datasource = self
        navigationBarShouldNotExist = true
    }

    func initUI() {
        fakeTabBar = UIView(frame: CGRect(x: 0, y: Screen.height - tabBarHeight, width: Screen.width, height: tabBarHeight))
        fakeTabBar.backgroundColor = .black

        let tab0 = UIButton(frame: CGRect(x: Screen.width * 0.08, y: 9, width: 32, height: 32))
        tab0.setBackgroundImage(#imageLiteral(resourceName: "tab-events"), for: .normal)
        tab0.addTarget(self, action: #selector(tapped0), for: .touchUpInside)
        fakeTabBar.addSubview(tab0)

        let tab1 = UIButton(frame: CGRect(x: Screen.width * 0.32, y: 9, width: 32, height: 32))
        tab1.setBackgroundImage(#imageLiteral(resourceName: "tab-feat"), for: .normal)
        tab1.addTarget(self, action: #selector(tapped1), for: .touchUpInside)
        fakeTabBar.addSubview(tab1)

        let tab2 = UIButton(frame: CGRect(x: Screen.width * 0.58, y: 9, width: 32, height: 32))
        tab2.setBackgroundImage(#imageLiteral(resourceName: "tab-chat"), for: .normal)
        tab2.addTarget(self, action: #selector(tapped2), for: .touchUpInside)
        fakeTabBar.addSubview(tab2)

        let tab3 = UIButton(frame: CGRect(x: Screen.width * 0.82, y: 9, width: 32, height: 32))
        tab3.setBackgroundImage(#imageLiteral(resourceName: "tab-profile"), for: .normal)
        tab3.addTarget(self, action: #selector(tapped3), for: .touchUpInside)
        fakeTabBar.addSubview(tab3)

        view.addSubview(fakeTabBar)
    }


    func tapped0() {
        self.moveToPage(0, animated: true)
    }

    func tapped1() {
        self.moveToPage(1, animated: true)
    }

    func tapped2() {
        self.moveToPage(2, animated: true)
    }

    func tapped3() {
        self.moveToPage(3, animated: true)
    }
}

extension KUSwipeViewController: EZSwipeControllerDataSource {
    func viewControllerData() -> [UIViewController] {

        let nav0 = UINavigationController()
        let nav3 = UINavigationController()

        let mapVC = EventMapViewController()
        let featuredVC = FeaturedEventsViewController()
        let chatVC = MessagesViewController()
        let profileVC = ProfileViewController()

        nav0.viewControllers = [mapVC]
        nav3.viewControllers = [profileVC]

        return [nav0, featuredVC, chatVC, nav3]
    }

    func titlesForPages() -> [String] {
        return ["", "", "", ""]
    }

    func indexOfStartingPage() -> Int {
        return 0
    }

    func changedToPageIndex(_ index: Int) {
        Haptic.selection.generate()
    }
}

Code for map view controller:

class EventMapViewController: CommonViewController {

    fileprivate var mapView: MGLMapView!
    fileprivate var statusBarView: UIView!
    fileprivate var searchBar: FloatingSearchBar!
    fileprivate var searchButton: UIButton!
    fileprivate var filterButton: UIButton!
    fileprivate var peekView: UIView!
    fileprivate var architectView: UIView!
    fileprivate var panArchitectView: UIView!
    fileprivate var peekArchitectView: UIView!
    fileprivate var peekLabel: UILabel!
    fileprivate var panView: UIPanGestureRecognizer!

    fileprivate let peekViewHeight: CGFloat = 140

    fileprivate var selectedEvent: Event?

    fileprivate var cardShowing: Bool = false
    fileprivate var peekShowing: Bool = false

    /*
    override func statusBarStyle() -> UIStatusBarStyle {
        return cardShowing ? .lightContent : .default
    }
    */

    override func navigationBarHidden() -> Bool {
        return true
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        initUI()
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "eventSegue" {
            let dvc = segue.destination as! EventViewController
            dvc.event = selectedEvent!
        }
    }

    func initUI() {
        mapView = MGLMapView(frame: view.bounds)
        mapView.styleURL = (UIColor.theme == .light) ? URL(string: mbThemeLight)! : URL(string: mbThemeDark)!
        mapView.showsUserLocation = true
        mapView.userTrackingMode = .none
        mapView.delegate = self
        mapView.setCenter(CLLocationCoordinate2D(latitude: 34, longitude: -118), zoomLevel: 5, animated: false)
        view.addSubview(mapView)
        mapView.snp.makeConstraints { (make) in
            make.edges.equalTo(view)
        }

        //architect view (for intercepting touch)
        architectView = UIView(frame: self.view.bounds)
        let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(EventMapViewController.handleTap(gestureRecognizer:)))
        gestureRecognizer.delegate = self
        architectView.addGestureRecognizer(gestureRecognizer)
        architectView.isHidden = true
        view.addSubview(architectView)

        statusBarView = UIView()
        statusBarView.backgroundColor = UIColor.clear
        view.addSubview(statusBarView)
        statusBarView.snp.makeConstraints { (make) in
            make.left.right.top.equalTo(0)
            make.height.equalTo(20)
        }

        searchButton = UIButton()
        searchButton.backgroundColor = UIColor.kuLightBackground
        searchButton.tintColor = (UIColor.theme == .light) ? UIColor.kuPrimary : UIColor.kuDeselected
        searchButton.setImage(#imageLiteral(resourceName: "icon_search_white").withRenderingMode(.alwaysTemplate), for: .normal)
        searchButton.layer.cornerRadius = 25
        searchButton.layer.borderColor = UIColor.kuExtraLightBackground.cgColor
        searchButton.layer.borderWidth = 1
        searchButton.addTarget(self, action: #selector(searchButtonTapped(_:)), for: .touchUpInside)
        view.addSubview(searchButton)
        searchButton.snp.makeConstraints { (make) in
            make.width.height.equalTo(50)
            make.left.equalTo(24)
            make.top.equalTo(34)
        }

        filterButton = UIButton()
        filterButton.backgroundColor = UIColor.kuLightBackground
        filterButton.tintColor = (UIColor.theme == .light) ? UIColor.kuPrimary : UIColor.kuDeselected
        filterButton.setImage(#imageLiteral(resourceName: "icon_filter_white").withRenderingMode(.alwaysTemplate), for: .normal)
        filterButton.layer.cornerRadius = 20
        filterButton.layer.borderColor = UIColor.kuExtraLightBackground.cgColor
        filterButton.layer.borderWidth = 1
        filterButton.addTarget(self, action: #selector(filterButtonTapped(_:)), for: .touchUpInside)
        view.addSubview(filterButton)
        filterButton.snp.makeConstraints { (make) in
            make.width.height.equalTo(40)
            make.left.equalTo(80)
            make.top.equalTo(40)
        }

        //peek view
        peekView = UIView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height, width: UIScreen.main.bounds.width, height: peekViewHeight))
        peekView.backgroundColor = UIColor(white: 1, alpha: 0.5)
        peekView.layer.cornerRadius = 10
        view.addSubview(peekView)

        //peek label
        peekLabel = UILabel(frame: CGRect(x: 0, y: 12, width: UIScreen.main.bounds.width, height: 68))
        peekLabel.font = UIFont.kuBoldFont(ofSize: 38)
        peekLabel.textColor = .black
        peekLabel.textAlignment = .center
        peekView.addSubview(peekLabel)

        //peek architect
        peekArchitectView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: peekViewHeight))
        let peekGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(EventMapViewController.peekSelected(gestureRecognizer:)))
        peekGestureRecognizer.delegate = self
        peekArchitectView.addGestureRecognizer(peekGestureRecognizer)
        peekView.addSubview(peekArchitectView)

        loadEventAnnotations()
    }

    func enterPeekView() {
        guard !peekShowing else { return }

        architectView.isHidden = false
        peekLabel.text = "\(selectedEvent!.title!) >"

        peekView.cheetah
            .move(0, peekViewHeight * -1)
            .duration(0.18)
            .easeOutExpo
            .run()

        peekShowing = true
    }

    func exitPeekView() {
        guard peekShowing else { return }

        architectView.isHidden = true
        peekView.cheetah
            .move(0, peekViewHeight)
            .duration(0.18)
            .easeInExpo
            .run()

        peekShowing = false
    }

    func loadEventAnnotations() {
        RealmManager.shared.defaultRealm.objects(Event.self).forEach { (event) in

            let annotation = EventAnnotation()
            annotation.event = event
            mapView.addAnnotation(annotation)
        }
    }

    func searchButtonTapped(_ sender: UIButton) {
        let eventSearchCardViewController = EventSearchCardViewController()
        eventSearchCardViewController.delegate = self
        UIApplication.rootViewController()?.presentCardViewController(eventSearchCardViewController)
    }

    func filterButtonTapped(_ sender: UIButton) {
        let eventFilterCardViewController = EventFilterCardViewController()
        eventFilterCardViewController.delegate = self
        UIApplication.rootViewController()?.presentCardViewController(eventFilterCardViewController)
    }
}

extension EventMapViewController: CardViewControllerDelegate {

    func cardViewControllerWillAppear(cardViewController: CardViewController) {
        cardShowing = true
        setNeedsStatusBarAppearanceUpdate()
    }

    func cardViewControllerWillDisappear(cardViewController: CardViewController) {
        cardShowing = false
        setNeedsStatusBarAppearanceUpdate()
    }
}

extension EventMapViewController: MGLMapViewDelegate {

    func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {

        guard let eventAnnotation = annotation as? EventAnnotation else {
            return
        }

        selectedEvent = eventAnnotation.event

        enterPeekView()
    }

    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return false
    }

    func mapView(_ mapView: MGLMapView, imageFor annotation: MGLAnnotation) -> MGLAnnotationImage? {
        var annotationImage = mapView.dequeueReusableAnnotationImage(withIdentifier: UIColor.kuMarkerName)

        if annotationImage == nil {
            var image = UIImage(named: UIColor.kuMarkerName)!
            image = image.withAlignmentRectInsets(UIEdgeInsets(top: 0, left: 0, bottom: image.size.height/2, right: 0))
            annotationImage = MGLAnnotationImage(image: image, reuseIdentifier: UIColor.kuMarkerName)
        }

        return annotationImage
    }
}

extension EventMapViewController: UIGestureRecognizerDelegate {

    func handleTap(gestureRecognizer: UIGestureRecognizer) {
        //close peek and keyboard on tap
        view.endEditing(true)

        if peekShowing {
            exitPeekView()
        }
    }

    func peekSelected(gestureRecognizer: UIGestureRecognizer) {
//        performSegue(withIdentifier: "eventSegue", sender: nil)

        let eventController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "EventViewController") as! EventViewController
        self.navigationController?.pushViewController(eventController, animated: true)
    }
}

Upvotes: 0

Views: 493

Answers (1)

Aaron Halvorsen
Aaron Halvorsen

Reputation: 2680

To use multiple gestures make your class a GestureRecognizer delegate UIGestureRecoginizerDelegate then use this function:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if (gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UITapGestureRecognizer) {
            return true
        } else {
            return false
        }
    }

Upvotes: 2

Related Questions