3

Add Maps to your iOS App using Amplify Geo, powered by Amazon Location Service

 2 years ago
source link: https://aws.amazon.com/blogs/mobile/add-maps-to-your-ios-app-using-amplify-geo-powered-by-amazon-location-service/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Add Maps to your iOS App using Amplify Geo, powered by Amazon Location Service

by Harshita Daddala | on 05 APR 2022 | in Amazon Location, Announcements, AWS Amplify, Front-End Web & Mobile, Technical How-to | Permalink | Comments |  Share

This blog post was written by Ian Saultz – Software Development Engineer at AWS Amplify.

Time to Read: 20 minutes
Time to Complete: 60 minutes

Today’s release of AWS Amplify Geo for iOS allows developers to quickly and easily add customizable maps with annotations and location searches to their iOS applications. The location APIs are powered by Amazon Location Service and the map rendering comes from the popular open-source map library, MapLibre. Today we’ll be building a simple application to showcase the capabilities of Amplify Geo.

Benefits

  • Add maps and search functionality to your iOS application using SwiftUI or UIKit.
  • Leverages the cost-effectiveness and privacy benefits of Amazon Location Service.
  • Uses the popular open-source library, MapLibre GL Native, for on device rendering.

What we’ll build

Today we’ll be building a simple, but fun, SwiftUI application that allows a user to find cafes near a hotel for their next trip. We’ll set up Amplify Geo with the Amplify MapLibre Adapter to render a map, allow a user to search for a place, and display cafes near that place on the map. In addition to being very important (yay coffee ☕️), you’ll be using some of the powerful customization features that Amplify Geo provides.

Prerequisites

  • Xcode 12.0 or higher.
  • The latest Amplify CLI version
  • You can obtain this by running npm install -g @aws-amplify/cli@geo

Note: There’s currently an Xcode bug related to rendering on simulator with M1 macs. So if you’re using one, you’ll need to run your project on a physical device.

1. Setting up Amplify Geo

Enough talk, let’s get started by creating a new Xcode project.

Select iOS and App, then click next.

On the next screen, enter “beans” for the product name. Select SwiftUI for the Interface and Swift for the Language.

Now that you have your Xcode project set up, you’ll setup Amplify Geo. Start by navigating to your project directory in your terminal, and invoking amplify init. Use the following answers for the prompts you receive:

- Enter a name for the projects (beans)
--> hit enter

- Initialize the project with the above configuration? (Y/n)
--> n

- Enter a name for the environment (dev)
--> hit enter

- Choose your default editor:
--> Xcode (macOS only)

- Choose the type of app that you're building (use the arrow keys)
--> ios

- Select the authentication method you want to use
--> AWS profile

You’ll see a message that your project has been successfully initialized and connected to the cloud! Congrats, you’ve setup the initial scaffolding for your Amplify project.

Now let’s get to business setting up adding Geo using the command amplify add geo.

- Select which capability you want to add: (use arrow keys)
--> Map (visualize the geospatial data)

- geo category resources require auth (Amazon Cognito). Do you want to add auth now? (Y/n)
--> Y

- Do you want to use the default authentication and security configuration? (use arrow keys)
--> Default configuration

- How do want users to be able to sign in? (use arrow keys)
--> Username

- Do you want to configure advanced settings? (use arrow keys)
--> No, I am done.

- Provide a name for the map:
--> Whatever you'd like. The default is fine.

- Who can access this Map?
--> Authorized and Guest users

- Do you want to configure advanced settings? (y/N)
--> N

You now have maps configured for your project. The last remaining Amplify CLI piece is adding location search capabilities. You’ll use amplify add geo again for this.

- Select which capability you want to add: (use arrow keys)
--> Location search (search by places, addresses, coordinates)

- Provide a name for the location search index (place index):
--> Whatever you'd like. The default is fine.

- Who can access this Map?
--> Authorized and Guest users

- Do you want to configure advanced settings? (y/N)
--> N

Now that your Amplify CLI setup is complete, go ahead and do an amplify push.

2. Pulling in AmplifyMapLibreAdapter

While the resources are being updated, it’s time to start writing your application. Open your Xcode project and go to Package Dependencies in the Project Navigator.

Project Navigator in Xcode

