如何制作具有动态宽度的水平 UIScrollView?

How to make horizontal UIScrollView with dynamic width?

我有一个动态增加宽度的标签。 我决定将它添加到 ScrollView(以便用户可以在标签中看到任意长度的文本)。

可是我运行遇到了问题。实现这个的正确方法是什么?我要怎么做。如何制作动态宽度的水平滚动视图?

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        scrollView.contentSize = .init(width: label.frame.width, height: label.frame.height)
    }
    
    func setupScrollView() {
        view.addSubview(scrollView)
        scrollView.addSubview(label)
        
        let frameLayoutGuide = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 400),
            frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
            frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -100),
            frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -300),
        ])
        
        let contentLayoutGuide = scrollView.contentLayoutGuide
        
        NSLayoutConstraint.activate([
            contentLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 400),
            contentLayoutGuide.centerXAnchor.constraint(equalTo: frameLayoutGuide.centerXAnchor),
            contentLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -300),

            label.centerXAnchor.constraint(equalTo: contentLayoutGuide.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: contentLayoutGuide.centerYAnchor)
        ])
    }

抱歉,您对 scrollView.contentLayoutGuidescrollView.frameLayoutGuide 的使用大错特错了。

您想将 scrollView 本身限制到它的超级视图(在本例中是主 view)并限制滚动视图的 子视图到 scrollView 的 .contentLayoutGuide。然后,您可以使用滚动视图的 .frameLayoutGuide 来帮助设置其子视图的大小。

因此,如果您希望标签在滚动视图中居中,然后在标签太宽无法容纳时启用水平滚动,您需要:

  • 将您的标签添加到“holder”视图,限制在中心
  • 将“holder”视图添加到滚动视图
    • 将其位置限制在 .contentLayoutGuide
    • 将其大小限制为 .frameLayoutGuide
  • 向标签添加前导和尾随约束以强制“holder”视图在需要时增长

这是一个简单的例子。它将循环显示标签的不同长度字符串...滚动将根据标签的最终宽度自动启用或禁用(全部具有自动布局 - 无需计算大小):

class ViewController: UIViewController {
    
    let scrollView = UIScrollView()
    let label = UILabel()
    let labelHolderView = UIView()

    let sampleStrings: [String] = [
        "Short (no scrolling).",
        "A little longer (still no scrolling).",
        "A much longer string, that will definitely require horizontal scrolling.",
        "Just for kicks, let's make this string really, really long, to help demonstrate the benefits of using auto-layout!",
    ]
    
    var stringIndex: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
    }
    
    func setupScrollView() {
        
        // add the label to the holder view
        labelHolderView.addSubview(label)
        
        // add the holder view to the scroll view
        scrollView.addSubview(labelHolderView)
        
        // add the scroll view to the view
        view.addSubview(scrollView)

        // all three need this
        labelHolderView.translatesAutoresizingMaskIntoConstraints = false
        label.translatesAutoresizingMaskIntoConstraints = false
        scrollView.translatesAutoresizingMaskIntoConstraints = false

        let safeG = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        let frameG = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // let's put a 100-pt tall scroll view
            //  40-pts from the bottom
            //  40-pts on each side
            scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -40.0),
            scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 40.0),
            scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -40.0),
            scrollView.heightAnchor.constraint(equalToConstant: 100.0),

            // constrain holder view to ContentGuide with Zero on all 4 sides
            labelHolderView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
            labelHolderView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
            labelHolderView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
            labelHolderView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),

            // we only want horizontal scrolling, so constrain
            //  holder view height to sccroll view FrameGuide
            labelHolderView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
            
            // we want the label centered horizontally
            //  in the holder view and in the scroll view
            //  so set holder view width >= FrameGuide
            labelHolderView.widthAnchor.constraint(greaterThanOrEqualTo: frameG.widthAnchor, constant: 0.0),
            
            // center the label in the holder view
            label.centerXAnchor.constraint(equalTo: labelHolderView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: labelHolderView.centerYAnchor),
            
            // as the label expands horizontally, we want at least
            //  8-pts on each side
            label.leadingAnchor.constraint(greaterThanOrEqualTo: labelHolderView.leadingAnchor, constant: 8.0),
            label.trailingAnchor.constraint(lessThanOrEqualTo: labelHolderView.trailingAnchor, constant: -8.0),
            
        ])
        
        // let's use some background colors so we can see the view frames
        scrollView.backgroundColor = .yellow
        labelHolderView.backgroundColor = .systemTeal
        label.backgroundColor = .green
        
        // add a button above the scroll view to cycle through our sample strings
        let b = UIButton()
        b.setTitle("Tap Me", for: [])
        b.setTitleColor(.white, for: .normal)
        b.setTitleColor(.lightGray, for: .highlighted)
        b.backgroundColor = .systemGreen
        b.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(b)
        NSLayoutConstraint.activate([
            b.bottomAnchor.constraint(equalTo: scrollView.topAnchor, constant: -20.0),
            b.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.6),
            b.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
        ])
        
        b.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
        
        // set the initial text
        gotTap(nil)
        
    }

    @objc func gotTap(_ sender: Any?) -> Void {
        label.text = sampleStrings[stringIndex % sampleStrings.count]
        stringIndex += 1
    }
    
}