UIScrollView:在不更改 contentView 的情况下放大时将滚动锁定到垂直轴

UIScrollView: Lock scrolling to vertical axis when zoomed in without changing the contentView

根据缩放比例将 UIScrollView 中的滚动动态锁定到垂直轴的最佳方法是什么? 我想允许在缩小时向任何方向滚动大 canvas (scrollView.zoomScale < 1.0) 但在放大时完全防止水平滚动 (scrollView.zoomScale == 1.0)。

这里的挑战是 UIScrollView 似乎没有内置设置来限制滚动到一个方向,如果 contentView 在两个方向上都大于视口。我想使用相同的大 contentView 但在放大时不允许水平滚动。 (我知道 scrollView.isDirectionalLockEnabled,但这不是我需要的:它只检查用户的平移手势是否有主导的滚动方向,然后动态锁定滚动到任一方向。)

谢谢!

如果我正确理解您的目标...

  • 您有一个 大于 滚动视图的“contentView”
  • 如果缩放比例为 1.0,则只允许垂直滚动
  • 如果缩放比例小于 1.0,则允许垂直和水平滚动

因此,如果我们有一个大小为 388 x 661 的滚动视图框架和一个大小为 2100 x 2100 的“contentView”,我们从缩放比例 1.0 开始(亮绿色是滚动视图框架):

并且只允许垂直滚动。

如果用户缩小到 0.8 比例:

允许垂直水平滚动。

如果用户然后放大回 1.0 比例:

我们回到仅垂直滚动。

您可以通过使您的控制器符合 UIScrollViewDelegate、将自己指定为 scrollView 的委托、添加“last scrollView content offset X”变量,然后实现 scrollViewDidScroll():

var lastOffsetX: CGFloat = 0

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    
    // if zoom scale is 1.0
    //  don't allow horizontal scrolling
    if scrollView.zoomScale == 1.0 {
        scrollView.contentOffset.x = lastOffsetX
        return
    }
    
    // zoom scale is less than 1.0, so
    //  allow the scroll and update lastX
    lastOffsetX = scrollView.contentOffset.x
    
}

这是您可以试用的完整示例:

class ExampleViewController: UIViewController, UIScrollViewDelegate {
    
    let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.backgroundColor = .systemYellow
        return v
    }()
    let contentView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemTeal
        return v
    }()
    let infoLabel: UILabel = {
        let v = UILabel()
        return v
    }()

    // we'll use this to track the current content X offset
    var lastOffsetX: CGFloat = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        scrollView.addSubview(contentView)
        view.addSubview(scrollView)
        view.addSubview(infoLabel)
        
        [contentView, scrollView, infoLabel].forEach {
            [=11=].translatesAutoresizingMaskIntoConstraints = false
        }
        
        // let's add a 8 x 8 "grid" of labels to the content view
        let outerVerticalStack = UIStackView()
        outerVerticalStack.axis = .vertical
        outerVerticalStack.spacing = 20
        outerVerticalStack.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(outerVerticalStack)
        
        var j: Int = 1
        for _ in 1...8 {
            let rowStack = UIStackView()
            rowStack.axis = .horizontal
            rowStack.spacing = 20
            rowStack.distribution = .fillEqually
            
            for _ in 1...8 {
                let v = UILabel()
                v.font = .systemFont(ofSize: 48.0, weight: .regular)
                v.text = "\(j)"
                v.textAlignment = .center
                v.backgroundColor = .green
                v.widthAnchor.constraint(equalToConstant: 240.0).isActive = true
                v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
                rowStack.addArrangedSubview(v)
                j += 1
            }
            outerVerticalStack.addArrangedSubview(rowStack)
        }
        
        let safeG = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
            scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
            scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -120.0),
            
            contentView.topAnchor.constraint(equalTo: contentG.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
            
            outerVerticalStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
            outerVerticalStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
            outerVerticalStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
            outerVerticalStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),

            // put the info label below the scroll view
            infoLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
            infoLabel.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
            infoLabel.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
            
        ])

        // we'll update min zoom in viewDidAppear
        //  (after all views have been laid out)
        scrollView.minimumZoomScale = 1.0
        scrollView.maximumZoomScale = 1.0
        
        // we need to disable zoom bouncing, or
        //  we get really bad positioning effect
        //  when zooming in past 1.0
        scrollView.bouncesZoom = false
        
        // assign the delegate
        scrollView.delegate = self

        // update the info label
        updateInfo()

    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // update min zoom scale so we can only "zoom out" until
        //  the content view fits the scroll view frame
        if scrollView.minimumZoomScale == 1.0 {
            print(contentView.frame.size)
            let xScale = scrollView.frame.width / contentView.frame.width
            let yScale = scrollView.frame.height / contentView.frame.height
            scrollView.minimumZoomScale = min(xScale, yScale)
        }

    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return contentView
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        // if zoom scale is 1.0
        //  don't allow horizontal scrolling
        if scrollView.zoomScale == 1.0 && !scrollView.isZooming {
            scrollView.contentOffset.x = lastOffsetX
            return
        }
        
        // zoom scale is less than 1.0, so
        //  allow the scroll and update lastX
        lastOffsetX = scrollView.contentOffset.x
        
    }

    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateInfo()
    }
    
    func updateInfo() {
        let s = String(format: "%0.4f", scrollView.zoomScale)
        infoLabel.text = "Zoom Scale: \(s)"
    }
    
}