Click the + icon and enter https://github.com/aws-amplify/amplify-ios-maplibre in the search box in the upper right hand corner. Select Up to Next Major Version: 1.0.0 as the Dependency Rule and click Add Package.

Search for Amplify MapLibre package in SPM

Check both boxes to add the AmplifyMapLibreAdapter and AmplifyMapLibreUI libraries.

Add AmplifyMapLibreAdapter and AmplifyMapLibreUI packages

3. Configuring Amplify

Next up, navigate to the beansApp file and configure Amplify. Your file should look like this when you’re done. The compiler will complain about not being able to find MapView in scope. So let’s add it. Continue on to the next step to create your MapView.

import SwiftUI
import Amplify
import AWSCognitoAuthPlugin
import AWSLocationGeoPlugin

@main
struct beansApp: App {
    var body: some Scene {
        WindowGroup {
            MapView()
        }
    }
    
    init() {
        let auth = AWSCognitoAuthPlugin()
        let geo = AWSLocationGeoPlugin()
        do {
            try Amplify.add(plugin: auth)
            try Amplify.add(plugin: geo)
            try Amplify.configure()
        } catch {
            assertionFailure("Error configuring Amplify: \(error)")
        }
    }
}
Swift

4. Let’s make an App

Create a SwiftUI View file called “MapView”. In MapView.swift add an import statement import AmplifyMapLibreUI and replace the Text("Hello, World!") placeholder with AMLMapView.

import SwiftUI
import AmplifyMapLibreUI

struct MapView: View {
    var body: some View {
        AMLMapView()
    }
}
Swift

Build and run (cmd + r) and voilà! you have a map. Throw a .edgesIgnoringSafeArea(.all) on there to make the map cover the whole screen.

AMLMapView()
  .edgesIgnoringSafeArea(.all)
Swift

A map by itself is cool and all, but you want to make a functioning app. Time to add some search functionality by leveraging AMLSearchBar. Change the body of your MapView to this:

var body: some View {
    ZStack(alignment: .top) {
        AMLMapView()
            .edgesIgnoringSafeArea(.all)
        
        ZStack(alignment: .center) {
            AMLSearchBar(
                text: .constant(""),
                displayState: .constant(.map),
                onEditing: {},
                onCommit: {},
                onCancel: {},
                showDisplayStateButton: false
            )
            .padding()
        }
    }
}
Swift

Build and run again. Now you’ll see a search bar at the top of the map. You might have caught that you’re setting the text to a constant binding with an empty string with .constant(""). That just won’t do. You’ll need the search bar to inform you about the text the user enters.

Create a new Swift File and call it MapViewModel. In MapViewModel, add these import statement and an ObservableObject called… you guessed it, MapViewModel.

import SwiftUI
import AmplifyMapLibreUI
import AmplifyMapLibreAdapter
import Amplify
import CoreLocation

class MapViewModel: ObservableObject { 
  @Published var searchText = ""
}
Swift

While you’re in there, add an AMLMapViewState property as well. This is how you’ll be managing the state of your AMLMapView. You can read all about the various functionality AMLMapViewState provides in the API documentation.

class MapViewModel: ObservableObject {
    @Published var searchText = ""
    @ObservedObject var mapState = AMLMapViewState()
}
Swift

Before you go back to MapView to wire up searchText to the AMLSearchBar, go ahead and add this method to the MapViewModel.

func findHotel() {
    Amplify.Geo.search(for: searchText) { [weak self] result in
        switch result {
        case .success(let places):
            if let hotel = places.first { }
        case .failure(let error):
            print("Search failed with: \(error)")
            
        }
    }
}
Swift

You’ll see a few compiler after adding findHotel(). Don’t worry, those will be addressed shortly. Back in MapView.swift add a @StateObject viewModel property, the mapState to your AMLMapView, wire up the searchText, and pass findHotel into the onCommit closure. When you’re done, MapView should look like:

struct MapView: View {
    @StateObject var viewModel = MapViewModel()
    
    var body: some View {
        ZStack(alignment: .top) {
            AMLMapView(mapState: viewModel.mapState)
                .edgesIgnoringSafeArea(.all)
            
            ZStack(alignment: .center) {
                AMLSearchBar(
                    text: $viewModel.searchText,
                    displayState: .constant(.map),
                    onEditing: {},
                    onCommit: viewModel.findHotel,
                    onCancel: {},
                    showDisplayStateButton: false
                )
                .padding()
            }
        }
    }
}
Swift

Now, when a user enters something into the search bar, your searchText property is observing those changes. When a user taps go, you’ll make a request to Amazon Location Service asking for applicable results.

beans isn’t displaying anything yet though – you’re almost there, I promise. With the addition of the next two methods in MapViewModel.swift beans will:

  1.  Grab the most applicable result back from the search.
  2. Center the map around that result.
  3. Search for coffee near that result.
  4. Display markers (features) on the map for each result from the ☕️ search.
private func setCenter(for place: Geo.Place) {
    DispatchQueue.main.async {
        self.mapState.center = CLLocationCoordinate2D(place.coordinates)
    }
}

private func findCoffee(near hotel: Geo.Place) {
    Amplify.Geo.search(for: "coffee", options: .init(area: .near(hotel.coordinates))) { [weak self] result in
        switch result {
        case .success(let cafes):
            DispatchQueue.main.async {
                self?.mapState.features = AmplifyMapLibre.createFeatures(cafes)
            }
        case .failure(let error):
            print(error)
        }
    }
}
Swift

(Graceful error handling is left as an exercise for the reader 😀)

All that’s left to do is call these methods with the initial search results. In the if let hotel = places.first block, add:

class MapViewModel: ObservableObject {
    @Published var searchText = ""
    @ObservedObject var mapState = AMLMapViewState()
    
    private func setCenter(for place: Geo.Place) {
        DispatchQueue.main.async {
            self.mapState.center = CLLocationCoordinate2D(place.coordinates)
        }
    }

    private func findCoffee(near hotel: Geo.Place) {
        Amplify.Geo.search(for: "coffee", options: .init(area: .near(hotel.coordinates))) { [weak self] result in
            switch result {
            case .success(let cafes):
                DispatchQueue.main.async {
                    self?.mapState.features = AmplifyMapLibre.createFeatures(cafes)
                }
            case .failure(let error):
                print(error)
            }
        }
    }
    
    func findHotel() {
        Amplify.Geo.search(for: searchText) { [weak self] result in
            switch result {
            case .success(let places):
                if let hotel = places.first {
                    self?.setCenter(for: hotel)
                    self?.findCoffee(near: hotel)
                }
            case .failure(let error):
                print("Search failed with: \(error)")
                
            }
        }
    }
}
Swift

Time to see the fruits of your hard labor – build and run your application!

In the search bar, type an address or hotel name, and tap go. For example: 1800 Yale Avenue, Seattle. You’ll see the map center move to the first result from the search and markers for coffee results appear on the map. Tap one of the markers to see the information for that particular place.

Map View with Markers

Tip: The default blue markers can be replaced with any image you’d like. Here’s an example of using smiley faces as markers : )

AMLMapView(mapState: viewModel.mapState)
  .featureImage { 
    UIImage(
      systemName: "face.smiling",
      withConfiguration: UIImage.SymbolConfiguration(pointSize: 40)
    )!
  }
  .edgesIgnoringSafeArea(.all)
Swift

Map View with Custom Markers i.e. Smiley faces

Conclusion

You have now built a functional app that displays coffee shops near an address or place, and in the process learned about some of the great features Amplify Geo offers. There are plenty more capabilities and customizations offered by Amplify Geo; learn more about this in the Next Steps section below.

Next Steps

Now that you’ve baked some functionality into beans, the next steps are completely up to you. Do you want to apply this knowledge to another application? Make sure to clean up beans by deleting the backend resources using amplify delete. Do you want to continue to build onto beans, maybe add a social element with ratings? That would be an awesome feature to build out using Amplify Geo and Amplify DataStore in combination.

Check out the official Amplify Geo documentation and AmplifyMapLibre API documentation to learn about what you can build with Amplify Geo. Also take a look at the AMLExamples for some inspiration on some more complex UI / UX you can build. We’re actively working on new features for the Geo category, so keep an eye out for new and exciting capabilities.

Keep in touch with us on Discord in #geo-help to let us know about any questions you have.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK