SwiftUI is a great thing to do for UI layout; However, if the app is related to maps, then it is not so comfortable, because SwiftUI does not have controls to encapsulate maps! If we need to use a map, then we have to do it ourselves, and MKMapView wrapped in! Ok, so let’s see now how to encapsulate it!

Basic usage

Encapsulation MKMapKit

UIViewRepresentable for UIKit components that we use in SwiftUI, we only need to meet the UIViewRepresentable protocol when encapsulating them, which implements makeUIView(Context 🙂 and updateUIView(_: , context:) function. Isn’t that easy? Based on this, we can simply wrap MKMapView:

import MapKit
import SwiftUI

struct MapViewWrapper: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: .zero)
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context){}}Copy the code

When used, there is no difference from normal SwiftUI components such as:

import SwiftUI

struct ContentView: View {
    var body: some View {
        MapViewWrapper()}}Copy the code

Run the program and we’ll see something exciting:

Set up the Frame

If you look closely at the code, you’ll see that we set frame to 0 earlier, as follows:

let mapView = MKMapView(frame: .zero)
Copy the code

In practice, it fits automatically, but it always feels a bit lumpy, like a stick in your throat. So how do we get the size of MKMapView? This is where the GeometryReader comes in.

We create a new MapView structure and wrap the MapViewWrapper with an GeometryReader and pass the size of the parent window to the MapViewWrapper when initialized, as in:

struct MapView: View {
    var body: some View {
        return GeometryReader { geometryProxy in
            MapViewWrapper(frame: CGRect(x: geometryProxy.safeAreaInsets.leading,
                                         y: geometryProxy.safeAreaInsets.trailing,
                                         width: geometryProxy.size.width,
                                         height: geometryProxy.size.height))
        }
    }
}

struct MapViewWrapper: UIViewRepresentable {
    var frame: CGRect

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: frame)
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context){}}Copy the code

Change MapViewWrapper to MapView:

struct ContentView: View {
    var body: some View {
        MapView()}}Copy the code

Encapsulate all UIKit components first and then put them into a struct View. The problem, however, is that, in practice, if this encapsulation is used, it is possible to use the @ObservableObject property without being notified of the change when the data changes. So this is just an illustration of how to get a frame, but in the following example we will remove the MapViewWrapper from the MapView.

Monitor the scaling scale

If we use both hands to scale the map, how do we know the scale multiple? This is where the upper proxy comes in. The code is a class that implements the MKMapViewDelegate protocol. We’re also creating a new class called MapViewState to save the data. The reason for this consideration is the separation of data and state, based on the principles of design patterns.

The first is the MapViewState class, which is relatively simple and has only one property:

import MapKit

class MapViewState: ObservableObject {
    var span: MKCoordinateSpan?
}

Copy the code

Then there is the MapViewDelegate class, which needs to implement the MKMapViewDelegate protocol, and in order to detect scaled events, it must also implement mapView(_ mapView: MKMapView, regionDidChangeAnimated: Bool) function:

import MapKit

class MapViewDelegate: NSObject.MKMapViewDelegate {
    var mapViewState : MapViewState
    
    init(mapViewState : MapViewState) {self.mapViewState = mapViewState
    }
    
    func mapView(_ mapView: MKMapView, regionDidChangeAnimated: Bool) {
        mapViewState.span = mapView.region.span
        print(mapViewState.span)
    }
}

Copy the code

Second, assign an instance of MapViewDelegate to the MapView:

struct MapView: View {
    var mapViewState: MapViewState
    var mapViewDelegate: MapViewDelegate

    var body: some View {
        return GeometryReader { geometryProxy in
            MapViewWrapper(frame: CGRect(x: geometryProxy.safeAreaInsets.leading,
                                         y: geometryProxy.safeAreaInsets.trailing,
                                         width: geometryProxy.size.width,
                                         height: geometryProxy.size.height),
                           mapViewState: self.mapViewState,
                           mapViewDelegate: self.mapViewDelegate)
        }
    }
}

struct MapViewWrapper: UIViewRepresentable {
    var frame: CGRect
    var mapViewState: MapViewState
    var mapViewDelegate: MapViewDelegate

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: frame)
        mapView.delegate = mapViewDelegate
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context){}}Copy the code

Finally, the way the call is called also needs to be changed slightly:

