Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthetechie committed Sep 7, 2024
0 parents commit 037c69b
Show file tree
Hide file tree
Showing 11 changed files with 409 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
27 changes: 27 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Copyright (c) 2023, Stadia Maps, Inc.

All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Stadia Maps nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 changes: 24 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"originHash" : "7e0e0b4f9f6cf2f1eb0a555a1c8cbf395ed94d8d167b2cb16c044c92df7ced2d",
"pins" : [
{
"identity" : "anycodable",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Flight-School/AnyCodable",
"state" : {
"revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05",
"version" : "0.6.7"
}
},
{
"identity" : "stadiamaps-api-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stadiamaps/stadiamaps-api-swift",
"state" : {
"revision" : "87d74dca9e79390e36a5909ee6be75f7f9e9dfac",
"version" : "4.0.0"
}
}
],
"version" : 3
}
36 changes: 36 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "StadiaMapsAutocompleteSearch",
platforms: [
.iOS(.v15),
.macOS(.v14),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "StadiaMapsAutocompleteSearch",
targets: ["StadiaMapsAutocompleteSearch"]
),
],
dependencies: [
.package(url: "https://github.com/stadiamaps/stadiamaps-api-swift", from: "4.0.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "StadiaMapsAutocompleteSearch",
dependencies: [
.product(name: "StadiaMaps", package: "stadiamaps-api-swift"),
]
),
.testTarget(
name: "StadiaMapsAutocompleteSearchTests",
dependencies: ["StadiaMapsAutocompleteSearch"]
),
]
)
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Stadia Maps Autocomplete Search for SwiftUI

This package lets you add geographic autocomplete search to a SwiftUI app.

* Displays a search box and list which you can embed in other views
* Provides a callback handler with the result details when users tap a result
* Can bias search results to be nearby a specific location
* Automatically localizes place names based on the user's device settings (where available)

![Screenshot](screenshot.png)

## Installation

The Xcode UI changes frequently, but you can usually add packages to your project using an option in the File menu.
Then, you'll need to paste in the repository URL to search: https://github.com/stadiamaps/swiftui-autocomplete-search.
See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app for the latest detailed
instructions from Apple.

## Getting an API key

You will need an API key to use this view.

