Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

ChromaTouch: A 3D Touch Color Picker in Swift

DZone's Guide to

ChromaTouch: A 3D Touch Color Picker in Swift

Get to grips with implementing 3D Touch in your apps with this simple project.

· Mobile Zone
Free Resource

Launching an app doesn’t need to be daunting. Whether you’re just getting started or need a refresher on mobile app testing best practices, this guide is your resource! Brought to you in partnership with Perfecto

If you're fairly new to Swift, you may have found my last post on 3D Touch a little daunting. Here's a much smaller project that may be a little easier to follow to get up and running with force, peek, pop and preview actions. 

ChromaTouch is an HSL-based color picker where the user can set the color with three horizontal sliders or by touching over a colur swatch where horizontal movement sets hue, vertical set saturation and the force of the touch sets the lightness of the color. As the user moves their finger over the swatch, the sliders update to reflect the new HSL values.

By force-touching the sliders, the user is presented with a small preview displaying the RGB hex value of their color:


By swiping up they can set their colour to one of three presets:


And by deep pressing, they're presented with a full screen preview of their color which is dismissed with a tap:


Let's look at each part of the 3D Touch code to see how everything has been implemented.

Setting Lightness with Force 

My Swatch class is responsible for handling the user's touches in three dimensions and populating a tuple containing the three values for hue, saturation and lightness. Each touch returned in touchesMoved() contains two dimensional spatial data in its touchLocation property and the amount of force the user is exerting in its force property. 


We can normalize these values to between zero and one by dividing the positions by the bounds of the Swatch's view and the force by the maximumPossibleForce. With those normalized values, we can construct an object representing the three properties of our desired color:

    

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)
    }



If you look at my code, you'll notice I keep a variable holding the previous touch force and compare it against the current touch force. This allows me to ignore large differences which happen when the user lifts their finger to end the gesture.

Peeking

Peeking happens when the user presses down on the sliders. In my main view controller, as I create each of the three sliders, I register them for previewing with that main view controller:

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

        registerForPreviewingWithDelegate(self, sourceView: sliderWidget)
    }


My view controller needs to implement UIViewControllerPreviewingDelegate in order to tell iOS what to pop up when the user wishes to preview. In the case of ChromaTouch, it's a PeekViewController and it's defined in previewingContext(viewControllerForLocation):

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

        return peek
    }


PeekViewController is pretty basic, containing a UILabel that has its text set based on an extension I wrote to extract the RGB components from a UIColor.

Preview Actions

By swiping up on the preview, the user can set their colour to a preset. This is super simple to implement: all my PeekViewController needs to do is return an array of UIPreviewActionItem which I create based on an array of predefined colour enumerations:

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)
                })
        }
    }


Because I passed the main view controller in the constructor of PeekViewController as delegate, theupdateColor() method in the PeekViewController can pass a newly constructed HSL tuple to it based on the colour selected as a preview action:

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)
    }


Popping

The final step is popping: when the user presses deeply on the preview, the preview will vanish (this is managed by the framework) and the main view controller is shown again. However, here I want to hide the sliders and make the swatch full screen. Once the user taps, I want the screen to return to its default state.

Popping requires the second method from UIViewControllerPreviewingDelegatepreviewingContext(commitViewController). Here, I simply turn off user interaction on the swatch and hide the stack view containing the three sliders:

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


To respond to the tap to return the user interface back to default, ChromaTouchViewController reenables user interaction on the swatch and unhides the progress bars:

 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 }
    }


Conclusion

I'm beginning to really love peek and pop in the day-to-day iOS apps. As I mentioned in my previous post and as hopefully demonstrated here, it's super easy to implement.

All the code for this project is available in my GitHub repository here. Enjoy!

Just For Fun...


Finally, I couldn't resist taking my old Core Image filtering live video project and having a CIBumpFilter controlled by 3D Touch. Here, the force controls the bump filter's scale. The branch for this silliness is right here.


Keep up with the latest DevTest Jargon with the latest Mobile DevTest Dictionary. Brought to you in partnership with Perfecto.

Topics:
swift ,ios ,3d touch

Published at DZone with permission of Simon Gladman, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}