struct ContentView: View {
    var mapViewState: MapViewState?
    var mapViewDelegate: MapViewDelegate?

    init() {
        mapViewState = MapViewState()
        mapViewDelegate = MapViewDelegate(mapViewState: mapViewState!)
    }

    var body: some View {
        ZStack {
            MapView(mapViewState: mapViewState! , mapViewDelegate: mapViewDelegate!) }}}Copy the code

Run the program, and you should be able to output the scaled span through the debug window.

Finally, to summarize the main points:

  • MapView feedback is all about calling notifications back and forth via MKMapViewDelegate
  • An instance of MKMapViewDelegate is implemented by assigning a value to mapView.delegate
  • When scaling, the mapView(_ mapView: MKMapView, regionDidChangeAnimated: Bool) function is called

Sets the center point for the display

Let’s consider a common application scenario. Many mapping apps have a feature that when you click the “current location” button, the map zooms to your current location. This scenario is also very easy to implement.

First, we need to add a center property to MapViewState to store the location variable:

import MapKit

class MapViewState: ObservableObject {
    var span: MKCoordinateSpan?
    @Published var center: CLLocationCoordinate2D?
}

Copy the code

The main reason why MapViewState is declared as ObservableObject, and why center is wrapped with @published, is that our data needs to be able to notify the SwiftUI component when it changes.

Let’s look at what needs to happen to the MapView code:

import MapKit
import SwiftUI

struct MapView: View {@ObservedObject var mapViewState: MapViewState
    var mapViewDelegate: MapViewDelegate

    var body: some View {
        return GeometryReader { geometryProxy in
            MapViewWrapper(frame: CGRect(x: geometryProxy.safeAreaInsets.leading,
                                         y: geometryProxy.safeAreaInsets.trailing,
                                         width: geometryProxy.size.width,
                                         height: geometryProxy.size.height),
                           mapViewState: self.mapViewState,
                           mapViewDelegate: self.mapViewDelegate)
        }
    }
}

struct MapViewWrapper: UIViewRepresentable {
    var frame: CGRect
    @ObservedObject var mapViewState: MapViewState
    var mapViewDelegate: MapViewDelegate

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: frame)
        mapView.delegate = mapViewDelegate
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        // Set the map display region
        if let center = mapViewState.center {
            var region: MKCoordinateRegion
            if let span = mapViewState.span {
                region = MKCoordinateRegion(center: center,
                                            span: span)
            } else {
                region = MKCoordinateRegion(center: center,
                                            latitudinalMeters: CLLocationDistance(400),
                                            longitudinalMeters: CLLocationDistance(400))
            }
            view.setRegion(region, animated: true)

            mapViewState.center = nil}}}Copy the code

The above code has the following caveats:

  • Setting the display center is done by calling setRegion
  • LatitudinalMeters and longitudinalMeters of setRegion are used to control the scaling ratio
  • SetRegion’s span also controls scaling, but in different units
  • Once set, mapViewState.center is set to nil, mainly to prevent center points from being set repeatedly while refreshing

Next, we need to add a button to the ContentView that sets the center position when the button is pressed. The code isn’t complicated, just a bit too much to add:

import MapKit
import SwiftUI

struct ContentView: View {@ObservedObject var mapViewState = MapViewState(a)var mapViewDelegate: MapViewDelegate?

    init() {
        mapViewDelegate = MapViewDelegate(mapViewState: self.mapViewState)
    }

    var body: some View {
        ZStack {
            MapView(mapViewState: mapViewState, mapViewDelegate: mapViewDelegate!)
            
             VStack {
                 Spacer(a)Button(action: {
                     self.mapViewState.center = CLLocationCoordinate2D(latitude: 39.9, longitude: 116.38) {})Text("MyLocation")
                         .background(Color.gray)
                         .padding()
                 }
             }
             
        }
    }
}
Copy the code

If you run this code full of information at this point, it’s frustrating to find that clicking a button doesn’t get you back to where you are anyway. Why is that? This was mentioned earlier in the “Set Frame” section. After multiple layers of encapsulation, it is possible that @observedobject cannot receive changes. So again, we’re going to simplify this two-level encapsulation into one layer.

If you need the failed code, use git to do the following:

Git clone github.com/no-rains/Ma…

git checkout base.use-bad.wrapper