You can create an API key for free
[here](https://client.stadiamaps.com/signup/?utm_source=github&utm_campaign=sdk_readme&utm_content=swiftui_autocomplete_readme)
(no credit card required).

## Using the SwiftUI view

```swift
import StadiaMapsAutocompleteSearch
let stadiaMapsAPIKey = "YOUR-API-KEY" // Replace with your API key

// Somewhere in your view body....
AutocompleteSearch(apiKey: stadiaMapsAPIKey, userLocation: userLocation.clLocation) { selection in
// Do something with the selection.
// For example, you might do something like this to start navigation in an app using Ferrostar.
Task {
do {
routes = try await ferrostarCore.getRoutes(initialLocation: userLocation, waypoints: [Waypoint(coordinate: GeographicCoordinate(lat: selection.geometry.coordinates[1], lng: selection.geometry.coordinates[0]), kind: .break)])

try ferrostarCore.startNavigation(route: routes!.first!)
errorMessage = nil
} catch {
errorMessage = "Error: \(error)"
}
}
}
```
100 changes: 100 additions & 0 deletions Sources/StadiaMapsAutocompleteSearch/AutocompleteSearch.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import CoreLocation
import StadiaMaps
import SwiftUI

/// An autocomplete search view that searches for geographic locations as you type.
public struct AutocompleteSearch: View {
@State private var searchText = ""
@State private var searchResults: [PeliasGeoJSONFeature] = []
@State private var isLoading = false

let userLocation: CLLocation?
let onResultSelected: ((PeliasGeoJSONFeature) -> Void)?

/// Creates an autocomplete geographic search view.
/// - Parameters:
/// - apiKey: Your Stadia Maps API key
/// - useEUEndpoint: Send requests to servers located in the European Union (may significantly degrade performance outside Europe)
/// - userLocation: If present, biases the search for results near a specific location and displays results with (straight-line) distances from this location
/// - onResultSelected: A callback invoked when a result is tapped in the list
public init(apiKey: String, useEUEndpoint: Bool = false, userLocation: CLLocation? = nil, onResultSelected: ((PeliasGeoJSONFeature) -> Void)? = nil) {
StadiaMapsAPI.customHeaders = ["Authorization": "Stadia-Auth \(apiKey)"]
if useEUEndpoint {
StadiaMapsAPI.basePath = "https://api-eu.stadiamaps.com"
}
self.userLocation = userLocation
self.onResultSelected = onResultSelected
}

public var body: some View {
// TODO: Language override?
// TODO: Min search length?
TextField("Search", text: $searchText)
.onChange(of: searchText) { query in
Task {
try await search(query: query, autocomplete: true)
}
}
.onSubmit {
Task {
try await search(query: searchText, autocomplete: false)
}
}

ZStack {
List {
ForEach(searchResults) { result in
SearchResult(feature: result, relativeTo: userLocation)
.contentShape(.rect)
.onTapGesture {
onResultSelected?(result)
}
}
}

if isLoading {
ProgressView()
}
}
}

private func search(query: String, autocomplete: Bool) async throws {
guard !query.isEmpty else {
searchResults = []
return
}

isLoading = true

defer {
self.isLoading = false
}

let result: PeliasResponse

if autocomplete {
result = try await GeocodingAPI.autocomplete(text: query, focusPointLat: userLocation?.coordinate.latitude, focusPointLon: userLocation?.coordinate.longitude)
} else {
result = try await GeocodingAPI.search(text: query, focusPointLat: userLocation?.coordinate.latitude, focusPointLon: userLocation?.coordinate.longitude)
}

// Only replace results if the text matches the current input
if query == searchText {
searchResults = result.features
}
}
}

// Set this to your own Stadia Maps API key.
// Get an free key at client.stadiamaps.com.
private let previewApiKey = "YOUR-API-KEY"

#Preview {
if previewApiKey == "YOUR-API-KEY" {
Text("You need an API key for this to be very useful. Get one at client.stadiamaps.com.")
} else {
AutocompleteSearch(apiKey: previewApiKey) { selection in
print("Selected: \(selection)")
}
}
}
81 changes: 81 additions & 0 deletions Sources/StadiaMapsAutocompleteSearch/Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Foundation
import StadiaMaps
import SwiftUI

extension PeliasGeoJSONFeature: Identifiable {
public var id: String? {
properties?.gid
}
}

public extension PeliasGeoJSONFeature {
var subtitle: String? {
if let layer = properties?.layer {
switch layer {
case .venue, .address, .street, .neighbourhood, .postalcode, .macrohood:
return properties?.locality ?? properties?.region ?? properties?.country
case .country, .dependency, .disputed, .continent:
return properties?.continent
case .macroregion, .region:
return properties?.country
case .locality, .localadmin, .borough, .macrocounty, .county:
return properties?.region ?? properties?.country
case .coarse, .marinearea, .empire, .ocean:
return nil
}
} else {
return nil
}
}
}

extension PeliasLayer {
var iconImage: Image {
let imageName = switch self {
case .venue:
"building.2.crop.circle"
case .address:
"123.rectangle"
case .street:
"road.lanes"
case .country:
"globe.americas"
case .macroregion:
"globe.americas"
case .region:
"globe.americas"
case .macrocounty:
"mappin.and.ellipse"
case .county:
"mappin.and.ellipse"
case .locality:
"mappin.and.ellipse"
case .localadmin:
"mappin.and.ellipse"
case .borough:
"mappin.and.ellipse"
case .neighbourhood:
"mappin.and.ellipse"
case .postalcode:
"mappin.and.ellipse"
case .coarse:
"mappin.and.ellipse"
case .dependency:
"globe.americas"
case .macrohood:
"globe.americas"
case .marinearea:
"water.waves"
case .disputed:
"questionmark.circle"
case .empire:
"globe.americas"
case .continent:
"globe.americas"
case .ocean:
"water.waves"
}

return Image(systemName: imageName)
}
}
Loading

0 comments on commit 037c69b

Please sign in to comment.