В конце моего последнего эксперимента по Swift, редактора кривых тонов изображения , я решил пойти еще дальше и создать приложение для iPad, которое позволит пользователям создавать цепочку фильтров изображений.
Моя демонстрация связывания фильтров представляет фильтры в виде серии узлов внутри UICollectionView в нижней части экрана. Первый узел, представленный в виде круга, позволяет пользователю выбирать изображение, а последующие узлы, отображаемые в виде квадратов, позволяют пользователю выбирать фильтр и редактировать его параметры (используя мои числовые циферблаты ) или изменять его тип фильтра на средней панели. ,
В верхней части экрана находятся два изображения; слева с синей рамкой — рендер цепочки фильтров до выбранного изображения, а справа с черной рамкой — рендеринг всей цепочки фильтров.
Состояние приложения моделируется массивом экземпляров UserDefinedFilter . Они содержат тип фильтра и заданные пользователем значения параметров для фильтра. Я также использовал способность Swift перегружать операторы для создания сделанного на заказ оператора ‘==’. Поскольку каждый UserDefinedFilter имеет константу UUID, мой новый ‘==’ выглядит так:
func == (left: UserDefinedFilter, right: UserDefinedFilter) -> Bool { return left.uuid == right.uuid }
Каждый UserDefinedFilter имеет экземпляр Filter, который содержит экземпляр CIFilter . Фильтр также имеет массив структур FilterParameter . Например, фильтр «Управление цветом» содержит три экземпляра FilterParameter для насыщенности, яркости и контрастности.
Код в контроллере представления содержит три основных компонента для трех разделов: FiltersCollectionView содержит узлы фильтра, FilterParameterEditor содержит средство выбора для изменения фильтра и значений параметров, а ImagePreview содержит два изображения. «Механизм» фильтрации выполняется внутри отдельного класса FilteringDelegate .
Компонент FiltersCollectionView на самом деле представляет собой UIControl с UICollectionView, добавленным к нему в качестве подпредставления. Он действует как источник данных и делегат UICollectionView, поэтому должен реализовывать два протокола: UICollectionViewDataSource и UICollectionViewDelegate . В качестве источника данных компонент возвращает количество элементов в массиве фильтров пользователя, а в качестве делегата компонент возвращает класс, который я хочу использовать в качестве средства визуализации элементов, FiltersCollectionViewCell .
Реализация пользовательского средства визуализации элементов занимает несколько шагов: сразу после создания экземпляра я зарегистрировал класс средств визуализации:
uiCollectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: layout) uiCollectionView.registerClass(FiltersCollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
… и внутри метода collectionView () делегата для cellForItemAtIndexPath я должен определить, какой класс использовать, и вставить правильный элемент в средство визуализации:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as FiltersCollectionViewCell cell.userDefinedFilter = userDefinedFilters[indexPath.item] return cell }
FiltersCollectionViewCell расширяет UICollectionViewCell и поставляется с такими переменными, как selected . Итак, используя это с передачей нескольких свойств пользовательского фильтра, я установил цвета и форму рендера внутренне:
func updateUI() { label.textColor = selected ? UIColor.blueColor() : UIColor.lightGrayColor() backgroundColor = UIColor.whiteColor() if let userDefinedFilterConst = userDefinedFilter { layer.borderWidth = 2 layer.cornerRadius = (userDefinedFilterConst.isImageInputNode || userDefinedFilterConst.isImageOutputNode) ? frame.width / 2 : 10 layer.borderColor = userDefinedFilterConst.isImageOutputNode ? UIColor.blackColor().CGColor : selected ? UIColor.blueColor().CGColor : UIColor.lightGrayColor().CGColor } }
Когда пользователь выбирает элемент в представлении коллекции, он отправляет действие для. Событие управления ValueChanged, которое принимается контроллером представления. Это устанавливает свое собственное свойство selectedFilter, которое через наблюдателя didSet устанавливает фильтр в FilterParameterEditor .
FilterParameterEditor содержит средство выбора для изменения типа фильтра и запускает средство выбора изображения, поэтому он реализует четыре дополнительных протокола: UIPickerViewDataSource , UIPickerViewDelegate , UINavigationControllerDelegate и UIImagePickerControllerDelegate . Я бы обычно стремился к тому, чтобы у классов было меньше обязанностей, чем это, поэтому этот класс созрел для рефакторинга.
В FilterParameterEditor откликается , когда его userDefinedFilter свойства изменяются с помощью, вы уже догадались, didSet наблюдатель. Если его userDefinedFilter имеет фильтр (терминальные узлы не имеют), он создает правильное количество числовых наборов — по одному для каждого из параметров фильтра. Если значение оказывается первым узлом загрузчика изображений, он создает кнопку «Загрузить изображение».
Когда какой-либо из наборов изменяется пользователем, FilterParameterEditor отправляет действие для .ValueChanged, которое, опять же, выбирается в контроллере представления, и именно тогда начинается магия фильтрации.
Контроллер представления имеет постоянный экземпляр FilteringDelegate, который предоставляет функцию applyFilters (), которая принимает совокупность всех определенных пользователем фильтров, текущий выбранный фильтр и функцию обратного вызова, которая вызывается после выполнения фильтрации. Итак, контроллер представления вызывает эту функцию следующим образом:
filteringDelegate.applyFilters(userDefinedFilters, selectedUserDefinedFilter: selectedFilter!, imagesDidChange)
Используя библиотеку Async Тобиаса , я отправляю все это вместе со ссылкой на CIContext в фоновый поток через applyFiltersAsync () .
В двух словах, applyFiltersAsync () проходит по всем выбранным фильтрам и по всем параметрам фильтров всех этих фильтров и создает цепочку фильтров. Когда он достигает либо выбранного фильтра, либо конечного фильтра, он выполняет небольшую дополнительную работу и создает фактическую Экземпляр UIImage, который можно отобразить на экране:
if userDefinedFilter == selectedUserDefinedFilter || index == userDefinedFilters.count - 2 { let filteredImageRef = context.createCGImage(filteredImageData, fromRect: filteredImageData.extent()) let filteredImage = UIImage(CGImage: filteredImageRef) if userDefinedFilter == selectedUserDefinedFilter { selectedImage = filteredImage } if (index == userDefinedFilters.count - 2) { finalImage = filteredImage } }
Эти изображения передаются обратно в контроллер представления, обернутый в структуру FilteredImages через функцию обратного вызова, которая затем передает эти два изображения в виджет предварительного просмотра изображения, который будет отображаться:
func imagesDidChange(images: FilteredImages) { imagePreview.filteredImages = images }
Теперь у нас есть практически полезное небольшое приложение для создания сложных цепочек фильтров. Тем не менее, есть еще много возможностей для улучшения. На моем личном продукте отставание:
- Удаление циклов над массивами в пользу более функционального подхода
- Уберите реплицированный код в контроллере представления и перейдите к наблюдателям
- Реализация PHImageManager (см. Эту статью на NSHipster )
- Анимация переходов при добавлении и удалении таких компонентов, как цифровые наборы
- Использование CoreData для сохранения состояния приложения
- Добавьте мой виджет кривой тона , естественно.