Since we don’t need frame, we’ll just use.zero. Then we rename MapViewWrapper to MapView and delete the original MapView, so we get the following code:

//
// MapView.swift
// MapViewGuider
//
// Created by norains on 2020/2/26.
// Copyright © 2020 Norains. All rights reserved
//

import MapKit
import SwiftUI

struct MapView: UIViewRepresentable {@ObservedObject var mapViewState: MapViewState
    var mapViewDelegate: MapViewDelegate

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: .zero)
        mapView.delegate = mapViewDelegate
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        // Set the map display region
        if let center = mapViewState.center {
            var region: MKCoordinateRegion
            if let span = mapViewState.span {
                region = MKCoordinateRegion(center: center,
                                            span: span)
            } else {
                region = MKCoordinateRegion(center: center,
                                            latitudinalMeters: CLLocationDistance(400),
                                            longitudinalMeters: CLLocationDistance(400))
            }
            view.setRegion(region, animated: true)

            mapViewState.center = nil}}}Copy the code

Run the code at this time, and then click the button, it will automatically move to the current set of coordinates to go!

This concludes the chapter. If you need the code at the end of this chapter, do as follows:

Git clone github.com/no-rains/Ma…

git checkout base.use

There is also a small tail. If you want to display the small circle that flashes when moving to the current location, just set the showsUserLocation of the mapView to true.

A pin

Pin in the map of the application, is mainly to let the user know that there are some customized information, click when you can obtain, such as the current business information ah, the current location of the picture and so on.

Add pins

The method of adding pins is relatively simple, generally speaking, there are several steps:

  1. Create a class that implements the MKAnnotation protocol, and let’s say the name of the class is PinAnnotation
  2. Create an instance of PinAnnotation
  3. Add an instance of PinAnnotation to the map using the addAnnotation function of MKMapView

Let’s take a look at it step by step, starting with the class that implements the MKAnnotation protocol. In this class, we’re basically implementing this property coordinate. So what does this coordinate property do? It basically tells you where to put the pin. Given this, it’s not hard to get a very simple PinAnnotation:

import MapKit

class PinAnnotation: NSObject.MKAnnotation {
    var coordinate: CLLocationCoordinate2D

    init(coordinate: CLLocationCoordinate2D) {
        self.coordinate = coordinate
    }
}

Copy the code

So back to our project, where would it be good to put this instance of PinAnnotation? It’s still in MapViewState:

class MapViewState: ObservableObject {...var pinAnnotation = PinAnnotation(coordinate: CLLocationCoordinate2D(latitude: 39.9, longitude: 116.38))}Copy the code

Then, what we need to do is add the pin to the makeUIView function:

func makeUIView(context: Context) -> MKMapView{... mapView.addAnnotation(mapViewState.pinAnnotation) ... }Copy the code

When run, the result should look like this:

Use a pin image consistent with the system’s own map

If you look carefully, you will find that the pins used in the previous section are not quite the same as the pins used in the system’s built-in map. So what if you want to use a pin image that matches the system’s built-in map? The steps are as follows:

  1. MapView (_ mapView: MKMapView, viewFor Annotation: MKAnnotation) -> MKAnnotationView? function
  2. When this function is called, an AnnotationView instance with an identifier of “MKPinAnnotationView” is created
  3. In the AnnotationView instance, we’re going to assign to it the PinAnnotation that we created

To simplify, we can add the following code:

class MapViewDelegate: NSObject.MKMapViewDelegate {...func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        // If the return value of MKAnnotationView is nil, it would be the default
        var annotationView: MKAnnotationView?
        
        let identifier = "MKPinAnnotationView"
        annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
        if annotationView == nil {
            annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier) } annotationView? .annotation = annotationreturn annotationView
    }
}
Copy the code

After running, the result should look like this:

Pop up the accessory box

Now we’re going to do an interesting thing, which is to click on the big head on the map and make it pop up a little widget that has text on it. And to do that, I need to go to mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotation view, okay? Function to do a little bit of stuff, and those things, we’re just going to encapsulate PinAnnotation.

class PinAnnotation: NSObject.MKAnnotation {...func makeTextAccessoryView(annotationView: MKPinAnnotationView) {
        var accessoryView: UIView

        // Create an accessory view for the text
        let textView = UITextView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
        textView.text = "Hello, PinAnnotation!"
        textView.isEditable = false
        accessoryView = textView

        // Set the constraints for text alignment
        let widthConstraint = NSLayoutConstraint(item: accessoryView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100)
        accessoryView.addConstraint(widthConstraint)
        let heightConstraint = NSLayoutConstraint(item: accessoryView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100)
        accessoryView.addConstraint(heightConstraint)
        
        // The attached view will be created and assigned
        annotationView.detailCalloutAccessoryView = accessoryView
        
        // Make the accessory view visible
        annotationView.canShowCallout = true}}Copy the code

The code is relatively simple, read the comments to get the general idea. In general, there are two:

  • MKPinAnnotationView. CanShowCallout variable is used to control the click, whether the accessory box
  • MKPinAnnotationView. DetailCalloutAccessoryView accessory box is used to display content

After the code runs, it looks like this:

If need to display images, is also very simple, is the place where the code statement UITextView, replacement for UIImage, then assigned to MKPinAnnotationView. DetailCalloutAccessoryView can. The principle is the same, and there is nothing more to be said, so I won’t go into detail here.

Switch to a different screen

Click the pin, then click the exclamation mark in the accessory box to navigate to another page. This scenario, I think, is fairly common. However, since we are using SwiftUI and MKMapView is UIKit, it is actually a little difficult to coordinate the two in the page switching area. All things are difficult before they are easy. Let’s take it one step at a time.

The first thing we do is display an exclamation mark on the attached page of the pin, and when clicked, a function is executed. This part of the code is relatively simple, only three sentences, as follows:

class PinAnnotation: NSObject.MKAnnotation {...// Click the exclamation point callback function
    @objc func onClickDetailButton(_ sender: Any, forEvent event: UIEvent) {
        print("onClickDetailButton")}func makeTextAccessoryView(annotationView: MKPinAnnotationView){...// The exclamation button
        let detailButton = UIButton(type: .detailDisclosure)
        
        // Clicking the exclamation mark calls the onClickDetailButton function passed in
        detailButton.addTarget(self, action: #selector(PinAnnotation.onClickDetailButton(_:forEvent:)), for: UIControl.Event.touchUpInside)
        
        // Assign the exclamation point button to the view
        annotationView.rightCalloutAccessoryView = detailButton
    }
}
Copy the code

After running, the interface looks like the following:

The next challenge is, how do we switch interfaces? Let’s start with the basic architecture of SwiftUI navigation:

NavigationView {
	NavigationLink(destination: XXX, isActive: $YYY) {... }}Copy the code

This is just some pseudo-code, but here’s what you need to know:

  • NavigationView is a navigational View, so you can use the entire APP in one place, as long as all the other views and their children are in scope
  • NavigationLink is mainly used to switch between pages and must be within the scope of NavigationView to be valid
  • Destination is the instance of the page to switch
  • IsActive is used to control switching, and it switches when it is true

Based on the above knowledge, let’s make the following code modifications:

  1. MapViewState adds a navigateView variable to hold an instance of the interface to navigate
  2. MapViewState adds an activeNavigate variable to control page switching

So, our code for MapViewState looks like this:

class MapViewState: ObservableObject {...var navigateView: SecondContentView?
    @Published var activeNavigate = false. }Copy the code

Accordingly, when the exclamation mark is clicked, we must assign values to these two variables:

class PinAnnotation: NSObject.MKAnnotation {...@objc func onClickDetailButton(_ sender: Any, forEvent event: UIEvent) {
        mapViewState.navigateView = SecondContentView()
        mapViewState.activeNavigate = true}}Copy the code

The final step is to insert these two variables into the UI component:

struct ContentView: View {@ObservedObject var mapViewState = MapViewState()
    ...

    var body: some View {
        NavigationView {
            ZStack {
                MapView(mapViewState: mapViewState, mapViewDelegate: mapViewDelegate!)
                    .edgesIgnoringSafeArea(.all)

                ...

                    ifmapViewState.navigateView ! =nil {
                        NavigationLink(destination: mapViewState.navigateView! , isActive: $mapViewState.activeNavigate) {EmptyView()}}}}}}}Copy the code

With that added, we now click on the exclamation mark to navigate to another page!

If you need the code for this phase, do as follows:

Git clone github.com/no-rains/Ma…

git checkout annotation

Fog and Track

If you’ve ever used something like Fogworld, you know there’s a very interesting scene in it, where you slowly explain the map as you walk. We will discuss this in this chapter, but we won’t cover how to capture and save GPS data, just how to draw it.

Draw the trajectory

There are two common ways to draw tracks. One is to use MKPolylineRenderer by proxy, and the other is to directly add a layer of CALayer on top of the MapView. Comparatively speaking, the former is relatively simple and easy to understand, while the latter is more complicated and troublesome. Both are covered in this chapter, but the rest is based on the latter CALayer approach.

But either way, the first thing to do is add tracks to the map. The way to add tracks is simply to generate a special overlay called MKPolyline based on coordinates and add it to the MapView view:

struct MapView: UIViewRepresentable {

    func makeUIView(context: Context) -> MKMapView{...// Add trace
        let polyline = MKPolyline(coordinates: mapViewState.tracks, count: mapViewState.tracks.count) mapView.addOverlay(polyline) ... }}Copy the code

Tracks defined in MapViewState are just one piece of data, such as:

class MapViewState: ObservableObject {...var tracks = [CLLocationCoordinate2D(latitude: 39.9, longitude: 116.38),
                  CLLocationCoordinate2D(latitude: 39.9, longitude: 116.39)]}Copy the code

Now that the trace is added, let’s see how to draw it.

MKPolylineRenderer way

MKPolylineRenderer method is relatively simple, the steps are as follows:

  1. Create a subclass derived from MKPolylineRenderer and set the color and size of the drawn trace in the draw function of that subclass
  2. The subclass object is fed back to the view in the MKMapViewDelegate callback function

Let’s start by subclassing MKPolylineRenderer:

import Foundation
import MapKit

class PolylineRenderer: MKPolylineRenderer {
    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        // Line color
        strokeColor = UIColor.red
        // The size of the line
        lineWidth = 5
        super.draw(mapRect, zoomScale: zoomScale, in: context)
    }
}

Copy the code

The PolylineRenderer code does two things, setting the color and size of the line in the callback function. Next, let’s look at how to feed this subclass’s object back to the view in the MKMapViewDelegate callback:

class MapViewDelegate: NSObject.MKMapViewDelegate {...// This function is called back when renderer is created
    func mapView(_ mapView: MKMapView, rendererFor: MKOverlay) -> MKOverlayRenderer {
        let renderer = PolylineRenderer(overlay: rendererFor)
        return renderer
    }
}
Copy the code

Creating the PolylineRenderer in the callback function and returning it is the main thing to do.

Run the code and see the effect:

If you need the code for this phase, do as follows:

Git clone github.com/no-rains/Ma…

git checkout polyline.renderer

However, MKPolylineRenderer is relatively simple, but it is more troublesome to customize some functions. MKPolylineRenderer might be enough to just show the tracks, but if more work is needed, then maybe CALayer’s approach is needed.

CALayer way

It’s a little bit complicated for The CALayer model, so let’s go through it step by step. First, since CALayer needs CADisplayLink, let’s take a look at what it does.

CADisplayLink is officially defined as follows:

A timer object that allows your application to synchronize its drawing to the refresh rate of the display.

CADisplayLink is a timer object that allows you to refresh your view at the same rate as the screen. To simplify, think of CADisplayLink as a timer that synchronizes the frequency of screen refreshes.

In the following example, we will use the following aspects:

  1. Call CADisplayLink’s constructor and associate it with a function that is called at a certain time
  2. Implements a function that is called on a timer

# 1 is simple, as follows:

