Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RoamControl.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
knownRegions = (
en,
Base,
"zh-Hans",
);
mainGroup = A00000000000000000000004;
productRefGroup = A00000000000000000000005 /* Products */;
Expand Down
16 changes: 16 additions & 0 deletions RoamControl/App/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ final class AppModel {
private static let historyKey = "locationHistory"
private static let appearanceKey = "appAppearance"
private static let mapDisplayStyleKey = "mapDisplayStyle"
private static let appLanguageKey = "appLanguage"
private static let activeSessionRecoveryKey = "activeSessionRecovery"
private static let anonymousUsageStatisticsKey = "sharesAnonymousUsageStatistics"

Expand All @@ -25,7 +26,12 @@ final class AppModel {
private(set) var locationHistory: [LocationTarget]
private(set) var appearance: AppAppearance
private(set) var mapDisplayStyle: MapDisplayStyle
private(set) var appLanguage: AppLanguage
private(set) var sharesAnonymousUsageStatistics: Bool

var appLocale: Locale? {
appLanguage.locale
}
private(set) var interruptedSession: SessionRecoveryRecord?
private(set) var isRestoringInterruptedSession = false
private(set) var interruptedSessionError: String?
Expand Down Expand Up @@ -63,6 +69,9 @@ final class AppModel {
self.mapDisplayStyle = MapDisplayStyle(
rawValue: preferences.string(forKey: Self.mapDisplayStyleKey) ?? ""
) ?? .standard
self.appLanguage = AppLanguage(
rawValue: preferences.string(forKey: Self.appLanguageKey) ?? ""
) ?? .system
self.sharesAnonymousUsageStatistics = Self.initialUsageStatisticsPreference(
in: preferences
)
Expand Down Expand Up @@ -204,6 +213,12 @@ final class AppModel {
preferences.set(style.rawValue, forKey: Self.mapDisplayStyleKey)
}

func setAppLanguage(_ language: AppLanguage) {
guard language != appLanguage else { return }
appLanguage = language
preferences.set(language.rawValue, forKey: Self.appLanguageKey)
}

func setSharesAnonymousUsageStatistics(_ enabled: Bool) {
sharesAnonymousUsageStatistics = enabled
preferences.set(enabled, forKey: Self.anonymousUsageStatisticsKey)
Expand Down Expand Up @@ -250,6 +265,7 @@ final class AppModel {
locationHistory = []
appearance = .automatic
mapDisplayStyle = .standard
appLanguage = .system
sharesAnonymousUsageStatistics = false
interruptedSession = nil
activeSessionRecovery = nil
Expand Down
1 change: 1 addition & 0 deletions RoamControl/App/RoamControlApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct RoamControlApp: App {
}
}
.environment(appModel)
.environment(\.locale, appModel.appLocale ?? .autoupdatingCurrent)
.preferredColorScheme(preferredColorScheme)
.animation(
reduceMotion ? nil : .easeInOut(duration: 0.25),
Expand Down
4 changes: 2 additions & 2 deletions RoamControl/Features/Home/MobileDataGuidanceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ struct MobileDataGuidanceView: View {
}
}

private var title: String {
private var title: LocalizedStringKey {
switch guidance {
case .connectionHelp:
"Still Connecting"
Expand All @@ -146,7 +146,7 @@ struct MobileDataGuidanceView: View {
}
}

private var message: String {
private var message: LocalizedStringKey {
switch guidance {
case .connectionHelp:
"If you're on Wi‑Fi, make sure LocalDevVPN says Connected, then try again. Choose mobile data only when you're actually using 4G or 5G."
Expand Down
4 changes: 2 additions & 2 deletions RoamControl/Features/Home/SessionRecoveryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ struct SessionRecoveryView: View {
return "Roam Control closed before it could confirm that the simulated location at \(recovery.lastReportedLocation.name) ended. Choose what this iPhone should do next."
}

private var resumeTitle: String {
private var resumeTitle: LocalizedStringKey {
recovery.isWalkingRoute ? "Resume Walking" : "Resume Location"
}

private func recoveryDetail(title: String, value: String, symbol: String) -> some View {
private func recoveryDetail(title: LocalizedStringKey, value: String, symbol: String) -> some View {
Group {
if dynamicTypeSize.isAccessibilitySize {
HStack(alignment: .top, spacing: 11) {
Expand Down
6 changes: 3 additions & 3 deletions RoamControl/Features/Home/StatusCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct StatusCard: View {
.accessibilityElement(children: .combine)
}

private var title: String {
private var title: LocalizedStringKey {
switch state {
case .notConfigured: "Not configured"
case .ready: "Ready"
Expand All @@ -30,13 +30,13 @@ struct StatusCard: View {
}
}

private var detail: String {
private var detail: LocalizedStringKey {
switch state {
case .notConfigured: "Pairing support has not been added yet."
case .ready: "The paired device is available."
case .connecting: "Roam Control is preparing the secure device session."
case .active: "Roam Control is controlling the session."
case .failed(let message): message
case .failed(let message): LocalizedStringKey(message)
}
}

Expand Down
2 changes: 1 addition & 1 deletion RoamControl/Features/Map/ConnectionBadge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct ConnectionBadge: View {
.accessibilityLabel("Connection status: \(label)")
}

private var label: String {
private var label: LocalizedStringKey {
switch state {
case .notConfigured: "Set up iPhone"
case .ready: "Ready"
Expand Down
6 changes: 3 additions & 3 deletions RoamControl/Features/Map/LocationSelectionCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ struct LocationSelectionCard: View {
return message.localizedCaseInsensitiveContains("Install LocalDevVPN")
}

private var primaryTitle: String {
private var primaryTitle: LocalizedStringKey {
switch sessionPhase {
case .openingLocalDevVPN:
"Opening LocalDevVPN…"
Expand Down Expand Up @@ -329,7 +329,7 @@ struct LocationSelectionCard: View {
}
}

private var statusMessage: String {
private var statusMessage: LocalizedStringKey {
switch sessionPhase {
case .idle:
return isPaired
Expand All @@ -349,7 +349,7 @@ struct LocationSelectionCard: View {
case .stopping:
return "Restoring this iPhone's real location. Keep Roam Control open until this finishes."
case .failed(let message):
return message
return LocalizedStringKey(message)
}
}

Expand Down
66 changes: 37 additions & 29 deletions RoamControl/Features/Map/MapViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreLocation
import MapKit
import Observation
import SwiftUI
Expand Down Expand Up @@ -208,10 +209,10 @@ final class MapViewModel: NSObject, MKLocalSearchCompleterDelegate {
return
}

let coordinate = item.location.coordinate
let coordinate = item.placemark.coordinate
let target = LocationTarget(
name: item.name ?? searchQuery,
subtitle: placeDescription(for: item),
subtitle: placeDescription(for: item.placemark),
latitude: coordinate.latitude,
longitude: coordinate.longitude
)
Expand Down Expand Up @@ -255,25 +256,16 @@ final class MapViewModel: NSObject, MKLocalSearchCompleterDelegate {
)
}

guard let request = MKReverseGeocodingRequest(
location: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
) else {
selectedLocation = LocationTarget(
name: fallbackName,
subtitle: fallbackDescription,
latitude: coordinate.latitude,
longitude: coordinate.longitude
)
return
}
let geocoder = CLGeocoder()
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)

do {
let items = try await request.mapItems
guard selectedLocation?.id == pendingTarget.id, let item = items.first else { return }
let placemarks = try await geocoder.reverseGeocodeLocation(location)
guard selectedLocation?.id == pendingTarget.id, let placemark = placemarks.first else { return }

selectedLocation = LocationTarget(
name: item.name ?? fallbackName,
subtitle: placeDescription(for: item),
name: placemark.name ?? fallbackName,
subtitle: placeDescription(for: placemark),
latitude: coordinate.latitude,
longitude: coordinate.longitude
)
Expand Down Expand Up @@ -346,22 +338,38 @@ final class MapViewModel: NSObject, MKLocalSearchCompleterDelegate {
}
}

private func placeDescription(for item: MKMapItem) -> String {
private func placeDescription(for placemark: CLPlacemark) -> String {
let invisibleCharacters = CharacterSet.whitespacesAndNewlines.union(
CharacterSet(charactersIn: "\u{200B}\u{200C}\u{200D}\u{FEFF}")
)
let candidates = [
item.address?.shortAddress,
item.addressRepresentations?.cityWithContext,
item.address?.fullAddress
]

for candidate in candidates {
let detail = candidate?.trimmingCharacters(in: invisibleCharacters) ?? ""
if !detail.isEmpty {
return detail
}
var addressParts: [String] = []
if let subThoroughfare = placemark.subThoroughfare, let thoroughfare = placemark.thoroughfare {
addressParts.append("\(subThoroughfare) \(thoroughfare)")
} else if let thoroughfare = placemark.thoroughfare {
addressParts.append(thoroughfare)
}
if let subLocality = placemark.subLocality {
addressParts.append(subLocality)
}
if let locality = placemark.locality {
addressParts.append(locality)
}
if let administrativeArea = placemark.administrativeArea {
addressParts.append(administrativeArea)
}
if let country = placemark.country {
addressParts.append(country)
}

let detail = addressParts.joined(separator: ", ").trimmingCharacters(in: invisibleCharacters)
if !detail.isEmpty {
return detail
}

if let name = placemark.name?.trimmingCharacters(in: invisibleCharacters), !name.isEmpty {
return name
}

return "Location details unavailable"
}

Expand Down
8 changes: 4 additions & 4 deletions RoamControl/Features/Map/SavedPlacesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,23 +186,23 @@ struct SavedPlacesView: View {
favouriteBeingRenamed = location
}

private var clearConfirmationTitle: String {
private var clearConfirmationTitle: LocalizedStringKey {
switch clearTarget {
case .favourites: "Clear all favourites?"
case .history: "Clear location history?"
case nil: "Clear saved places?"
}
}

private var clearConfirmationButton: String {
private var clearConfirmationButton: LocalizedStringKey {
switch clearTarget {
case .favourites: "Clear Favourites"
case .history: "Clear History"
case nil: "Clear"
}
}

private var clearConfirmationMessage: String {
private var clearConfirmationMessage: LocalizedStringKey {
switch clearTarget {
case .favourites: "Every favourite will be removed. Your history will be kept."
case .history: "Every recently used location will be removed. Your favourites will be kept."
Expand Down Expand Up @@ -275,7 +275,7 @@ private struct SavedPlaceRow: View {

private struct EmptySavedPlacesRow: View {
let symbol: String
let message: String
let message: LocalizedStringKey

var body: some View {
Label(message, systemImage: symbol)
Expand Down
10 changes: 6 additions & 4 deletions RoamControl/Features/Map/WalkingRoutePlanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ final class WalkingRoutePlanner {
let request = MKDirections.Request()
if let source {
request.source = MKMapItem(
location: CLLocation(latitude: source.latitude, longitude: source.longitude),
address: nil
placemark: MKPlacemark(
coordinate: CLLocationCoordinate2D(latitude: source.latitude, longitude: source.longitude)
)
)
} else {
request.source = .forCurrentLocation()
}
request.destination = MKMapItem(
location: CLLocation(latitude: target.latitude, longitude: target.longitude),
address: nil
placemark: MKPlacemark(
coordinate: CLLocationCoordinate2D(latitude: target.latitude, longitude: target.longitude)
)
)
request.transportType = .walking
request.requestsAlternateRoutes = false
Expand Down
11 changes: 6 additions & 5 deletions RoamControl/Features/Map/WalkingRoutePreviewCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,18 @@ struct WalkingRoutePreviewCard: View {
if dynamicTypeSize.isAccessibilitySize {
Picker("Walking pace", selection: paceBinding) {
ForEach(WalkingPace.allCases) { pace in
Text(pace.title).tag(pace)
Text(LocalizedStringKey(pace.title)).tag(pace)
}
}
.pickerStyle(.menu)
} else {
Picker("Walking pace", selection: paceBinding) {
ForEach(WalkingPace.allCases) { pace in
Text(pace.title).tag(pace)
Text(LocalizedStringKey(pace.title)).tag(pace)
}
}
.pickerStyle(.segmented)
.labelsHidden()
}
}

Expand Down Expand Up @@ -359,7 +360,7 @@ struct WalkingRoutePreviewCard: View {
}
}

private var phaseTitle: String {
private var phaseTitle: LocalizedStringKey {
switch simulation.phase {
case .idle: "Walking route"
case .preparing: "Preparing walk"
Expand All @@ -371,7 +372,7 @@ struct WalkingRoutePreviewCard: View {
}
}

private var phaseSubtitle: String {
private var phaseSubtitle: LocalizedStringKey {
switch simulation.phase {
case .idle, .preparing, .failed:
"Current Location to \(destination.name)"
Expand Down Expand Up @@ -467,7 +468,7 @@ struct WalkingRoutePreviewCard: View {
}

private struct RouteMetric: View {
let title: String
let title: LocalizedStringKey
let value: String
let symbol: String

Expand Down
Loading