-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #110 from stadiamaps/feat/ios-arrival-view
feat: added basic arrival view
- Loading branch information
Showing
19 changed files
with
444 additions
and
27 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
9 changes: 9 additions & 0 deletions
9
apple/Sources/FerrostarCore/Extensions/TripProgressExtensions.swift
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,9 @@ | ||
import FerrostarCoreFFI | ||
import Foundation | ||
|
||
public extension TripProgress { | ||
/// The estimated arrival date and time. | ||
func estimatedArrival(from startingDate: Date = Date()) -> Date { | ||
startingDate.addingTimeInterval(durationRemaining) | ||
} | ||
} |
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
32 changes: 32 additions & 0 deletions
32
apple/Sources/FerrostarSwiftUI/Library/DefaultFormatters.swift
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,32 @@ | ||
import Foundation | ||
import MapKit | ||
|
||
/// A collection of arrival view formatters that work reasonably well for most applications. | ||
public class DefaultFormatters { | ||
/// An MKDistance formatter with abbreviated units for the arrival view. | ||
/// | ||
/// E.g. 120 mi | ||
public static var distanceFormatter: MKDistanceFormatter { | ||
let formatter = MKDistanceFormatter() | ||
formatter.unitStyle = .abbreviated | ||
return formatter | ||
} | ||
|
||
/// A formatter for estimated time of arrival using the shortened style for the current locale. | ||
/// | ||
/// E.g. `5:20 PM` or `17:20` | ||
public static var estimatedArrivalFormat: Date.FormatStyle { | ||
Date.FormatStyle(date: .omitted, time: .shortened) | ||
} | ||
|
||
/// A formatter for duration on the arrival view using (optional) hours and minutes. | ||
/// | ||
/// E.g. `1h 20m` | ||
public static var durationFormat: DateComponentsFormatter { | ||
let formatter = DateComponentsFormatter() | ||
formatter.allowedUnits = [.hour, .minute] | ||
formatter.unitsStyle = .abbreviated | ||
formatter.zeroFormattingBehavior = .dropAll | ||
return formatter | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
apple/Sources/FerrostarSwiftUI/Resources/Localizable.xcstrings
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,42 @@ | ||
{ | ||
"sourceLanguage" : "en", | ||
"strings" : { | ||
"%@" : { | ||
|
||
}, | ||
"Arrival" : { | ||
"extractionState" : "manual", | ||
"localizations" : { | ||
"en" : { | ||
"stringUnit" : { | ||
"state" : "translated", | ||
"value" : "Arrival" | ||
} | ||
} | ||
} | ||
}, | ||
"Distance" : { | ||
"extractionState" : "manual", | ||
"localizations" : { | ||
"en" : { | ||
"stringUnit" : { | ||
"state" : "translated", | ||
"value" : "Distance" | ||
} | ||
} | ||
} | ||
}, | ||
"Duration" : { | ||
"extractionState" : "manual", | ||
"localizations" : { | ||
"en" : { | ||
"stringUnit" : { | ||
"state" : "translated", | ||
"value" : "Duration" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"version" : "1.0" | ||
} |
40 changes: 40 additions & 0 deletions
40
apple/Sources/FerrostarSwiftUI/Theme/ArrivalViewTheme.swift
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,40 @@ | ||
import SwiftUI | ||
|
||
public enum ArrivalViewStyle: Equatable { | ||
/// The simplified/default which only shows actual values | ||
case simplified | ||
|
||
/// An expanded informational arrival view that labels each value. | ||
case informational | ||
} | ||
|
||
public protocol ArrivalViewTheme: Equatable { | ||
/// The style of the arrival view controls the general theme. | ||
var style: ArrivalViewStyle { get } | ||
|
||
/// The color for the measurement values (top row) | ||
var measurementColor: Color { get } | ||
|
||
/// The font for the measurement values (top row) | ||
var measurementFont: Font { get } | ||
|
||
/// The color for the secondary text. | ||
var secondaryColor: Color { get } | ||
|
||
/// The font for the secondary text. | ||
var secondaryFont: Font { get } | ||
|
||
/// The color of the background. | ||
var backgroundColor: Color { get } | ||
} | ||
|
||
public struct DefaultArrivalViewTheme: ArrivalViewTheme { | ||
public var style: ArrivalViewStyle = .simplified | ||
public var measurementColor: Color = .primary | ||
public var measurementFont: Font = .title2.bold() | ||
public var secondaryColor: Color = .secondary | ||
public var secondaryFont: Font = .subheadline | ||
public var backgroundColor: Color = .init(.systemBackground) | ||
|
||
public init() {} | ||
} |
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,138 @@ | ||
import FerrostarCore | ||
import FerrostarCoreFFI | ||
import MapKit | ||
import SwiftUI | ||
|
||
public struct ArrivalView: View { | ||
let progress: TripProgress | ||
let distanceFormatter: Formatter | ||
let estimatedArrivalFormatter: Date.FormatStyle | ||
let durationFormatter: DateComponentsFormatter | ||
let theme: any ArrivalViewTheme | ||
let fromDate: Date | ||
|
||
/// Initialize the ArrivalView | ||
/// | ||
/// - Parameters: | ||
/// - progress: The current Trip Progress providing durations and distances. | ||
/// - distanceFormatter: The distance formatter to use when displaying the remaining trip distance. | ||
/// - estimatedArrivalFormatter: The estimated time of arrival Date-Time formatter. | ||
/// - durationFormatter: The duration remaining formatter. | ||
/// - theme: The arrival view theme. | ||
/// - fromDate: The date time to estimate arrival from, primarily for testing (default is now). | ||
public init( | ||
progress: TripProgress, | ||
distanceFormatter: Formatter = DefaultFormatters.distanceFormatter, | ||
estimatedArrivalFormatter: Date.FormatStyle = DefaultFormatters.estimatedArrivalFormat, | ||
durationFormatter: DateComponentsFormatter = DefaultFormatters.durationFormat, | ||
theme: any ArrivalViewTheme = DefaultArrivalViewTheme(), | ||
fromDate: Date = Date() | ||
) { | ||
self.progress = progress | ||
self.distanceFormatter = distanceFormatter | ||
self.estimatedArrivalFormatter = estimatedArrivalFormatter | ||
self.durationFormatter = durationFormatter | ||
self.theme = theme | ||
self.fromDate = fromDate | ||
} | ||
|
||
public var body: some View { | ||
HStack { | ||
VStack { | ||
Text(estimatedArrivalFormatter.format(progress.estimatedArrival(from: fromDate))) | ||
.font(theme.measurementFont) | ||
.foregroundStyle(theme.measurementColor) | ||
.multilineTextAlignment(.center) | ||
|
||
if theme.style == .informational { | ||
Text("Arrival", bundle: .module) | ||
.font(theme.secondaryFont) | ||
.foregroundStyle(theme.secondaryColor) | ||
} | ||
} | ||
|
||
if let formattedDuration = durationFormatter.string(from: progress.durationRemaining) { | ||
VStack { | ||
Text(formattedDuration) | ||
.font(theme.measurementFont) | ||
.foregroundStyle(theme.measurementColor) | ||
.frame(maxWidth: .infinity) | ||
.multilineTextAlignment(.center) | ||
|
||
if theme.style == .informational { | ||
Text("Duration", bundle: .module) | ||
.font(theme.secondaryFont) | ||
.foregroundStyle(theme.secondaryColor) | ||
} | ||
} | ||
} | ||
|
||
VStack { | ||
Text(distanceFormatter.string(for: progress.distanceRemaining) ?? "") | ||
.font(theme.measurementFont) | ||
.foregroundStyle(theme.measurementColor) | ||
.multilineTextAlignment(.center) | ||
|
||
if theme.style == .informational { | ||
Text("Distance", bundle: .module) | ||
.font(theme.secondaryFont) | ||
.foregroundStyle(theme.secondaryColor) | ||
} | ||
} | ||
} | ||
.padding(.horizontal, 32) | ||
.padding(.vertical, 16) | ||
.background(theme.backgroundColor) | ||
.clipShape(.rect(cornerRadius: 48)) | ||
.shadow(radius: 12) | ||
} | ||
} | ||
|
||
#Preview { | ||
var informationalTheme: any ArrivalViewTheme { | ||
var theme = DefaultArrivalViewTheme() | ||
theme.style = .informational | ||
return theme | ||
} | ||
|
||
return VStack(spacing: 16) { | ||
ArrivalView( | ||
progress: TripProgress( | ||
distanceToNextManeuver: 123, | ||
distanceRemaining: 120, | ||
durationRemaining: 150 | ||
) | ||
) | ||
|
||
ArrivalView( | ||
progress: TripProgress( | ||
distanceToNextManeuver: 123, | ||
distanceRemaining: 14500, | ||
durationRemaining: 1234 | ||
) | ||
) | ||
|
||
ArrivalView( | ||
progress: TripProgress( | ||
distanceToNextManeuver: 123, | ||
distanceRemaining: 14500, | ||
durationRemaining: 1234 | ||
), | ||
theme: informationalTheme | ||
) | ||
.environment(\.locale, .init(identifier: "de_DE")) | ||
|
||
ArrivalView( | ||
progress: TripProgress( | ||
distanceToNextManeuver: 5420, | ||
distanceRemaining: 1_420_000, | ||
durationRemaining: 520_800 | ||
), | ||
theme: informationalTheme | ||
) | ||
|
||
Spacer() | ||
} | ||
.padding() | ||
.background(Color.green) | ||
} |
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
51 changes: 51 additions & 0 deletions
51
apple/Tests/FerrostarSwiftUITests/Library/DefaultFormatterTests.swift
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,51 @@ | ||
import XCTest | ||
@testable import FerrostarSwiftUI | ||
|
||
final class DefaultFormatterTests: XCTestCase { | ||
let referenceDate = Date(timeIntervalSince1970: 1_718_065_239) | ||
|
||
// MARK: Distance Formatter | ||
|
||
func testDistanceFormatter() { | ||
let formatter = DefaultFormatters.distanceFormatter | ||
XCTAssertEqual(formatter.string(fromDistance: 150), "500 ft") | ||
} | ||
|
||
func testDistanceFormatter_de_DE() { | ||
let formatter = DefaultFormatters.distanceFormatter | ||
formatter.locale = .init(identifier: "de_DE") | ||
formatter.units = .metric | ||
XCTAssertEqual(formatter.string(fromDistance: 150), "150 m") | ||
} | ||
|
||
// MARK: Estimated Time of Arrival (ETA) Formatter | ||
|
||
func testEstimatedArrivalFormatter() { | ||
var formatter = DefaultFormatters.estimatedArrivalFormat | ||
formatter.timeZone = .init(secondsFromGMT: 0)! | ||
|
||
XCTAssertEqual(referenceDate.formatted(formatter), "12:20 AM") | ||
} | ||
|
||
func testEstimatedArrivalFormatter_de_DE() { | ||
var formatter = DefaultFormatters.estimatedArrivalFormat | ||
.locale(.init(identifier: "de_DE")) | ||
formatter.timeZone = .init(secondsFromGMT: 0)! | ||
|
||
XCTAssertEqual(referenceDate.formatted(formatter), "0:20") | ||
} | ||
|
||
// MARK: Duration Formatters | ||
|
||
func testDurationFormatter() { | ||
let formatter = DefaultFormatters.durationFormat | ||
let duration: TimeInterval = 1200.0 | ||
XCTAssertEqual(formatter.string(from: duration), "20m") | ||
} | ||
|
||
func testDurationFormatter_Long() { | ||
let formatter = DefaultFormatters.durationFormat | ||
let duration: TimeInterval = 120_000.0 | ||
XCTAssertEqual(formatter.string(from: duration), "33h 20m") | ||
} | ||
} |
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
Oops, something went wrong.