UIScrollView 使用 Autolayout 或缩放后停止滚动

UIScrollView stops scrolling with Autolayout or after zooming

我有以下代码来设置 UIScrollView 的子视图。我首先创建一个名为 MyScrollView 的 UIScrollView 的子类。然后我添加一个名为 contentView 的视图作为子视图。我将所有其他视图添加到滚动视图作为此 contentView 的子视图。问题是在使用自动布局约束设置以下代码后,scrollView 不会滚动。

public class MyScrollView:UIScrollView {


  private var contentView:UIView!

   override init(frame: CGRect) {
    super.init(frame: frame)
    self.translatesAutoresizingMaskIntoConstraints = false
    self.isScrollEnabled = true
    self.isDirectionalLockEnabled = true
    self.showsHorizontalScrollIndicator = true
    self.showsVerticalScrollIndicator = false
    self.decelerationRate = .normal
    self.delaysContentTouches = false
    self.bouncesZoom = true
    
    setupSubviews()
}


  private func setupSubviews() {

     contentView = UIView()
    contentView.backgroundColor = UIColor.clear
    contentView.translatesAutoresizingMaskIntoConstraints = false
    contentView.isUserInteractionEnabled = true
    
    self.addSubview(contentView)
    
    contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
    contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
    contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
    contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
    contentView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: CGFloat(19), constant: CGFloat(5)).isActive = true
    contentView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 2.0, constant: CGFloat(5)).isActive = true
    
   // self.contentSize = CGSize(width: 10000, height: 2*frame.height)


    let subviewWidth = CGFloat(450)
    let subviewHeight = CGFloat(20)

    //Add other subviews to contentView
    (0...4).compactMap { i in
        (0...19).compactMap { j in
             let x = CGFloat(j)*(subviewWidth + 5.0) + 5.0
            let y = CGFloat(i)*(subviewHeight + 5.0) + 5.0
             let subview = UIView(frame: CGRect(x: x, y: y, width: subviewWidth, height: subviewHeight))
                contentView.addSubview(subview)
      }
  }

    

问题是,当自动布局约束设置如上时,滚动视图不会滚动。如果没有设置约束,它会滚动,但在缩放后,滚动会再次被禁用。我只是在滚动视图委托中设置了这个。

  public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
    return scrollView.subviews.first //contentView
}

对于你的方法...

您想将 contentView 限制在滚动视图的内容布局指南中。这将自动确定“可滚动”区域。

由于您没有对 contentView 的子视图使用 auto-layout,因此每次添加新子视图时都需要更新 .contentView 宽度和高度限制。

举个例子。我们将在距顶部/前导/尾部 40 点处创建一个 MyScrollView,高度为 240 点。

子视图将有一个绿色背景,contentView 将有一个蓝色背景,滚动视图将有一个红色背景(所以我们可以很容易地看到框架)。

我们将从一个子视图开始,以便于查看会发生什么。一开始只有一个小的subview,不会有滚动。

每次我们点击任何地方,我们都会向 contentView 添加一个新的子视图,背景为黄色,并根据需要更新 contentView 的宽度和高度约束。您会看到蓝色内容视图变大以匹配子视图。一旦子视图导致内容视图大于滚动视图的宽度或高度,滚动将自动进行。

public class MyScrollView: UIScrollView {
    
    private var contentView:UIView!
    
    // contentView's Width and Height constraints
    //  we'll update the .constant values when we add subviews
    private var cvWidthConstraint: NSLayoutConstraint!
    private var cvHeightConstraint: NSLayoutConstraint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        self.translatesAutoresizingMaskIntoConstraints = false
        self.isScrollEnabled = true
        self.isDirectionalLockEnabled = true
        self.showsHorizontalScrollIndicator = true
        self.showsVerticalScrollIndicator = false
        self.decelerationRate = .normal
        self.delaysContentTouches = false
        self.bouncesZoom = true
        
        setupSubviews()
        
    }
    
    private func setupSubviews() {
        
        contentView = UIView()
        contentView.backgroundColor = UIColor.clear
        contentView.translatesAutoresizingMaskIntoConstraints = false
        contentView.isUserInteractionEnabled = true
        
        self.addSubview(contentView)
        
        // constrain contentView to scroll view's Content Layout Guide
        //  this determines the "scrollable" area
        contentView.leadingAnchor.constraint(equalTo: self.contentLayoutGuide.leadingAnchor).isActive = true
        contentView.trailingAnchor.constraint(equalTo: self.contentLayoutGuide.trailingAnchor).isActive = true
        contentView.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor).isActive = true

        // create contentView's Width and Height constraints
        cvWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0.0)
        cvHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 0.0)
        
        // activate them
        cvWidthConstraint.isActive = true
        cvHeightConstraint.isActive = true
        
        //Add other subviews to contentView
        
        // we'll start with ONE subview, so we can easily see what's happening
        let subviewWidth = CGFloat(240)
        let subviewHeight = CGFloat(20)

        let subview = UILabel(frame: CGRect(x: 5, y: 5, width: subviewWidth, height: subviewHeight))
        subview.textAlignment = .center
        subview.text = "First"
        subview.backgroundColor = .green
        contentView.addSubview(subview)

        // so we can see the frames
        self.backgroundColor = .red
        self.contentView.backgroundColor = .blue
        
        // update the contentView constraints
        updateContent()
    }
    
    private func updateContent() -> Void {
        // array of subviews
        let views = contentView.subviews
        // get the
        //  max Y of the subview frames
        //  max X of the subview frames
        guard let maxYValue = views.lazy.map({ [=10=].frame.maxY }).max(),
              let maxXValue = views.lazy.map({ [=10=].frame.maxX }).max()
        else { return }
        
        // update contentView Width and Height constraints
        cvWidthConstraint.constant = maxXValue + 5.0
        cvHeightConstraint.constant = maxYValue + 5.0
    }
    
    func addLabel(frame _frame: CGRect, text: String) -> Void {

        // add a new subview
        let subview = UILabel(frame: _frame)
        subview.textAlignment = .center
        subview.text = text
        subview.backgroundColor = .yellow
        contentView.addSubview(subview)
        
        // update the contentView constraints
        updateContent()

    }
}


class ExampleViewController: UIViewController {
    
    let myScrollView = MyScrollView()
    
    var count: Int = 1
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(myScrollView)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([

            // constrain custom scroll view Top / Leading / Trailing
            //  40-pts from the safe-area edges
            myScrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            myScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            myScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            // scroll view Height: 240-pts
            myScrollView.heightAnchor.constraint(equalToConstant: 240.0),
        ])
        
        // add tap gesture recognizer so we can add a new subview
        //  every time we tap
        let t = UITapGestureRecognizer(target: self, action: #selector(self.testAddSubview))
        view.addGestureRecognizer(t)
    }
    
    @objc func testAddSubview() -> Void {
        let s = "New Subview \(count)"
        let x: CGFloat = CGFloat(count) * 60.0
        let y: CGFloat = CGFloat(count) * 35.0
        myScrollView.addLabel(frame: CGRect(x: x, y: y, width: 200, height: 30), text: s)
        count += 1
    }
}

启动时 - 一个子视图 - 无滚动:

添加一个新的子视图后 - 蓝色内容视图更大,但不足以滚动:

添加 4 个新子视图后 - 现在我们可以滚动了: