Avatar ·

Неоднозначность размера прокручиваемого контента UIScrollView в Auto Layout

Всем привет! Я новичок в Swift и никак не могу разобраться с UIScrollView в Interface Builder. Использую Auto Layout, но Xcode постоянно выдает ошибку: "Scrollable Content Size Ambiguity". Проблема в том, что я не могу понять, как правильно задать размер содержимого, чтобы ScrollView работал корректно. Вот что я делаю: добавляю UIScrollView на ViewController, растягиваю его по краям (leading, trailing, top, bottom) через констрейнты к Safe Area. Внутрь ScrollView кладу UIView (contentView), к которой добавляю лейблы, картинки и кнопки. ContentView я тоже привязываю ко всем четырем сторонам ScrollView. Но Xcode ругается, что ScrollView не знает, какой у него контент. Я пробовал добавлять фиксированную высоту для contentView, но тогда приложение не скроллится, если контент меньше экрана, или скроллится неправильно. Также пробовал использовать intrinsicContentSize, но это не помогло. Подскажите, пожалуйста, как правильно настроить UIScrollView с Auto Layout, чтобы он работал как надо: скроллился только тогда, когда контент больше экрана, и не было ошибок в Interface Builder? Заранее спасибо! Пример моего кода (вьюха создается в сториборде, но для ясности привожу код): ```swift import UIKit class ViewController: UIViewController { @IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var contentView: UIView! override func viewDidLoad() { super.viewDidLoad() // Настройка констрейнтов в коде не делаю, все в сториборде } } ``` <<>>

Проблема и её корень

Ошибка "Scrollable Content Size Ambiguity" возникает из-за того, что UIScrollView не может определить свой contentSize — размер области, которую нужно прокручивать. В Auto Layout contentSize вычисляется на основе констрейнтов между UIScrollView и его дочерними вьюхами. Если эти констрейнты не задают однозначного размера (например, не привязаны к ширине/высоте самого ScrollView или не имеют фиксированных значений), возникает неоднозначность.

Правильная настройка UIScrollView в Interface Builder

Вот пошаговая инструкция, которая гарантированно решит проблему:

  1. Добавьте UIScrollView на сцену и задайте ему констрейнты ко всем четырем сторонам (leading, trailing, top, bottom) относительно Safe Area (или superview, если нужно).
  2. Добавьте один UIView (ContentView) внутрь ScrollView. Это будет контейнер для всего вашего контента.
  3. Привяжите ContentView к ScrollView:
    • Leading, Trailing, Top, Bottom — все к ScrollView (значение 0).
    • Важно: Не добавляйте констрейнты на ширину или высоту ContentView вручную! Вместо этого задайте два специальных констрейнта:
// В Interface Builder:
// 1. Выберите ContentView.
// 2. Control + перетащите от ContentView к ScrollView.
// 3. Выберите "Equal Widths" (Равная ширина).
// 4. Control + перетащите от ContentView к ScrollView.
// 5. Выберите "Equal Heights" (Равная высота).

// После этого измените приоритет констрейнта "Equal Heights" на 250 (Low).
// Это позволит ScrollView понимать, что контент может быть выше экрана.

Теперь contentSize будет вычисляться на основе вложенных элементов. Если вы добавите, например, UILabel с длинным текстом, который не помещается по высоте, ScrollView автоматически увеличит contentSize и включит прокрутку.

Пример полной настройки в коде (если вы предпочитаете код, а не сториборд)

import UIKit

class ViewController: UIViewController {
    
    let scrollView: UIScrollView = {
        let sv = UIScrollView()
        sv.translatesAutoresizingMaskIntoConstraints = false
        return sv
    }()
    
    let contentView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    let label: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.text = "Очень длинный текст, который занимает много строк и должен прокручиваться..."
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLayout()
    }
    
    private func setupLayout() {
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        contentView.addSubview(label)
        
        // Констрейнты ScrollView к краям экрана
        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // Констрейнты ContentView к ScrollView
        NSLayoutConstraint.activate([
            contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
        ])
        
        // Ключевые констрейнты: ширина ContentView = ширине ScrollView
        NSLayoutConstraint.activate([
            contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
            // Высота НЕ привязывается — она определяется контентом!
        ])
        
        // Констрейнты для лейбла (определяют высоту ContentView)
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
            label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16)
        ])
    }
}

Ключевые моменты

  • Ширина ContentView должна быть равна ширине ScrollView (чтобы не было горизонтального скролла, если он не нужен).
  • Высота ContentView определяется вложенными элементами — их констрейнты от верха до низа ContentView.
  • Приоритет констрейнта высоты (если вы его задаете) должен быть низким (250), чтобы ScrollView мог игнорировать его, когда контент больше.
  • Не забывайте, что UIScrollView использует contentLayoutGuide и frameLayoutGuide в iOS 11+, но описанный выше метод работает во всех версиях.

После такой настройки ошибка исчезнет, а ScrollView будет корректно скроллиться только тогда, когда контент действительно не помещается на экране.

<<>>

Альтернативный подход: Использование StackView внутри ScrollView

Если вам не хочется вручную задавать констрейнты для каждого элемента, можно использовать UIStackView как контейнер внутри UIScrollView. Это упрощает верстку динамического контента.

import UIKit

class ViewController: UIViewController {
    
    let scrollView = UIScrollView()
    let stackView = UIStackView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
        addContent()
    }
    
    private func setupScrollView() {
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.spacing = 16
        
        view.addSubview(scrollView)
        scrollView.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
            stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
            stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
            stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -32) // Учитываем отступы
        ])
    }
    
    private func addContent() {
        for i in 1...20 {
            let label = UILabel()
            label.text = "Строка \(i)"
            label.backgroundColor = .lightGray
            stackView.addArrangedSubview(label)
        }
    }
}

Преимущества: StackView автоматически управляет расположением элементов, а его высота определяется суммой высот всех добавленных вьюх. Это избавляет от необходимости вручную задавать констрейнты для каждого элемента. Недоста