В конце моего последнего эксперимента по 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 для сохранения состояния приложения
 - Добавьте мой виджет кривой тона , естественно.