From bc424e5fb31490f5a180f6bfa0414beeae53864b Mon Sep 17 00:00:00 2001 From: Krystof Date: Tue, 5 Nov 2024 21:25:59 +0100 Subject: [PATCH] feat(ios): platforms list --- .../common/components/route-name.view.swift | 29 +++ .../common/utils/metro-line.utils.swift | 4 + .../metro-now.xcodeproj/project.pbxproj | 28 +-- .../metro-now/metro-now/ContentView.swift | 165 +++++++++++++++--- 4 files changed, 180 insertions(+), 46 deletions(-) create mode 100644 apps/mobile/metro-now/common/components/route-name.view.swift diff --git a/apps/mobile/metro-now/common/components/route-name.view.swift b/apps/mobile/metro-now/common/components/route-name.view.swift new file mode 100644 index 0000000..f7f9ab7 --- /dev/null +++ b/apps/mobile/metro-now/common/components/route-name.view.swift @@ -0,0 +1,29 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI + +struct RouteNameIconView: View { + let systemName: String + let background: Color + + var body: some View { + Image(systemName: systemName) // TODO: replace with Text component for more flexibility + .imageScale(.medium) + .padding(5) + .foregroundStyle(.white) + .background(background) + .clipShape(.rect(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(.white, lineWidth: 2) + ) + } +} + +#Preview { + RouteNameIconView( + systemName: "a", + background: .green + ) +} diff --git a/apps/mobile/metro-now/common/utils/metro-line.utils.swift b/apps/mobile/metro-now/common/utils/metro-line.utils.swift index 8a7f0b4..e71f853 100644 --- a/apps/mobile/metro-now/common/utils/metro-line.utils.swift +++ b/apps/mobile/metro-now/common/utils/metro-line.utils.swift @@ -11,3 +11,7 @@ func getMetroLineColor(_ line: MetroLine?) -> Color? { default: nil } } + +func getMetroLineColor(_ line: String) -> Color? { + getMetroLineColor(MetroLine(rawValue: line.uppercased())) +} diff --git a/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj b/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj index dc39206..9cb89fa 100644 --- a/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj +++ b/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj @@ -40,24 +40,11 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 2D9601C72CC812D6000EF3D5 /* Exceptions for "common" folder in "metro-now" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - components/countdown.view.swift, - "const/api-const.swift", - "managers/location-manager.swift", - "managers/network-manager.swift", - "types/api-types.swift", - "types/metro-line.swift", - "utils/metro-line.utils.swift", - utils/station.utils.swift, - ); - target = 2D001BA72CC8099B00C6B4F8 /* metro-now */; - }; 2D9601C92CC812EF000EF3D5 /* Exceptions for "common" folder in "metro-now Watch App" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( components/countdown.view.swift, + "components/route-name.view.swift", "const/api-const.swift", "managers/location-manager.swift", "managers/network-manager.swift", @@ -84,7 +71,6 @@ 2D9601C12CC8126F000EF3D5 /* common */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - 2D9601C72CC812D6000EF3D5 /* Exceptions for "common" folder in "metro-now" target */, 2D9601C92CC812EF000EF3D5 /* Exceptions for "common" folder in "metro-now Watch App" target */, ); path = common; @@ -187,7 +173,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1600; - LastUpgradeCheck = 1600; + LastUpgradeCheck = 1610; TargetAttributes = { 2D001BA72CC8099B00C6B4F8 = { CreatedOnToolsVersion = 16.0; @@ -294,6 +280,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -355,6 +342,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -443,7 +431,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0.2; DEVELOPMENT_ASSET_PATHS = "\"metro-now/Preview Content\""; DEVELOPMENT_TEAM = R6WU5ABNG2; ENABLE_PREVIEWS = YES; @@ -456,7 +444,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 18; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -477,7 +465,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0.2; DEVELOPMENT_ASSET_PATHS = "\"metro-now/Preview Content\""; DEVELOPMENT_TEAM = R6WU5ABNG2; ENABLE_PREVIEWS = YES; @@ -490,7 +478,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 18; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/apps/mobile/metro-now/metro-now/ContentView.swift b/apps/mobile/metro-now/metro-now/ContentView.swift index 2451096..3264fbb 100644 --- a/apps/mobile/metro-now/metro-now/ContentView.swift +++ b/apps/mobile/metro-now/metro-now/ContentView.swift @@ -1,46 +1,159 @@ // metro-now // https://github.com/krystxf/metro-now -import SwiftUI - import CoreLocation import Foundation +import SwiftUI + +struct ContentView: View { + @StateObject private var locationManager = LocationManager() + @State var stops: [ApiStop]? = nil + @State var departures: [ApiDeparture]? = nil + private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect() + + var body: some View { + NavigationStack { + if let location = locationManager.location, + let stops, + let closestStop = findClosestStop(to: location, stops: stops) + + { + List(closestStop.platforms, id: \.id) { platform in + let icon = RouteNameIconView( + systemName: platform + .routes[0].name + .lowercased(), + background: getMetroLineColor(platform + .routes[0].name) ?? .black + ) + + if let departures { + let platformDepartures = departures.filter { departure in + departure.platformId == platform.id + } -class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { - private let locationManager = CLLocationManager() + VStack(alignment: .trailing) { + if platformDepartures.count > 0 { + HStack { + icon - @Published var location: CLLocation? + Text(platformDepartures[0].headsign) + Spacer() + CountdownView( + targetDate: platformDepartures[0].departure.predicted + ) + } + } + if platformDepartures.count > 1 { + if platformDepartures[0].headsign != platformDepartures[1].headsign { + HStack { + Text(platformDepartures[1].headsign) + Spacer() + CountdownView( + targetDate: platformDepartures[1].departure.predicted + ) + } - override init() { - super.init() - locationManager.delegate = self - locationManager.desiredAccuracy = kCLLocationAccuracyBest - locationManager.requestWhenInUseAuthorization() - locationManager.startUpdatingLocation() + } else { + CountdownView( + targetDate: platformDepartures[1].departure.predicted + ) { "Also in \($0)" } + } + } + } + } else { + VStack { + HStack { + icon + + Text("Loading...") + .redacted(reason: .placeholder) + Spacer() + Text("--m --s") + .redacted(reason: .placeholder) + } + HStack { + Spacer() + Text("also in --m --s") + .redacted(reason: .placeholder) + } + } + } + } + .navigationTitle(closestStop.name) + } else { + ProgressView() + } + } + .onAppear { + getAllMetroStops() + } + .onReceive(timer) { _ in + getStopDepartures() + } } - func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - if let location = locations.first { - DispatchQueue.main.async { - self.location = location + func findClosestStop(to location: CLLocation, stops: [ApiStop]) -> ApiStop? { + var closestStop: ApiStop? + var closestDistance: CLLocationDistance? + + for stop in stops { + let stopLocation = CLLocation(latitude: stop.avgLatitude, longitude: stop.avgLongitude) + + let distance = location.distance(from: stopLocation) + + guard closestDistance != nil else { + closestStop = stop + closestDistance = distance + continue + } + + if distance < closestDistance! { + closestStop = stop + closestDistance = distance } } + + return closestStop } -} -struct ContentView: View { - @StateObject private var locationManager = LocationManager() + func getAllMetroStops() { + NetworkManager.shared.getMetroStops { result in + DispatchQueue.main.async { + switch result { + case let .success(stops): - var body: some View { - VStack { - if let location = locationManager.location { - Text("Latitude: \(location.coordinate.latitude)") - Text("Longitude: \(location.coordinate.longitude)") - } else { - Text("Fetching location...") + self.stops = stops + + case let .failure(error): + print(error.localizedDescription) + } } } - .padding() + } + + func getStopDepartures() { + guard + let location = locationManager.location, + let stops, + let closestStop = findClosestStop(to: location, stops: stops) + else { + return + } + + NetworkManager.shared + .getDepartures(stopIds: [closestStop.id], platformIds: []) { result in + DispatchQueue.main.async { + switch result { + case let .success(departures): + + self.departures = departures + + case let .failure(error): + print(error.localizedDescription) + } + } + } } }