Cropping iOS Maps With PencilKit

PencilKit and MapKit go out on a date

Anupam Chugh
Oct 20 · 4 min read
Picture by mohamed_hassan at Pixabay

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!


Introduction

Goals

  • Drawing on Maps using PencilKit;

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:

Screengrab from iOS Simulator

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

var toggleDrawItem : UIBarButtonItem!

var disableDraw : Bool = false

func setNavigationBar() {
let previewItem = UIBarButtonItem(title: "Preview", style: .done, target: self, action: #selector(preview))

let clearItem = UIBarButtonItem(title: "Clear", style: .plain, target: self, action: #selector(clear))
toggleDrawItem = UIBarButtonItem(title: "Drag", style: .plain, target: self, action: #selector(dragDrawToggler))let navigationItem = UINavigationItem(title: "")
navigationItem.rightBarButtonItems = [clearItem,previewItem]
navigationItem.leftBarButtonItem = toggleDrawItem
navigationBar = UINavigationBar(frame: .zero)
navigationBar?.isTranslucent = false

navigationBar!.setItems([navigationItem], animated: false)
navigationBar!.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(navigationBar!)
navigationBar!.backgroundColor = .clearNSLayoutConstraint.activate([
navigationBar!.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
navigationBar!.heightAnchor.constraint(equalToConstant: 60),
navigationBar!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationBar!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}

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.viewpopoverPresentationController.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.

That’s a wrap up for this piece. If you enjoyed it, here’s another PencilKit article that you may like:


Better Programming

Advice for programmers.

Anupam Chugh

Written by

I develop apps and write about them. Blogging weekly at iowncode.com

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade