Из этой статьи вы узнаете как создать компонент отображения фотографий для просмотра, редактирования и обрезки.
В этой статье вы прочитаете:
Примеры изображений и кода из этой статьи доступны на GitHub
Отображение фотографий в разных режимах
В приложении фотография отображается в трех режимах:
Просмотр фото - для отображения готового изображения. Оно уже обрезано, наклеены наклейки и применен фильтр.
Редактор фото - отображается исходное изображение, которое обрезано и к нему применен фильтр. Наклейки отображаются поверх в виде самостоятельных View, которые можно перемещать. Про наклейки будет отдельный пост.
Обрезка фото и поворот - отображается исходное изображение, которое можно вращать и поверх него будет отображаться рамка для обрезки фотографии. Про это будет отдельный пост.
Во всех трех режимах изображение можно масштабировать двумя пальцами или слайдером и двигать. Режим аналогичен системному приложению "Фото".
Про добавление стикеров и работу рамки для обрезки я расскажу в будущих статьях, в этой будет только просмотр.
Какие есть сложности в работе с просмотром фото в SwiftUI
Для корректного отображения изображения во всех трех режимах мне необходимо управлять расположением элементов внутри контейнера, осуществлять обрезку и поворот фотографии. А также добавлять отступы для фото в зависимости от масштаба, чтобы оно располагалось по центру.
ScrollView в SwiftUI не умеет из коробки работать с Zoom. Его можно эмулировать через ScaleEffect и MagnificationGesture(). Однако смещение будет сбиваться, т.к. жест масштабирования не возвращает свои стартовые точки
Аналогичная проблема будет с вращением. От жеста можно получить угол поворота, но не центр
Тяжело развести жесты обрезки и скролла. Если привязать жест к "ручке" изменения размера, то при резком перемещении он будет "отваливаться" и перемещение будет сбрасываться. И я не смог привязать жест к контейнеру и фильтровать его работу в зависимости от того, где был начат жест.
Я решил, что эти экраны мне будет легче создать на базе UIKit и добавить в SwiftUI как готовый компонент. Возможно в будущем, когда будет больше опыта и подходящих API, я смогу реализовать все элементы на SwiftUI.
Результат опытов со SwiftUI можно посмотреть в Github
Просмотр фото на UIKit и обертка в Representative
Я создаю общий компонент, который реализует все три режима просмотра. Основная цель в том, чтобы получить бесшовное переключение между режимами просмотра.
Код готового компонента можно найти на Github
Как будет устроен компонент:
Scroll View - будет выполнять Zoom и Scroll
Container View - будет делать обрезку фото в режиме редактирования и обеспечивать зону для вращения фото в режиме обрезки и поворота. Внутри ScrollView, является ViewForZooming
Image View - будет отображать изображение, обрезанное или оригинальное. Центром привязано к Container View. В режиме редактирования смещается так, чтобы видимая в Container View часть совпадала с CropZoneView. В режиме редактирования и обрезки поворачивается на заданный угол.
CropZoneView - виртуальная View для отладки. В режиме обрезки рамка обрезки совпадает с CropZoneView, но отрисовывается в глобальных координатах, чтобы ручки изменения размера были одного размера независимо от масштаба.
Какие функции будет реализовывать компонент:
Скролл с отступами и центрированием
Если изображение больше, чем ScrollView, то к изображению будут добавлены фиксированные отступы, чтобы с ним было удобнее работать и просматривать. Если размер изображения меньше, чем ScrollView, к изображению будут добавлены плавающие отступы, чтобы его центрировать.
Для этого необходимо управлять параметром ContentOffset
Расположение фото фото и поворот
Для режимов редактора фото и обрезки необходимо повернуть фотографию, для этого она будет центром привязана к контейнеру внутри ScrollView, И с помощью transform будет повернута на необходимый угол. Изображение в режиме просмотра будет работать также, только у него угол всегда равен нулю
В режиме редактора будет использоваться исходная фотография, но она будет обрезана контейнером так, чтобы края были не видны. Для этого в контейнере будет проставлен clipsToBounds, сам он будет иметь размер конечной фотографии, поэтому края будут обрезаны. В режиме обрезки контейнер будет иметь размер квадрата со стороной, равной диагонали изображения
Сохранение данных об обрезке и повороте фотографии
Обрезанная и повернутая фотография получается из оригинальной с помощью серии операций:
Сдвиг центра - сохраняется как Crop Center
Новый размер - сохраняется как Crop Size
Поворот - В приложении для детей есть ползунок с диапазоном поворота -45...45 градусов и кнопка вращения на 90 градусов. Поэтому хранятся два значения, малый угол и квадрант нуля
Все размеры хранятся в виде относительных значений. За 1 берется диагональ изображения. Сдвиг центра отсчитывается от центра изображения.
Использование Representable
Чтобы SwiftUI работал с UIKit не вдаваясь в детали, создается UIViewRepresentable или UIViewControllerReplresentable.
У меня ViewController, поэтому дальше будет про него, но для UIVIew работает точно так же. Его код можно найти на GitHub
Этот прокси объект отвечает за то, чтобы создать View или ViewController, положить его в правильное место иерархии View и обработать передачу состояния в обе стороны.
View создается в методе makeUIViewController. Необходимо вернуть настроенный ViewController
У этого объекта создаются свойства SwiftUI: @State, @Bindable и др. Изменение эти свойств приводит к вызову метода updateUIViewController, который позволяет обновить свойства у UIKit ViewController.
Для обновления данных в обратную сторону создается объект, который называется Coordinator. В нем можно разместить все Binding переменные и передать сам объект во ViewController, чтобы обновлять значения.
При изменений значений внутри ViewController, необходимо новые значения сохранить в wrappedValue Binding координатора. Тогда SwiftUI пробросит их всем подпищикам изменений
Заключение
В этой статье я рассказал о том, как решил не мучаться себя и вместо чистого SwiftUI с костылями использовал UIKit через Representable.
Все работающие примеры есть в проекте на Github, их можно скачать и запустить.
Пишите в комментариях интересующие вас темы для будущих статей, а чтобы их не пропустить - подписывайтесь на дневник.