'Interacting with the view controller behind

I know there are several posts on SE relating to this issue but I couldn't get around them to find a proper solution for my situation.

I've a map view inside a view controller. I'm presenting another view controller with modalPresentationStyle set as custom(housing the card view) on top of the map view VC:

Location Card on Map View

What I want for this scenario is that the map view to still be able to recognise touches when the location card view is presented along with the location card still recognising touches inside itself. Like how Apple Maps functions when the below list view is presented.

I know I can't pass touches through view controllers but I can't decide in between:

  • dumping the location card into the map view which I'll highly dislike,
  • presenting the location card via addChildViewController which I'm not quite sure how to implement in order to achieve the effect I want.

This SE question gives a detailed answer and a small sample project on mimicking Apple Maps' interface although I still can't figure out how to solve my issue.



Solution 1:[1]

I went with the option of dumping the location card view into the map view. BUT I didn't like it.

The thing was that I didn't want any of the logic of the location card inside the map view. But at the same time, iOS didn't provided an infrastructure and touch transfer between view controllers is basically not possible and the suggested workarounds seemed hard to implement.

First off, I migrated the location card view from a view controller to a struct.

Then, I defined a protocol named LocationCardPresentable which when extended, requires Self to be of type MapVC. Therefore, inside the protocol extension, I can work like if I'm inside MapVC but actually not. The protocol extension has various methods for presenting and dismissing the location card which I can call from inside MapVC.

Solution 2:[2]

So I managed to fix this just now by using a combination of loadView() and point(inside) to replace the rootView in the presented ViewController.

First, make a subclass of UIView which you will use as your rootView in any ViewControllers where you want touches to be passed on the ViewControllers underneath:

IgnoreTouchesView

import UIKit

class IgnoreTouchesView: UIView {

override init(frame: CGRect) {
    super.init(frame: frame)

}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for view in self.subviews {
        if view.isKind(of: UIButton) {
            let foundButtonView = view

            if foundButtonView.isUserInteractionEnabled && !foundButtonView.isHidden && foundButtonView.point(inside: self.convert(point, to: foundButtonView), with: event) {
                return true
            }
        }
    }

    return false
}

The point(inside) function checks if the user taps on any UIButtons in the OverlayVC, in that case touches should still be handled. Any other touches get passed on to the VC below. This could be done different by only ignoring the rootView but I haven't figured out quiet how to do this yet.

Then, in the VC that you want to present, add the following:

let ignoreView: IgnoreTouchesView = {
    let view = IgnoreTouchesView(frame: .zero)
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

override func loadView() {
    view = ignoreView
}

Now add the ViewControllers to your original ViewController:

lazy var overlayVC : OverlayVC = {
    let vc = SongOverlayVC()
    vc.view.translatesAutoresizingMaskIntoConstraints = false
    vc.delegate = self
    return vc
}()

viewDidLoad(){
    view.addSubview(songOverlayVC.view)
}

And that's it :)

Let me know if you run into any issues.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2