Disable Scrolling of UIScrollView on part of the ScrollView.frame

Viewed 414

Hello hope everyone is safe. I have a ViewController(vc) which contains a UIScrollView(scrlView). The scrlView contains two other ViewControllers(vc1 & vc2). On the vc1 I have a button which pressed adds a subview(subViewVc1) to vc1. In order to not be shown on the other scrlView page on delegation begin dragging I remove the subviewVc1. The problem I have is that I can't deactivate scrolling of the scrlView where the subViewVc1 frame is.

I have tried multiple ways as subclassing the scrollview as modifying touchesBegan, but touchesBegan recognises a touch, if the user perform even a small drag the gesture is not recognised anymore. I have tried to add a swipe gesture recogniser but I realised it interfere with the scrollview gesture. Anybody has any idea on what to do?

1 Answers

Here is one way this can be achieved, instead of disabling a specific frame, you can disable interaction on over a specific view:

  1. SubClass the UIView you add on button tap with no real implementation but just for recognition - you can also use view tags if you prefer
  2. SubClass the UIScrollView and implement touchesShouldCancel to cancel scrolling when interacting with a specific view
  3. Set scrollView.delaysContentTouches = false

Implementation

First I tried to recreate your situation, I created the child view controller class with a button which will go into the scrollview:

class ChildVC: UIViewController
{
    let button = UIButton(type: .system)
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        view.backgroundColor = .yellow
        configureButton()
    }
    
    private func configureButton()
    {
        button.setTitle("Add View", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self,
                         action: #selector(didTapAddView),
                         for: .touchUpInside)
        view.addSubview(button)
        
        view.addConstraints([
        
            button.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            button.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            button.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            button.heightAnchor.constraint(equalToConstant: 50)
        
        ])
    }
    
    @objc
    private func didTapAddView()
    {
        let customView = UIView()
        customView.backgroundColor = .white
        customView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(customView)
        
        view.addConstraints([
        
            customView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
                                           constant: 16),
            customView.topAnchor.constraint(equalTo: view.topAnchor,
                                           constant: 16),
            customView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
                                           constant: -16),
            customView.bottomAnchor.constraint(equalTo: button.topAnchor,
                                           constant: -16)
        
        ])
    }
}

Then I created your container view controller with the scroll view and embedded the child vc into the view controller:

class ScrollTestViewController: UIViewController
{
    private let scrollView = UIScrollView()
    private let childVC1 = ChildVC()
    private let childVC2 = ChildVC()
    
    private let childVCHeight: CGFloat = 250
    private let childVCWidth: CGFloat = UIScreen.main.bounds.width
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        view.backgroundColor = .white
        title = "Scroll Test"
        configureScrollView()
        configureChildVC1()
        configureChildVC2()
    }
    
    override func viewDidLayoutSubviews()
    {
        super.viewDidLayoutSubviews()
        scrollView.contentSize = CGSize(width: childVC1.view.bounds.width * 2,
                                        height: scrollView.bounds.height)
    }
    
    private func configureScrollView()
    {
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(scrollView)
        
        view.addConstraints([
        
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        
        ])
    }
    
    private func configureChildVC1()
    {
        addChild(childVC1)
        childVC1.view.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(childVC1.view)
        
        view.addConstraints([
        
            childVC1.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            childVC1.view.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
            childVC1.view.widthAnchor.constraint(equalToConstant: childVCWidth),
            childVC1.view.heightAnchor.constraint(equalToConstant: childVCHeight)
        
        ])
    }
    
    private func configureChildVC2()
    {
        addChild(childVC2)
        childVC2.view.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(childVC2.view)
        
        view.addConstraints([
        
            childVC2.view.leadingAnchor.constraint(equalTo: childVC1.view.trailingAnchor),
            childVC2.view.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
            childVC2.view.widthAnchor.constraint(equalToConstant: childVCWidth),
            childVC2.view.heightAnchor.constraint(equalToConstant: childVCHeight)
        
        ])
    }
}

After doing this, you will get this result:

Child View controller swift iOS embed view controller in UIScrollView

No different, user can scroll anywhere even when swiping on top of the newly added view.

So I made these changes:

  1. Create a custom view subclass so I can recognize which view I am dragging over
class CustomView: UIView
{
    
}
  1. Create a custom scrollview to disable interaction over specific views
fileprivate class CustomScrollView: UIScrollView
{
    override func touchesShouldCancel(in view: UIView) -> Bool
    {
        // Just for demo, I added this
        if view is CustomView
        {
            print("Scroll disabled, tapping custom view")
        }
        else
        {
            print("Scroll enabled")
        }
        
        // You only need this line
        return !(view is CustomView)
    }
}
  1. Make the changes in the container view controller with the scroll view
// Replace private let scrollView = UIScrollView() with
private let scrollView = CustomScrollView()

// Add this as well
scrollView.delaysContentTouches = false
  1. Make changes in the child view controllers to use Custom view when tapping the button
@objc
private func didTapAddView()
{
    let customView = CustomView()
    customView.backgroundColor = .white
    customView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(customView)
    
    view.addConstraints([
    
        customView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
                                       constant: 16),
        customView.topAnchor.constraint(equalTo: view.topAnchor,
                                       constant: 16),
        customView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
                                       constant: -16),
        customView.bottomAnchor.constraint(equalTo: button.topAnchor,
                                       constant: -16)
    
    ])
}

Now when you swipe over the view, it will prevent you from scrolling

Prevent Disable scroll in UIScrollView when swiping scrolling in UIView swift iOS

Update based on Mihai's (OP's) comments

Do you have any idea how I could do it for a UIViewController instead of UIView, Im asking because the view is currently UIViewController and I lose some specifications about it in transforming it to an UIView.

So on the button press the view will add a UIViewController instead of a UIView

A scrollview can add UIViews to it's view hierarchy therefore you can only check if the view you are scrolling on is a specific type of view.

However, I still think the solution can work if you want to add a view controller on a button tap instead of a UIView.

  1. Keep the custom view as it is:
fileprivate class CustomView: UIView
{
    
}
  1. Create a view controller subclass and change the default view to be the custom view subclass
fileprivate class PurpleVC: UIViewController
{
    override func loadView()
    {
        super.loadView()

        // Change the default view from a UIView 
        // to be of type CustomView
        view = CustomView()

        view.backgroundColor = .purple
    }
}
  1. Add the new view controller on button tap instead of UIView
@objc
private func didTapAddView()
{
    let customVC = PurpleVC()
    customVC.view.translatesAutoresizingMaskIntoConstraints = false
    addChild(customVC)
    view.addSubview(customVC.view)
    
    view.addConstraints([
        
        customVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor,
                                               constant: 16),
        customVC.view.topAnchor.constraint(equalTo: view.topAnchor,
                                           constant: 16),
        customVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor,
                                                constant: -16),
        customVC.view.bottomAnchor.constraint(equalTo: button.topAnchor,
                                              constant: -16)
        
    ])
}
  1. No change to custom scroll view as it should still detect the custom view which is the main view inside your view controller
fileprivate class CustomScrollView: UIScrollView
{
    override func touchesShouldCancel(in view: UIView) -> Bool
    {
        // Just for demo, I added this
        if view is CustomView
        {
            print("Scroll disabled, tapping custom view")
        }
        else
        {
            print("Scroll enabled")
        }
        
        // You only need this line
        return !(view is CustomView)
    }
}
Related