Статьи

Демо-приложение Swift Filter Chaining

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

Весь исходный код этого проекта доступен в 
моем репозитории GitHub . Наслаждайтесь!