iOS PencilKit Meets MapKit

After discussing PencilKit and using it with Core ML, it’s time for Pencil on Maps. Let’s see what happens when PencilKit asks out MapKit for a date!

Our Goal

  • 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 the class used to display a map on the screen similar to the Apple Maps.

PencilKit is the new framework in town. Introduced with iOS 13 it allows us to create our own doodles and noodles in third party applications. PKCanvasView class is our drawing arena.

Without wasting any more time, let’s see how the date ends between these two cool frameworks!

Our Final Destination

Here’s what we’ll achieve by the end of this article.

ios-pencilkit-mapkit-demo

It’s time to deep dive into the implementation! Launch a new Single View Application in your Xcode.

Maps Under Pencil

To start with, we need 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()
    }

How can I drag the Map when it’s beneath the Canvas?

This 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 Navigation Bar because both can’t coexist.

ios-mapkit-pencilkit-meme

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 clipImageForRect function where we 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 Alert Controller 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.

The addPreviewImage is where we embed the image in the content view of the Alert Controller by using another View Controller.

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 at the end of this piece.

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 full source code is available in this repository.

That’s a wrap up. Hope you enjoyed.

Leave a Reply

Your email address will not be published. Required fields are marked *