Front-End Web & Mobile
Add Maps to your iOS App using Amplify Geo, powered by Amazon Location Service
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
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.
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.
Check both boxes to add the AmplifyMapLibreAdapter
and AmplifyMapLibreUI
libraries.
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)")
}
}
}
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()
}
}
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)
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()
}
}
}
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 = ""
}
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()
}
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)")
}
}
}
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()
}
}
}
}
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:
- Grab the most applicable result back from the search.
- Center the map around that result.
- Search for coffee near that result.
- 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)
}
}
}
(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)")
}
}
}
}
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.
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)
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.