swiftuiscrollviewuitouchhittestuiresponder

How to pass a responder to ScrollView?


My view hierarchy

This is the View layer I created. I want to transfer all Touch Events that happen in UIView to UIScrollView, but I don't know how. I didn't put it as a SubView of ScrollView because UIView should be lock. I want to use the default Gesture in UIScrollView for up or down drag (PanGesture) that happens in UIView. I made a PanGesture in the ViewController using ResponderChain to respond, but when it is Touch State.Ended like ScrollView I couldn't implement a scroll that ends smoothly. Any way to solve this problem? Thank you for your reply.


Solution

  • One approach that may work for you...

    Use a UIView subclass for your "stationary pink view" and implement hitTest:

    class MyParentView: UIView {
        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            let hitView = super.hitTest(point, with: event)
            if hitView == self {
                return nil
            } else {
                return hitView
            }
        }
    }
    

    that will allow the touch/drag to effectively "pass through" to the scroll view underneath, while still allowing user interaction with its own subviews.

    Here's an example controller showing it in use:

    class ViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = UIColor(red: 0.61, green: 0.86, blue: 0.93, alpha: 1.0)
            
            let scrollView = UIScrollView()
            scrollView.backgroundColor = UIColor(red: 0.96, green: 0.93, blue: 0.79, alpha: 1.0)
            
            let pinkView = MyParentView()
            pinkView.backgroundColor = UIColor(red: 0.96, green: 0.70, blue: 0.86, alpha: 1.0)
        
            // a button and label to add to the pink view
            let btn = UIButton()
            btn.setTitle("Some Button", for: [])
            btn.setTitleColor(.white, for: .normal)
            btn.setTitleColor(.lightGray, for: .highlighted)
            btn.backgroundColor = .systemRed
            btn.layer.cornerRadius = 8
            btn.translatesAutoresizingMaskIntoConstraints = false
    
            let label = UILabel()
            label.text = "Some Label"
    
            [btn, label].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                pinkView.addSubview(v)
            }
    
            // a vertical stack view with a bunch of labels
            //  to use as the scroll content
            let stack = UIStackView()
            stack.axis = .vertical
            stack.spacing = 40.0
            for i in 1...30 {
                let l = UILabel()
                l.text = "Label \(i)"
                stack.addArrangedSubview(l)
            }
    
            // add stack view to scroll view
            stack.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(stack)
            
            // add scroll view to view
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(scrollView)
    
            // add pink view ot view
            pinkView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(pinkView)
    
            let g = view.safeAreaLayoutGuide
            let cg = scrollView.contentLayoutGuide
            let fg = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain scroll view to all 4 sides with 20-points "padding"
                scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
    
                // constrain stack view to all 4 sides of scroll view's Content Layout Guide
                //  with 20-points "padding"
                stack.topAnchor.constraint(equalTo: cg.topAnchor, constant: 20.0),
                stack.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20.0),
                stack.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20.0),
                stack.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -20.0),
                // stack view width is scroll view's Content Layout Guide width
                //  minus 40-points (20-points on each side)
                stack.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -40.0),
                
                // constrain pink view relative to scroll view
                //  10-points from top
                //  100-points leading (so we can see the scrolling)
                //  trailing
                //  height of 320-points
                pinkView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 10.0),
                pinkView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 100.0),
                pinkView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0),
                pinkView.heightAnchor.constraint(equalToConstant: 320.0),
    
                // constrain button and label inside pink view
                btn.topAnchor.constraint(equalTo: pinkView.topAnchor, constant: 40.0),
                btn.leadingAnchor.constraint(equalTo: pinkView.leadingAnchor, constant: 20.0),
                btn.trailingAnchor.constraint(equalTo: pinkView.trailingAnchor, constant: -20.0),
                
                label.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
                label.leadingAnchor.constraint(equalTo: pinkView.leadingAnchor, constant: 20.0),
                label.trailingAnchor.constraint(equalTo: pinkView.trailingAnchor, constant: -20.0),
                
            ])
            
            // add an action for the button so we know it can be tapped
            btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
    
            // a little "styling"
            scrollView.layer.cornerRadius = 16
            pinkView.layer.cornerRadius = 16
        }
        
        @objc func btnTapped(_ sender: Any?) {
            print("Tapped")
        }
    
    }
    

    The result - I inset the "pink view" from the leading edge of the scroll view to make it easy to see the scroll contents scrolling behind it:

    enter image description here

    Touching and dragging on the yellow OR the pink will scroll the scrollView as normal.