Chapter 13: City Explorer – Using Maps in SwiftUI


What you’ll learn in this chapter:

  • How to implement and manage Apple Maps in SwiftUI and how to request the user’s location
  • Dropping pins on a map by using an UILongPressGesture
  • Using SwiftUI Grids to display collections of views 
  • Working with the Flickr API to fetch photos shot near the selected location 
  • Working with AsyncImage to fetch images asynchronously

What we’ll create 🚀

In this chapter, we will create an interactive app called “City Explorer”. The app mainly consists of a map on which the user can drop pins at any location. At the dropped pin’s location, the user can browse through images others shot near this place. We’ll use the API of the photo platform Flickr for fetching those photos.

At the end of this chapter, our app will look like this:

Take a look at how this app will look like in action:

The cool thing about this app is that the user can quickly get an impression of places close to him or that he would like to visit.

By creating this app, we will learn how to use maps with SwiftUI. We will also get to know how to query the user’s location. We will learn how to utilize third parties’ data, in our case, the database of the photo platform Flickr by using its API. Last but not least, we will learn how to fetch images asynchronously using AsyncImage views.

As always, we start by creating a new Xcode project. Let’s get started by implementing a simple map for our app.

Setting up the map 🗺

Although it’s possible to embed maps directly into SwiftUI, the way over embedding a map using a UIViewRepresentable offers us more customization options. For example, we can implement a UILongPressGesture that allows the user to drop a pin. Therefore, we utilize the UIKit framework, similar to how we did in the last chapter.

Let’s create a new Swift file, which we call “MyMap.swift”. Next, we need to import two frameworks – first, as usual, the SwiftUI and second, the MapKit framework used to create and manage Apple Maps.

import MapKit
import SwiftUI

In contrast to the last chapter, we don’t want to add a UIViewController but only an MKMapView instance as UIView in our SwiftUI view. Therefore we create a struct that corresponds to the UIViewRepresentable protocol.

struct MyMap: UIViewRepresentable {
    
}

To conform to the UIViewRepresentable protocol, we need to implement two methods: the makeUIView methods to initialize the map and the updateUIView function to update it if necessary.

Let’s start with the makeUIView function that should return an MKMapView instance.

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

At this point, we can implement the updateUIView function as well, but it can remain empty for now.

func updateUIView(_ map: MKMapView, context: Context) {
    
}

We will use methods of the MKMapViewDelegate protocol later on. Can you guess what we need to handle delegate methods? Right, a coordinator!

As already learned in the last chapter, we implement the coordinator as a subclass of our MyMap struct …

struct MyMap: UIViewRepresentable {
    
    //...
    
    class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
        
        var parent: MyMap
        var map: MKMapView?
        
        init(_ parent: MyMap) {
            self.parent = parent
        }
        
    }
    
}

… which we then initialize within our MyMap using the makeCoordinator method:

struct MyMap: UIViewRepresentable {
    
    //...
    
    func makeCoordinator() -> MyMap.Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
        
        //...
        
    }
    
}

Awesome! Now we can insert this map in our ContentView! To do this, we replace the “Hello, world!” Text in our ContentView with a MyMap instance:

struct ContentView: View {
    var body: some View {
        MyMap()
    }
}

Start a Live preview to see how your MyMap looks.

We want to make two more changes: first, we want the MyMap to cover the entire screen. For this purpose, we need to know the following: By default, the ContentView‘s content stays inside the so-called safe area what causes the MyMap not to touch the upper and lower edge of the screen. However, if we want our MyMap view to be genuinely fullscreen, we can use the .edgesIgnoringSafeArea modifier. 

MyMap()
    .edgesIgnoringSafeArea(.all)

This causes the MyMap contained in the ContentView to exceed the safe area’s boundaries. 

Besides that, we would like to have a small button at the bottom right corner with which the user can zoom to the current location. To stack this button on top of our Map, we wrap it into a ZStack and choose the .bottomTrailing alignment mode. Of course, we don’t only want the MyMap to ignore the safe area, but the whole ZStack:


ZStack(alignment: .bottomTrailing) {
    MyMap()
}
    .edgesIgnoringSafeArea(.all)

Below the MyMap instance add an Image view showing the location symbol from the SF Symbols app.

Image(systemName: "location")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: 25, height: 25)
    .clipped()
    .padding(40)

To add a blurred background to our location icon we can use the material background style available since iOS 15 (ranging from .ultraThinMaterial to .ultraThickMaterial).

Image(systemName: "location")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: 25, height: 25)
    .clipped()
    .padding(40)
    .frame(width: 60, height: 60)
    .background(.ultraThinMaterial, in: Circle())
    .padding()

To keep the overview of our code, we outsource the created Image view by CMD-clicking on it and selecting “Extract Subview”. We name the extracted subview “LocationButtonContent”. 

Next, we wrap the LocationButtonContent instance of our ContentView into a Button like this:

struct ContentView: View {
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            MyMap()
            Button(action: {
                print("Go to user location.")
            }) {
                LocationButtonContent()
            }
        }
            .edgesIgnoringSafeArea(.all)
    }
}

If we run our app again, we should see our new Button!

The location button gets stacked on top of the map and allows the user to zoom to his current location

Great! We just created the map for our City Explorer app! Next, we’ll learn how to query the user’s location, display it on the map, and finally use the Button we just created to move the map to the user location.

2 replies on “Chapter 13: City Explorer – Using Maps in SwiftUI”

func makeCoordinator() -> MyMap.Coordinator {
Coordinator(self)
}
ERROR
Argument passed to call that takes no arguments

Leave a Reply

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