Scroll View / 1.1. UIScrollView



Солнце и луна сменяют день и ночь, но что сменит разум?

Аррилл

В этом уроке


Собственное решение

Чтобы понять проблему, решаемую видами с прокруткой, давайте создадим следующий проект на основе SingleViewApp:

  1. На холст перетащите обычный UIView
    • Прикрепите его к границам представления
  2. В него вставьте три лейбла:
    • Все они должны содержать какие-то эмодзи с головами людей (например, 🤪😛🤨)
    • Размер текста - 200 точек
    • Выравнивание по центру горизонтально
    • Верхнюю метку подвиньте к безопасной зоне
    • Остальные делайте так, чтобы они касались друг друга сторонами
  3. Добавьте остальные ограничения в автоматическом режиме

Запустив приложение, Вы получите такую-вот проблему. Контент не влазит в экран!

Это неудивительно - размеры экранов не слишком велики относительно компьютера.

Попробуем решить эту проблему самостоятельно, создав свой класс с возможностью прокрутки с использованием жеста для перемещения объектов (Pan)!

Создайте класс SWScrollView, унаследовав его от простого UIView.

Сперва мы создадим метод, который станет действием для распознавания жестов:

@objc  func panView(with gestureRecognizer: UIPanGestureRecognizer) {
    let translation = gestureRecognizer.translation(in: self)
    //Получаем объект перемещения из жеста
    UIView.animate(withDuration: 0.20) {
        self.bounds.origin.y -= translation.y
        //Смещаем объект
        gestureRecognizer.setTranslation(.zero, in: self)
        //Сбрасываем движение ноль
    }
}

А теперь создадим требуемый конструктор класса, в котором будем добавлять наш жест:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panView(with:)))
    addGestureRecognizer(panGesture)
}

В инспекторе идентичности для нашего представления, в котором лежат эмодзи, задайте класс SWScrollView и запустите программу:

Мы решили проблему с прокруткой, но этот метод остаётся солоноватым и никак не реагирует на вложенные объекты - проведите ещё немного и все объекты уплывут вверх, а это плохой пользовательский опыт.

Изучим же решение проблемы из UIKit!


ScrollView

Создадим новое приложение!

  1. В ассеты перетащите нашу подготовленную картинку, посвящённую зомби.
  2. На холст добавьте ScrollView
    • Разместите (но не пока что без ограничений) его так, чтобы он заполнял весь объем контроллера
    • Аутлет - scrollView
  3. А также добавьте внутрь него ImageView:
    • Картинка - wd
    • Выровняйте также без ограничений по родительскому представлению
    • Аутлет - imageView
    • Content Mode - Top Left, что даст отображение картинки от левого верхнего угла.

Во viewDidLoad добавим:

scrollView.contentInsetAdjustmentBehavior = .never

Это уберет автоматическое смещение внутренней зоны представления с прокруткой относительно безопасной зоны.

Также представлению с прокруткой нужно знать размер вложенного контента для корректной работы:

scrollView.contentSize = (imageView.image?.size)!

Мы задаём его через размеры самой картинки.

Запустите приложение прокрутка во все стороны работать будет

Однако у нас ешь многое не работает - например, увеличение/уменьшение. Что мы исправим уже в следующем параграфе!


Zoom

Apple первыми представили жест сведения или разведения двух пальцев, который срабатывает как зум.

Добавим способности зума к нашему приложению.

Реализуем для этого расширение ViewController для реализации делегата представления с прокруткой:

extension ViewController: UIScrollViewDelegate {
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }
}

Данный метод делегата возвращает представление внутри представления, которое следует зуммировать.

Если Вы вложите ScrollView в другой ScrollView, то он будет работать как и ожидалось. Однако, если у обоих из них будет один и тот же объект-делегат, то наступят некоторые проблемы - так как метод будет получать уведомление от обоих. Однако именно поэтому метод делегата поручает и сам scrollView в качестве объекта.

Соедините делегат вида с прокруткой с самим контроллером,

У делегата также есть полезный метод - scrollViewDidZoom, который вызывается при зуме и позволяет выполнить какие-то дополнительные действия.

Создадим ешь метод для задания параметров зуммирования для заданных размеров:

