We’re going to do one more cool thing with our DrawingPad. Drawing programs often allow you to long press on a color and choose it as your drawing color.
In this example, we’re long pressing on a blue area and a blue circle pops up over the long press location, magnifying the blue pixel underneath.
We’re going to go really low level into Core Graphics here.
A bitmap image is just a two dimensional array of pixels. It has rows and columns.
We’ll use the touch location as the index into the drawing image’s pixel data to get the color at that pixel.
A UIImage is an object that represents image data.
Core Graphics has its own object type that represents image data, and thats a CGImage. Each UIImage has a cgImage property as a bridge to Core Graphics, so you can always get a handle to the Core Graphics image data.
A CGImage has a CGDataProvider property which manages the underlying source data so that you don’t need to know where that data’s come from.
The CGDataProvider has a data property which holds the pixel data as a CFData type. This data is a buffer of bytes containing the pixel data, and you can get a pointer to the start of the buffer with CFDataGetBytePtr.
So with all that knowledge, we’ll be able to write a method that’s an extension on UIImage that takes in a CGPoint location and returns a UIColor.
DrawingColorView.swift
The starter project for this video has a pre-built UIView class in DrawingColorView.swift. This will be the round view that magnifies the pixel color at the long press location.
Open this file and take a look at this very simple view.
When you’re setting borders and shadows for UIView, you have to do it on the view’s Core Animation layer. SwiftUI has done away with all that, and in SwiftUI we work directly with Views. But here we’re using a UIView, so we change the view’s layer properties.
The view is 50 points wide and 50 points high, so it’ll be quite small. It’ll be round because the corner radius is half the width. And it’ll have a border and a shadow to make it stand out from the background.
When we first initialize CanvasView, we’ll initialize this view, and show and hide it as necessary using the show and hide methods.
If you’re using your own project, make sure to copy DrawingColorView.swift from the starter project into your project. Now to create our color picking method.
DrawingPadExtensions.swift
Create a new Swift file called DrawingPadExtensions.swift. Import UIKit
import UIKit
and create a UIImage extension with the new method to extract a color from the image at a given location.
extension UIImage {
func getColor(at location: CGPoint) -> UIColor? {
}
}
A UIImage has an optional cgImage property, that’s the underlying Core Graphics image data, so we’ll check that there is one.
guard let cgImage = cgImage,
A CGImage has an optional dataProvider that looks after the source of the data,
let dataProvider = cgImage.dataProvider,
A dataProvider has optional pixel data.
let pixelData = dataProvider.data
else {
return nil
}
If any of these fail, we’ll return nil, as we’re unable to read the bitmap associated with the UIImage. Now we’ll work out the location of the pixel within the bitmap.
let scale = UIScreen.main.scale
let pixelLocation = CGPoint(x: location.x * scale,
y: location.y * scale)
A retina screen has a 2 by scale, and the larger iPhones have a 3 by scale. We’ve been working in points, where an iPhone 11 might have a width of 414 points. At a 2 by scale, the underlying image backing the display will have 828 pixels. So we workout the pixel location from the point location in the correct scale. Let’s make sure that the pixel we’re addressing isn’t off the end of the data.
let pixel = cgImage.bytesPerRow * Int(pixelLocation.y) +
cgImage.bitsPerPixel / 8 * Int(pixelLocation.x)
guard pixel < CFDataGetLength(pixelData) else {
print("WARNING: mismatch of pixel data")
return nil
}
Bitmaps come in all sorts of formats. Our image should only have 4 bytes per pixel - that’s a byte for each of red, green, blue and alpha.
We multiply the y position of the pixel location by the number of bytes per row in the cgimage and add the number of bytes in the x position.
Then we get the length of the pixelData bitmap array and check that the pixel is within that array. If it’s not, we can’t read the bitmap and we return nil.
Now we can read the rgb values using a pointer. Set up the pointer to point at the start of the bitmap array
guard let pointer = CFDataGetBytePtr(pixelData) else {
return nil
}
The rgb values in the pixel data are from 0 to 255. An rgb value of 255, 0, 0 would be red. We need color values from 0 to 1, so create an inline function to do this conversion.
func convert(_ color: UInt8) -> CGFloat {
CGFloat(color) / 255.0
}
Use the pixel position to get the position of each pixel in the bitmap array.
let red = convert(pointer[pixel])
let green = convert(pointer[pixel + 1])
let blue = convert(pointer[pixel + 2])
let alpha = convert(pointer[pixel + 3])
Now we can use these values to return a UIColor.
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
We’ve written a pretty nifty method to return a color at a location. Now we have to work out how to use it. When we long press on the drawing image, we’re going to show the DrawingColorView that magnifies the color underneath the touch.
CanvasView.swift
Open CanvasView.swift. Create a new property to hold a DrawingColorView.
private let drawingColorView = DrawingColorView()
Create a new long press gesture method which will be the one in which we show and hide this drawingColorView.
@objc func handleLongPress(gesture: UILongPressGestureRecognizer) {
guard let drawingImage = drawingImage else { return }
}
We’ll use our getColor method to get the color at the gesture’s location.
let location = gesture.location(in: self)
guard let color = drawingImage.getColor(at: location) else {
return
}
Now we can check the gesture’s state. When the gesture begins, we should show the magnifier view and when the gesture ends we should both hide it and set the current drawing color temporarily to the magnified color.
switch gesture.state {
case .began:
drawingColorView.show(color: color, location: location)
case .ended:
drawingColorView.hide()
self.color = color
default: break
}
color will only temporarily change, because after the following draw stroke, DrawingPadRepresentation will override the color with the Color Picker selection in its updateUIView method.
This is all our gesture has to do, so let’s add the gesture to the view in init, after the call to super.
let longPress = UILongPressGestureRecognizer(target: self,
action: #selector(handleLongPress))
addGestureRecognizer(longPress)
There’s one more thing to do. We created a property called DrawingColorView, but we haven’t added it to the view hierarchy yet. So let’s do that in init, before adding the long press.
addSubview(drawingColorView)
Compile the code, open ContentView.swift, and check out the preview. When I long press, it shows the magnifier view and for one stroke changes the drawing color.
This doesn’t work great in the preview, so I recommend trying it out in the simulator or on device. We can even pick up the white color from the background and use it as an eraser. Thanks for watching this course on Kodeco. I’ll see you again soon.