Статьи

ChromaTouch: 3D Touch Color Picker в Swift

Если вы новичок в Swift, вы, возможно, нашли  мою последнюю публикацию о 3D Touch  немного пугающей. Вот  гораздо меньший проект, за  которым, возможно, будет немного легче следовать, чтобы начать работу с принудительными, заглядывающими, всплывающими и предварительными действиями. 

ChromaTouch  — это  средство выбора цвета на основе HSL, в котором пользователь может установить цвет с помощью трех горизонтальных ползунков или прикоснувшись к образцу цвета, где горизонтальное движение задает оттенок, вертикальная установка насыщенности и сила прикосновения устанавливает яркость цвета. Когда пользователь перемещает палец по образцу, ползунки обновляются, отражая новые значения HSL.

При принудительном касании ползунков пользователю предоставляется небольшой предварительный просмотр, отображающий шестнадцатеричное значение RGB их цвета:

Проведя пальцем вверх, они могут установить свой цвет на одну из трех предустановок:

И при глубоком нажатии они получают полноэкранный предварительный просмотр своего цвета, который отклоняется одним касанием:

Давайте посмотрим на каждую часть кода 3D Touch, чтобы увидеть, как все было реализовано.

Урегулирование Легкости с Силой 

Мой  Swatch класс отвечает за обработку касаний пользователя в трех измерениях и заполнение кортежа, содержащего три значения оттенка, насыщенности и яркости. Каждое прикосновение, возвращаемое в,  touchesMoved() содержит двумерные пространственные данные в своем  touchLocation свойстве и величину силы, которую пользователь оказывает в своем  force свойстве. 

Мы можем нормализовать эти значения между нулем и единицей, разделив позиции на границы  Swatchпредставления и силу на  maximumPossibleForce. С этими нормализованными значениями мы можем построить объект, представляющий три свойства нашего желаемого цвета:

    

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)

        guard let touch = touches.first else
        {
            return
        }

        let touchLocation = touch.locationInView(self)
        let force = touch.force
        let maximumPossibleForce = touch.maximumPossibleForce

        let normalisedXPosition = touchLocation.x / frame.width
        let normalisedYPosition = touchLocation.y / frame.height
        let normalisedZPosition = force / maximumPossibleForce

        hsl = HSL(hue: normalisedXPosition,
            saturation: normalisedYPosition,
            lightness: normalisedZPosition)
    }

Если вы посмотрите на мой код, вы заметите, что я сохраняю переменную, удерживающую предыдущую силу касания, и сравниваю ее с текущей силой касания. Это позволяет мне игнорировать большие различия, которые происходят, когда пользователь поднимает палец, чтобы закончить жест.

Заглянув

Просмотр происходит, когда пользователь нажимает на ползунки. В моем  контроллере основного вида , когда я создаю каждый из трех ползунков, я регистрирую их для предварительного просмотра с помощью этого контроллера основного вида:

for sliderWidget in [hueSlider, saturationSlider, lightnessSlider]
    {
        progressBarsStackView.addArrangedSubview(sliderWidget)
        sliderWidget.addTarget(self, action: "sliderChangeHandler", forControlEvents: UIControlEvents.ValueChanged)

        registerForPreviewingWithDelegate(self, sourceView: sliderWidget)
    }

Мой контроллер представления должен реализовать  UIViewControllerPreviewingDelegate  для того, чтобы сказать iOS, что выскакивать, когда пользователь желает просмотреть. В случае ChromaTouch это —  PeekViewController и это определено в previewingContext (viewControllerForLocation):

   func previewingContext(previewingContext: UIViewControllerPreviewing,
        viewControllerForLocation location: CGPoint) -> UIViewController?
    {
        let peek = PeekViewController(hsl: hsl,
            delegate: previewingContext.delegate)

        return peek
    }

PeekViewController довольно простой, содержащий  UILabel текстовый набор, основанный на  расширении, которое я написал для извлечения компонентов RGB из  UIColor .

Предварительный просмотр действий

Проведя вверх по предварительному просмотру, пользователь может установить свой цвет на предустановку. Это очень просто реализовать: все, что мне  PeekViewController нужно сделать, это вернуть массив,  UIPreviewActionItem который я создаю на основе массива предопределенных перечислений цвета:

var previewActions: [UIPreviewActionItem]
    {
        return [ColorPresets.Red, ColorPresets.Green, ColorPresets.Blue].map
        {
            UIPreviewAction(title: $0.rawValue,
                style: UIPreviewActionStyle.Default,
                handler:
                {
                    (previewAction, viewController) in
                    (viewController as? PeekViewController)?.updateColor(previewAction)
                })
        }
    }

Поскольку я передал контроллер основного представления в конструктор  PeekViewController делегата, updateColor() метод в нем  PeekViewController может передать ему вновь созданный кортеж HSL на основе цвета, выбранного в качестве действия предварительного просмотра:

func updateColor(previewAction: UIPreviewActionItem)
    {
        guard let delegate = delegate as? ChromaTouchViewController,
            preset = ColorPresets(rawValue: previewAction.title) else
        {
            return
        }

        let hue: CGFloat

        switch preset
        {
        case .Blue:
            hue = 0.667
        case .Green:
            hue = 0.333
        case .Red:
            hue = 0
        }

        delegate.hsl = HSL(hue: hue, saturation: 1, lightness: 1)
    }

Выскакивают

Последний шаг — всплывающий: когда пользователь сильно нажимает на предварительный просмотр, предварительный просмотр исчезает (это управляется платформой), и контроллер основного представления снова отображается. Однако здесь я хочу скрыть ползунки и сделать палитру полноэкранным. Как только пользователь нажмет, я хочу, чтобы экран вернулся в состояние по умолчанию.

Выталкивание требует второго метода из  UIViewControllerPreviewingDelegatepreviewingContext(commitViewController). Здесь я просто отключаю взаимодействие пользователя с образцом и скрываю представление стека, содержащее три ползунка:

 func previewingContext(previewingContext: UIViewControllerPreviewing,
        commitViewController viewControllerToCommit: UIViewController)
    {
        swatch.userInteractionEnabled = false
        progressBarsStackView.hidden = true
    }

Чтобы отреагировать на нажатие и вернуть пользовательский интерфейс в ChromaTouchViewController исходное состояние по умолчанию,  включите взаимодействие с пользователем в образце и отобразите индикаторы выполнения:

 override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesBegan(touches, withEvent: event)

        swatch.userInteractionEnabled = true

        UIView.animateWithDuration(0.25){ self.progressBarsStackView.hidden = false }
    }

Вывод

Я начинаю действительно любить заглядывать и вставлять в повседневные приложения для iOS. Как я упоминал в моем  предыдущем посте  и, как мы надеемся, продемонстрировал здесь, это очень легко реализовать.

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

Просто для удовольствия …

Наконец, я не удержался, взяв свой старый проект Core Image с фильтрацией живого видео и  CIBumpFilter контролируемый 3D Touch. Здесь сила управляет масштабом фильтра бампера. Ветвь этой глупости прямо здесь .