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),
])
view raw pkcanvasview.swift hosted with ❤ by GitHub

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 = .clear
NSLayoutConstraint.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.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.

Share iOSDevie