func setZoomParametersForSize(_ scrollViewSize: CGSize) {
    let imageSize = image.bounds.size
    //Размер картинки
    let widthScale = scrollViewSize.width / imageSize.width
    //Масштабирование по ширине
    let heightSize = scrollViewSize.height / imageSize.height
    //И по высоте
    let minScale = min(widthScale, heightSize)
    //Получение минимального масштабирования
    scrollView.minimumZoomScale = minScale
    //Минимально возможное масштабирование
    scrollView.maximumZoomScale = 3.0
    //Максимально возможное
    scrollView.zoomScale = minScale
    //Изначальное масштабирование
}

Изменим немного метод viewDidLoad для использования созданного метода:

override func viewDidLoad() {
    super.viewDidLoad()

    scrollView.contentInsetAdjustmentBehavior = .never
    //        scrollView.contentSize = (imageView.image?.size)!
    imageView.frame.size = (imageView.image?.size)!
    //Теперь представление для картинки равно по размеру картинке

    setZoomParametersForSize(scrollView.bounds.size)
}

А также добавим ешь один метод:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    setZoomParametersForSize(scrollView.bounds.size)
}

Он будет срабатывать при всяком изменении размеров главного представления контроллера (например, при изменении ориентации).

Запустите приложение и Вы сможете двигать изображение:

Для зума на стимуляторе зажмите OPT и двигайте мышкой (появятся два круга, соответствующие пальцам).

Попа Нигана так же хороша, как его бита.


Центрирование изображения

Для того, чтобы создать внутренние отступы контента для представления с прокруткой используется понятие инсетов, которое ранее нам кратко встречалось.

Создадим метод контроллера:

func recenterImage() {

}

И будем последовательно в него добавлять.

let scrollViewSize = scrollView.bounds.size
let imageSize = imageView.frame.size

Так мы получим размеры представления с прокруткой и картинки.

let horizontalSpace = imageSize.width < scrollViewSize.width ? (scrollViewSize.width - imageSize.width) / 2 : 0

let verticalSpace = imageSize.height < scrollViewSize.height ? (scrollViewSize.height - imageSize.height) / 2 : 0

Создадим отступы.

И в конце концов создадим инсеты:

scrollView.contentInset = UIEdgeInsets(top: verticalSpace, left: horizontalSpace, bottom: verticalSpace, right: horizontalSpace)

Вызов функции добавьте в методы загрузки представления и смены разметки.


AutoLayout и ScrollView

Мы опять создадим новое приложение.

  1. Перенесите уже полюбившиеся нам ассеты
  2. Создайте ScrollView на полотне, прикрепив его ко всем границам представления с помощью ограничений
  3. Внутри него разместите картинку:
    • Также прикрепите её ко всем границам
    • Режим - Top Left
    • Картинка - wd

Теперь если запустить приложение, всё будет работать само по себе, так как на основании ограничений представление с прокруткой сможет само понять, как и что делать.

Для дальнейшей работы мы удалим это изображение.

Теперь мы создадим представление из прокручивающихся прямоугольников, не используя код.

Другой способ решить проблему - выбрать все объекты и перейти по пути Editor -> Embed In -> Scroll View. Все констреинты сохранятся, но представление будет автоматически встроено в Scroll View.

Измените цвет фона представления прокрутки на черный.

Ещё одна альтернатива - встроить все элементы в какой-то контейнер-представление, настроить ограничения относительно него, а уже его встроить в представление с прокруткой. Но этот способ требует большей работы руками.

Добавьте на холст представления с размерами (359 * 179) и цветами (сверху вниз):

Если возникает проблема при создании интерфейсов с прокручиваемыми фрагментами, то стоит зайти в инспектор размеров самого контроллера и выбрать Freeform, что позволит задавать свободные размеры при отображении в Interface Builder.

Запустите приложение и всё будет работать примерно так, как мы привыкли видеть в табличных представлениях.

Не пытайтесь таким образом реализовать собственные таблицы, галереи и тому подобное. Чистый ScrollView чаще используется как основа для других представлений или же как способ прокрутить группу несходного контента. Для прокрутки фотографий лучше, например, подойдёт его подкласс CollectionView.

Система определения размера контента для AutoLayout будет работать... если этот размер можно определить. Если вложить элементы, чьи размеры так же могут меняться, то будут проблемы.