Неоднозначность размера прокручиваемого контента UIScrollView в Auto Layout
Проблема и её корень
Ошибка "Scrollable Content Size Ambiguity" возникает из-за того, что UIScrollView не может определить свой contentSize — размер области, которую нужно прокручивать. В Auto Layout contentSize вычисляется на основе констрейнтов между UIScrollView и его дочерними вьюхами. Если эти констрейнты не задают однозначного размера (например, не привязаны к ширине/высоте самого ScrollView или не имеют фиксированных значений), возникает неоднозначность.
Правильная настройка UIScrollView в Interface Builder
Вот пошаговая инструкция, которая гарантированно решит проблему:
- Добавьте UIScrollView на сцену и задайте ему констрейнты ко всем четырем сторонам (leading, trailing, top, bottom) относительно Safe Area (или superview, если нужно).
- Добавьте один UIView (ContentView) внутрь ScrollView. Это будет контейнер для всего вашего контента.
- Привяжите 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 автоматически управляет расположением элементов, а его высота определяется суммой высот всех добавленных вьюх. Это избавляет от необходимости вручную задавать констрейнты для каждого элемента. Недоста