-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extend Example with ability to edit waypoints
- Loading branch information
Showing
5 changed files
with
255 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import Foundation | ||
import SwiftUI | ||
import MapboxDirections | ||
|
||
struct RoutesView: View { | ||
private static let distanceFormatter: LengthFormatter = .init() | ||
private static let travelTimeFormatter: DateComponentsFormatter = { | ||
let f = DateComponentsFormatter() | ||
f.unitsStyle = .short | ||
return f | ||
}() | ||
|
||
let routes: [Route] | ||
|
||
var body: some View { | ||
ScrollView { | ||
LazyVStack(spacing: 10, content: { | ||
ForEach(routes, id: \.distance) { route in | ||
VStack(alignment: .leading, spacing: 3) { | ||
headerView(for: route) | ||
ForEach(0..<route.legs.count, id: \.self) { legIdx in | ||
if let source = route.legs[legIdx].source?.name, | ||
let destination = route.legs[legIdx].destination?.name { | ||
Text("From '\(source)' to '\(destination)'").font(.title2) | ||
} | ||
else { | ||
Text("Steps:").font(.title2) | ||
} | ||
stepsView(for: route.legs[legIdx]) | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
.navigationTitle("Routes") | ||
.padding(5) | ||
} | ||
|
||
@ViewBuilder | ||
private func headerView(for route: Route) -> some View { | ||
VStack(alignment: .leading, spacing: 10) { | ||
HStack { | ||
Text("Route: ").fontWeight(.bold) | ||
Text(route.description) | ||
.fixedSize(horizontal: false, vertical: true) | ||
} | ||
HStack { | ||
Text("Distance: ").fontWeight(.bold) | ||
Text(formattedDistance(for:route)) | ||
} | ||
HStack { | ||
Text("ETA: ").fontWeight(.bold) | ||
Text(formattedTravelTime(for: route)) | ||
} | ||
HStack { | ||
Text("Typical travel time: ").fontWeight(.bold) | ||
Text(formattedTypicalTravelTime(for: route)) | ||
} | ||
Divider() | ||
} | ||
} | ||
|
||
@ViewBuilder | ||
private func stepsView(for leg: RouteLeg) -> some View { | ||
LazyVStack(alignment: .leading, spacing: 5, content: { | ||
ForEach(0..<leg.steps.count, id: \.self) { stepIdx in | ||
HStack { | ||
Text("\(stepIdx + 1). ").fontWeight(.bold) | ||
Text(stepDescriptions(for: leg.steps[stepIdx])) | ||
} | ||
.padding([.top, .bottom], 3) | ||
|
||
Divider() | ||
} | ||
}) | ||
} | ||
|
||
func formattedDistance(for route: Route) -> String { | ||
return Self.distanceFormatter.string(fromMeters: route.distance) | ||
} | ||
|
||
func formattedTravelTime(for route: Route) -> String { | ||
return Self.travelTimeFormatter.string(from: route.expectedTravelTime)! | ||
} | ||
|
||
func formattedTypicalTravelTime(for route: Route) -> String { | ||
if let typicalTravelTime = route.typicalTravelTime, | ||
let formattedTypicalTravelTime = Self.travelTimeFormatter.string(from: typicalTravelTime) { | ||
return formattedTypicalTravelTime | ||
} | ||
else { | ||
return "Not available" | ||
} | ||
} | ||
|
||
func stepDescriptions(for step: RouteStep) -> String { | ||
var description: String = "" | ||
let direction = step.maneuverDirection?.rawValue ?? "none" | ||
description.append("\(step.instructions) [\(step.maneuverType) \(direction)]") | ||
if step.distance > 0 { | ||
let formattedDistance = Self.distanceFormatter.string(fromMeters: step.distance) | ||
description.append(" (\(step.transportType) for \(formattedDistance))") | ||
} | ||
return description | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import Foundation | ||
import SwiftUI | ||
import CoreLocation | ||
import MapboxDirections | ||
|
||
struct Waypoint: Identifiable, Hashable { | ||
let id: UUID = UUID() | ||
var latitude: CLLocationDegrees = 0 | ||
var longitude: CLLocationDegrees = 0 | ||
var name: String = "" | ||
|
||
var native: MapboxDirections.Waypoint { | ||
.init(coordinate: .init(latitude: latitude, longitude: longitude), name: name) | ||
} | ||
} | ||
|
||
struct WaypointsEditor: View { | ||
@Binding | ||
var waypoints: [Waypoint] | ||
|
||
var body: some View { | ||
List { | ||
ForEach(waypoints) { waypoint in | ||
HStack { | ||
WaypointView(waypoint: .init(get: { | ||
self.waypoints.first(where: { $0.id == waypoint.id })! | ||
}, set: { newValue in | ||
self.waypoints.firstIndex(of: waypoint).map { | ||
waypoints[$0] = newValue | ||
} | ||
})) | ||
|
||
Menu { | ||
Button("Insert Above") { | ||
addNewWaypoint(before: waypoint) | ||
} | ||
Button("Insert Below") { | ||
addNewWaypoint(after: waypoint) | ||
} | ||
} label: { | ||
Image(systemName: "ellipsis") | ||
.frame(width: 30, height: 30, alignment: .center) | ||
} | ||
} | ||
} | ||
.onMove { indices, newOffset in | ||
waypoints.move(fromOffsets: indices, toOffset: newOffset) | ||
} | ||
.onDelete { indexSet in | ||
waypoints.remove(atOffsets: indexSet) | ||
} | ||
} | ||
.listStyle(InsetGroupedListStyle()) | ||
.toolbar { | ||
EditButton() | ||
} | ||
} | ||
|
||
private func addNewWaypoint(after waypoint: Waypoint) { | ||
guard let waypointIndex = waypoints.firstIndex(of: waypoint) else { | ||
preconditionFailure("Waypoint is in the array of waypoints") | ||
} | ||
let insertionIndex = waypoints.index(after: waypointIndex) | ||
waypoints.insert(Waypoint(), at: insertionIndex) | ||
} | ||
private func addNewWaypoint(before waypoint: Waypoint) { | ||
guard let insertionIndex = waypoints.firstIndex(of: waypoint) else { | ||
preconditionFailure("Waypoint is in the array of waypoints") | ||
} | ||
waypoints.insert(Waypoint(), at: insertionIndex) | ||
} | ||
} | ||
|
||
struct WaypointView: View { | ||
@Binding | ||
var waypoint: Waypoint | ||
|
||
@State | ||
private var latitudeString: String | ||
|
||
@State | ||
private var longitudeString: String | ||
|
||
init(waypoint: Binding<Waypoint>) { | ||
_waypoint = waypoint | ||
_latitudeString = State<String>(initialValue: waypoint.wrappedValue.latitude.description) | ||
_longitudeString = .init(initialValue: waypoint.wrappedValue.longitude.description) | ||
} | ||
|
||
var body: some View { | ||
HStack { | ||
TextField("Name", text: $waypoint.name) | ||
TextField("Lat", text: $latitudeString, onEditingChanged: { _ in | ||
waypoint.latitude = .init(latitudeString) ?? 0 | ||
}) | ||
|
||
TextField("Lon", text: $longitudeString, onEditingChanged: { _ in | ||
waypoint.longitude = .init(longitudeString) ?? 0 | ||
}) | ||
} | ||
} | ||
} |
Oops, something went wrong.