let link = CADisplayLink(target: self, selector: #selector(self.updateDisplayLink))
Copy the code

The prototype for the updateDisplayLink passed in selector looks like this:

@objc func updateDisplayLink(a){... }Copy the code

So the next thing we need to think about is, what do we need to implement in our updateDisplayLink function? Because the map is constantly moving, the track that’s attached to it is not fixed, so we need to get the position of the track on CALayer in the updateDisplayLink function and save it as a UIBezierPath curve, Draw it when CALayer calls the Draw function.

So let’s first think about how to get the trajectory line. First of all, we know that we can get the overlay from lays using mkmapView. overlays, and then we can use the as operator to determine if we have added the MKPolyline to the track:

for overlay inmapView! .overlays {if let overlay = overlay as? MKPolyline{... }}Copy the code

Mkmapview. convert converts the GPS coordinates to CALayer’s UI coordinates relative to MKMapView:

var points = [CGPoint] ()for mapPoint in UnsafeBufferPointer(start: overlay.points(), count: overlay.pointCount){
    let coordinate = mapPoint.coordinate
    letpoint = mapView! .convert(coordinate, toPointTo: mapView!) points.append(point) }Copy the code

Finally, bezier curves can be drawn from these coordinate points:

let path = UIBezierPath(a)if let first = points.first {
    path.move(to: first)
}
for point in points {
    path.addLine(to: point)
}
for point in points.reversed() {
    path.addLine(to: point)
}
path.close()
Copy the code

This completes CADisplayLink’s mission. But at this point, we’ve just sketched out the shape of the curve, and we need to show the shape, and that’s where CALayer comes in.

CALayer’s task is much simpler. It simply implements a draw function and draws the stored Bezier curve:

override func draw(in ctx: CGContext) {
    UIGraphicsPushContext(ctx)
    ctx.setStrokeColor(UIColor.red.cgColor) path? .lineWidth =5path? .stroke() path? .fill()UIGraphicsPopContext()}Copy the code

We pooled the above code into a class, so we have a class named FogLayer:

import MapKit
import UIKit

class FogLayer: CALayer {
    var mapView: MKMapView?
    var path: UIBezierPath?

    lazy var displayLink: CADisplayLink = {
        let link = CADisplayLink(target: self, selector: #selector(self.updateDisplayLink))
        return link
    }()


    override func draw(in ctx: CGContext) {
        UIGraphicsPushContext(ctx)
        ctx.setStrokeColor(UIColor.red.cgColor) path? .lineWidth =5path? .stroke() path? .fill()UIGraphicsPopContext()}@objc func updateDisplayLink(a) {
        if mapView == nil {
            // Do nothing
            return
        }

        let path = UIBezierPath(a)for overlay inmapView! .overlays {if let overlay = overlay as? MKPolyline {
                if let linePath = self.linePath(with: overlay) {
                    path.append(linePath)
                }
            }
        }

        path.lineJoinStyle = .round
        path.lineCapStyle = .round

        self.path = path
        setNeedsDisplay()
    }

 

    private func linePath(with overlay: MKPolyline) -> UIBezierPath? {
        if mapView == nil {
            return nil
        }

        let path = UIBezierPath(a)var points = [CGPoint] ()for mapPoint in UnsafeBufferPointer(start: overlay.points(), count: overlay.pointCount) {
            let coordinate = mapPoint.coordinate
            letpoint = mapView! .convert(coordinate, toPointTo: mapView!) points.append(point) }if let first = points.first {
            path.move(to: first)
        }
        for point in points {
            path.addLine(to: point)
        }
        for point in points.reversed() {
            path.addLine(to: point)
        }

        path.close()

        return path
    }
}
Copy the code

So where does this FogLayer object go? Naturally, it’s still in MapViewState:

class MapViewState: ObservableObject {...var fogLayer = FogLayer()}Copy the code

When FogLayer is associated with MKMapView, it’s still in a makeUIView:

struct MapView: UIViewRepresentable {...func makeUIView(context: Context) -> MKMapView{.../ / add SubLayer
        mapView.layer.addSublayer(mapViewState.fogLayer)
        mapViewState.fogLayer.mapView = mapView
        mapViewState.fogLayer.frame = UIScreen.main.bounds
        mapViewState.fogLayer.displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common)
        mapViewState.fogLayer.setNeedsDisplay()

        return mapView
    }
}
Copy the code

The caveat here is that CADisplay must be added to the RunLoop queue, otherwise it will not function as a timer.

Finally, run the code as shown with MKPolylineRenderer, as in:

If you need the code for this phase, do as follows:

Git clone github.com/no-rains/Ma…

git checkout be0c0f28101df0c548a40f8bd53a5d8265657d36

Draw the fog

For friends who have used the world fog, they may be curious about the function that the map inside is covered by fog and only the track through is clear. How is it realized? The principle is very simple, just draw a layer of gray on CALayer, set the line to transparent, and then draw. So, we can modify CALayer’s draw function here:

class FogLayer: CALayer {...override func draw(in ctx: CGContext) {
        UIGraphicsPushContext(ctx)
        UIColor.darkGray.withAlphaComponent(0.75).setFill()
        UIColor.clear.setStroke()
        ctx.fill(UIScreen.main.bounds) ctx.setBlendMode(.clear) path? .lineWidth =5path? .stroke() path? .fill()UIGraphicsPopContext()}}Copy the code

The effect is as follows:

Dynamically change the track width

Let’s consider another problem. Assuming that we need the track to cover Chang ‘an Avenue, how can we dynamically set the width of the track when the local map is zoomed in and out? Mkmapview. convert function to convert THE GPS coordinates on the map to the UI coordinates on the View, and then we know that the width of Changan Avenue is about 50 meters, so we can select two GPS coordinates of the distance is about 50 meters. And then every time you draw, convert these two points to UI points, and then calculate the distance between these two UI points as the width of the track? In fact, it works.

Based on this, we choose the following two test coordinates:

let mapPoint1 = CLLocationCoordinate2D(latitude: 22.629052, longitude: 114.136977)
let mapPoint2 = CLLocationCoordinate2D(latitude: 22.629519, longitude: 114.137098)
Copy the code

How can we be sure that the two GPS coordinates are about 50 or so apart? This can be determined by calling the following function:

func coordinateDistance(_ first: CLLocationCoordinate2D, _ second: CLLocationCoordinate2D) -> Int {
        func radian(_ value: Double) -> Double {
            return value * Double.pi / 180.0
        }

        let EARTH_RADIUS: Double = 6378137.0

        let radLat1: Double = radian(first.latitude)
        let radLat2: Double = radian(second.latitude)

        let radLng1: Double = radian(first.longitude)
        let radLng2: Double = radian(second.longitude)

        let a: Double = radLat1 - radLat2
        let b: Double = radLng1 - radLng2

        var distance: Double = 2 * asin(sqrt(pow(sin(a / 2), 2) + cos(radLat1) * cos(radLat2) * pow(sin(b / 2), 2)))
        distance = distance * EARTH_RADIUS
        return Int(distance)}Copy the code

Because this function is designed to the latitude and longitude of some knowledge and algorithm, so I will not expand here, just need to know the two GPS coordinates, you can calculate the distance between the two.

To get back to business, let’s convert two GPS coordinates to UI coordinates:

let viewPoint1 = mapView.convert(mapPoint1, toPointTo: mapView)
let viewPoint2 = mapView.convert(mapPoint2, toPointTo: mapView)
Copy the code

Then, we use a formula that junior high school students know to calculate the distance between two points. Since the distance may be less than 1, we need to check whether the return value is less than 1 after renting. If so, let it still be equal to 1, so that when drawing the map, nothing will be lost:

let distance = sqrt(pow(viewPoint1.x - viewPoint2.x, 2) + pow(viewPoint1.y - viewPoint2.y, 2))
if distance < 1 {
    return 1.0
} else {
    return CGFloat(distance)}Copy the code

Finally, we just need to assign a value to the trajectory. The code for the above dismemberment is put together as follows:

class FogLayer: CALayer {...override func draw(in ctx: CGContext){...if letlineWidth = lineWidth { path? .lineWidth = lineWidth }else{ path? .lineWidth =5}... }var lineWidth: CGFloat? {
        if let mapView = self.mapView {
            // The distance between mapPoint1 and mapPoint2 in the map is about 53m
            let mapPoint1 = CLLocationCoordinate2D(latitude: 22.629052, longitude: 114.136977)
            let mapPoint2 = CLLocationCoordinate2D(latitude: 22.629519, longitude: 114.137098)

            let viewPoint1 = mapView.convert(mapPoint1, toPointTo: mapView)
            let viewPoint2 = mapView.convert(mapPoint2, toPointTo: mapView)

            let distance = sqrt(pow(viewPoint1.x - viewPoint2.x, 2) + pow(viewPoint1.y - viewPoint2.y, 2))
            if distance < 1 {
                return 1.0
            } else {
                return CGFloat(distance)}}else {
            return nil}}}Copy the code

After running, zoom in on the map and you can see that the track has been enlarged as the map has been enlarged:

If you need the code for this phase, do as follows:

Git clone github.com/no-rains/Ma…

git checkout foglayer