Введение: "Магия" UIScrollView, которая перестаёт работать
Вы когда-нибудь сталкивались с ситуацией: добавляете UIScrollView в свой проект на Swift, настраиваете Auto Layout, запускаете приложение — и ничего не скроллится? Или, что ещё хуже, Xcode выдаёт загадочную ошибку: "UIScrollView scrollable content size ambiguity"? Если да — вы не одиноки. Этот вопрос собрал тысячи просмотров на Stack Overflow, и сегодня мы раз и навсегда разберёмся, в чём тут дело.
Представьте: вы создаёте экран с длинной формой регистрации или статьёй, которая не помещается на одном экране. Вы добавляете UIScrollView, кидаете в него все элементы, расставляете констрейнты — и... тишина. Скролл не работает. Знакомо? Добро пожаловать в клуб разработчиков, которые проклинали Auto Layout за "непонятливость".
На самом деле, проблема Content Size Ambiguity — это не баг iOS, а логическая ловушка. Система просто не знает, насколько большим должен быть ваш контент. В этом туториале мы разберём причину, три способа решения и напишем реальный рабочий код. Поехали!
Что такое Content Size Ambiguity и почему она возникает?
UIScrollView — это особый зверь в мире UIKit. В отличие от обычных вью, он не определяет свой размер по констрейнтам дочерних элементов. Вместо этого он использует свойство contentSize (тип CGSize), которое указывает, насколько большой "подкапотный" холст. Если contentSize меньше или равен размеру самого UIScrollView, скролл... ну, не нужен.
Ошибка Scrollable Content Size Ambiguity возникает, когда Auto Layout не может однозначно вычислить contentSize на основе констрейнтов внутри скролл-вью. Типичные сценарии:
- Вы не задали констрейнты для высоты или ширины внутреннего контента.
- Констрейнты "плавают" (например, нет связи с верхом и низом скролл-вью).
- Вы используете
UIStackViewвнутриUIScrollView, но не настроили его размеры. - Вы забыли, что
UIScrollViewтребует полной цепочки констрейнтов от края до края.
В консоли Xcode вы увидите что-то вроде:
Unable to simultaneously satisfy constraints.
Will attempt to recover by breaking constraint.
...
UIScrollView scrollable content size ambiguity
Звучит пугающе, но решение простое. Давайте напишем код.
Шаг 1: Создаём проект и добавляем UIScrollView
Откройте Xcode, создайте новый проект с шаблоном App (iOS, Swift). Назовём его, например, ScrollFixDemo. Перейдите в ViewController.swift и замените весь код на следующий:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
}
private func setupScrollView() {
// 1. Создаём UIScrollView
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .systemGray6
view.addSubview(scrollView)
// 2. Констрейнты для scrollView (заполняем весь экран)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
// 3. Создаём contentView - контейнер для всего контента
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.backgroundColor = .white
scrollView.addSubview(contentView)
// 4. Ключевой момент: констрейнты contentView к scrollView
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
// ВАЖНО: ширина contentView = ширине scrollView
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
])
// 5. Добавим немного контента, чтобы проверить скролл
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Это длинный текст, который должен скроллиться. " + String(repeating: "Ещё текст. ", count: 50)
label.numberOfLines = 0
contentView.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20)
])
// 6. Добавим ещё одну вью снизу, чтобы создать вертикальный скролл
let bottomView = UIView()
bottomView.translatesAutoresizingMaskIntoConstraints = false
bottomView.backgroundColor = .systemBlue
contentView.addSubview(bottomView)
NSLayoutConstraint.activate([
bottomView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 40),
bottomView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
bottomView.widthAnchor.constraint(equalToConstant: 200),
bottomView.heightAnchor.constraint(equalToConstant: 1000), // большая высота
bottomView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
}
}
Почему это работает? Мы задали contentView ширину, равную ширине scrollView, и привязали его ко всем четырём краям. Высота contentView автоматически вычисляется из констрейнтов дочерних элементов (лейбл + синяя вью). Если высота контента превышает высоту скролл-вью — появляется скролл. Если нет — нет скролла. Магия!
Шаг 2: Типичная ошибка новичка (и как её избежать)
Самая популярная ошибка — забыть про width constraint для contentView. Если вы его не поставите, Auto Layout не сможет определить ширину контента, и UIScrollView будет "думать", что contentSize равен нулю. Вот что происходит:
// ❌ Неправильно: нет привязки по ширине
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
// Нет widthAnchor!
Результат: скролл не работает, ошибка в консоли. Запомните правило: для вертикального скролла вы должны явно задать ширину contentView равной ширине scroll