PencilKit Meets MapKit in iOS 13
PencilKit was unveiled at WWDC 2019 and has finally released with iOS 13. Having covered its basics in a previous piece. Now it’s time to implement and use Pencil on Maps.
Let’s see what happens when PencilKit asks MapKit out on a date!
Goals
Drawing on Maps using PencilKit;
Displaying enclosed Map images from the drawing area in Action Sheets;
Saving the images in the Photos Library.
Quick recap
The MapKit framework is used to embed maps in our views and windows. We can do tons of stuff with the MapKit framework, like adding annotations and polylines, marking destinations and points of interest, etc.
MKMapView
is used to display and embed maps in our applications.
PencilKit is the new framework in town. Introduced with iOS 13, it allows us to create our own doodles and noodles in applications.
PKCanvasView
class is our drawing arena.
End product
Here’s what we’ll achieve by the end of this article:
It’s time to deep dive into the implementation!
Maps Under Pencil
Start by launching a new Single View Application in your Xcode.
Our first step will be to put our MKMapView
under the PKCanvasView
, so that we can draw over it!
Setting the MKMapView
It’s easy! You just need to import MapKit
and add MKMapView
in your View Controller. The following code does it without a storyboard.
var mapView = MKMapView(frame: CGRect(x: 0, y: 60, width: | |
view.frame.size.width, height: view.frame.size.height - 60)) | |
self.view.addSubview(mapView) |
Setting the PKCanvasView
let canvasView = PKCanvasView(frame: .zero) | |
canvasView.translatesAutoresizingMaskIntoConstraints = false | |
canvasView.isOpaque = false | |
view.addSubview(canvasView) | |
canvasView.backgroundColor = .clear | |
NSLayoutConstraint.activate([ | |
canvasView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 40), | |
canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor), | |
canvasView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | |
canvasView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | |
]) |
We’ve set the background color of the Canvas to transparent so that the Map underneath it is visible.
Setting the PKToolPicker
The following code adds the PencilKit ToolPicker for you.
override func viewDidAppear(_ animated: Bool) { | |
super.viewDidAppear(animated) | |
guard | |
let window = view.window, | |
let toolPicker = PKToolPicker.shared(for: window) else {return} | |
toolPicker.setVisible(true, forFirstResponder: canvasView) | |
toolPicker.addObserver(canvasView) | |
canvasView.becomeFirstResponder() | |
} |
Dragging the Map when it’s beneath the Canvas isn’t a tricky scenario. All we need to do is allow passing touches from the CanvasView to the views underneath.
So we’ll keep a toggle button which allows alternating dragging and drawing. In the first case, we pass the touches from the CanvasView and in the second case, we don’t!
We override the point function present inside the PKCanvasView
class in the extension below:
extension PKCanvasView{ | |
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { | |
return DragOrDraw.disableDrawing | |
} | |
} | |
class DragOrDraw{ | |
static var disableDrawing = true | |
} |
disableDrawing
is a boolean flag that can toggle map dragging and pencil drawing from the NavigationBar
because both can't coexist at the same time.
Setting up navigationBar
Now that the UI components are section, it’s time to convert the PencilKit drawings to Map Images.
Convert PencilKit Drawings to Map Images
In order to get the Map image from the drawn area, we need to get the bounds of the drawing and clip the MapView enclosed in that rectangle:
@objc func preview() { | |
let bounds = canvasView.drawing.bounds | |
if let image = clippedImageForRect(clipRect: bounds, inView: mapView!){ | |
showPreviewImage(image: image) | |
} | |
} |
Here’s the implementation of the functionclipImageForRect
where we’ll pass the PencilKit bounds and Map instance:
func clippedImageForRect(clipRect: CGRect, inView view: UIView) -> UIImage? { | |
UIGraphicsBeginImageContextWithOptions(clipRect.size, true, UIScreen.main.scale) | |
if let ctx = UIGraphicsGetCurrentContext(){ | |
ctx.translateBy(x: -clipRect.origin.x, y: -clipRect.origin.y); | |
view.layer.render(in: ctx) | |
let img = UIGraphicsGetImageFromCurrentImageContext() | |
UIGraphicsEndImageContext() | |
return img | |
} | |
return nil | |
} |
Now that we have the image, we can show it in an UIAlertController
with an option to add it to Photos Library:
func showPreviewImage(image: UIImage) | |
{ | |
let alert = UIAlertController(title: "Preview", message: "", preferredStyle: .actionSheet) | |
alert.addPreviewImage(image: image) | |
alert.addAction(UIAlertAction(title: "Add To Photos", style: .default){ | |
action in | |
UIImageWriteToSavedPhotosAlbum(image, self, nil, nil) | |
}) | |
alert.addAction(UIAlertAction(title: "Cancel", style: .destructive, handler: nil)) | |
present(alert, | |
animated: true, | |
completion: nil) | |
} |
Note: Don’t forget to set the Privacy Usage Permission for the Photos Library in the info.plist
.
For iPadOS, you need to use popoverPresentationController
to display action sheets as shown in the below snippet.
if let popoverPresentationController = actionSheet.popoverPresentationController { | |
popoverPresentationController.sourceView = self.view | |
popoverPresentationController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) | |
popoverPresentationController.permittedArrowDirections = [] | |
} |
The addPreviewImage
is where we embed the image in the content view of the Alert Controller by using another ViewController.
extension UIAlertController { | |
func addPreviewImage(image: UIImage) { | |
let vc = PreviewVC(image: image) | |
setValue(vc, forKey: "contentViewController") | |
} | |
} |
The code for the PreviewVC
is available with the full source code in the next section.
Conclusion
So that concludes our date with MapKit and PencilKit. The above example is handy when you need to share a part of your map with someone without taking screenshots. The source code is available in this Github Repository.