From 0dd31e79550f7079231bc6270f53a493deb5ff43 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 | 56 ++++++++++ .../common/utils/metro-line.utils.swift | 4 + .../pages/stop/list-item.view.swift | 2 - .../metro-now.xcodeproj/project.pbxproj | 38 +++---- .../metro-now/metro-now/ContentView.swift | 105 +++++++++++++----- ...stop-page-list-item-placeholder.view.swift | 63 +++++++++++ .../closest-stop-page-list-item.view.swift | 73 ++++++++++++ .../closest-stop/closest-stop-page.view.swift | 35 ++++++ 8 files changed, 324 insertions(+), 52 deletions(-) create mode 100644 apps/mobile/metro-now/common/components/route-name.view.swift create mode 100644 apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item-placeholder.view.swift create mode 100644 apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item.view.swift create mode 100644 apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page.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 00000000..74570d87 --- /dev/null +++ b/apps/mobile/metro-now/common/components/route-name.view.swift @@ -0,0 +1,56 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI + +struct RouteNameIconView: View { + let label: String + let background: Color + + var body: some View { + Text(label.uppercased()) + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(.white) + .fixedSize(horizontal: true, vertical: true) + .frame(width: 26, height: 26) + .background(Rectangle().fill(background)) + .clipShape(.rect(cornerRadius: 6)) + } +} + +#Preview { + RouteNameIconView( + label: "a", + background: .green + ) + + RouteNameIconView( + label: "b", + background: .yellow + ) + + RouteNameIconView( + label: "c", + background: .red + ) + + RouteNameIconView( + label: "28", + background: .purple + ) + + RouteNameIconView( + label: "99", + background: .black + ) + + RouteNameIconView( + label: "149", + background: .blue + ) + + RouteNameIconView( + label: "912", + background: .black + ) +} 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 8a7f0b4d..e71f853f 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 Watch App/pages/stop/list-item.view.swift b/apps/mobile/metro-now/metro-now Watch App/pages/stop/list-item.view.swift index c6363fb8..de3e39ec 100644 --- a/apps/mobile/metro-now/metro-now Watch App/pages/stop/list-item.view.swift +++ b/apps/mobile/metro-now/metro-now Watch App/pages/stop/list-item.view.swift @@ -12,8 +12,6 @@ struct StopDepartureListItemView: View { let nextHeadsign: String? let nextDeparture: Date? - let dateFormat = DateFormatter() - init( color: Color?, headsign: String, diff --git a/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj b/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj index dc39206f..257ac449 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; @@ -380,7 +368,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_ASSET_PATHS = "\"metro-now Watch App/Preview Content\""; DEVELOPMENT_TEAM = R6WU5ABNG2; ENABLE_PREVIEWS = YES; @@ -389,6 +377,7 @@ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "com.krystof.metro-now"; + INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -411,7 +400,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_ASSET_PATHS = "\"metro-now Watch App/Preview Content\""; DEVELOPMENT_TEAM = R6WU5ABNG2; ENABLE_PREVIEWS = YES; @@ -420,6 +409,7 @@ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "com.krystof.metro-now"; + INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -443,7 +433,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_ASSET_PATHS = "\"metro-now/Preview Content\""; DEVELOPMENT_TEAM = R6WU5ABNG2; ENABLE_PREVIEWS = YES; @@ -456,12 +446,12 @@ 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", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.2; PRODUCT_BUNDLE_IDENTIFIER = "com.krystof.metro-now"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -477,7 +467,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_ASSET_PATHS = "\"metro-now/Preview Content\""; DEVELOPMENT_TEAM = R6WU5ABNG2; ENABLE_PREVIEWS = YES; @@ -490,12 +480,12 @@ 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", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.2; PRODUCT_BUNDLE_IDENTIFIER = "com.krystof.metro-now"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/mobile/metro-now/metro-now/ContentView.swift b/apps/mobile/metro-now/metro-now/ContentView.swift index 24510966..06da3821 100644 --- a/apps/mobile/metro-now/metro-now/ContentView.swift +++ b/apps/mobile/metro-now/metro-now/ContentView.swift @@ -1,46 +1,99 @@ // metro-now // https://github.com/krystxf/metro-now +import CoreLocation import SwiftUI -import CoreLocation -import Foundation +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() -class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { - private let locationManager = CLLocationManager() + var body: some View { + NavigationStack { + if let location = locationManager.location, + let stops, + let closestStop = findClosestStop(to: location, stops: stops) + { + ClosestStopPageView( + closestStop: closestStop, + departures: departures + ) + .navigationTitle(closestStop.name) + } else { + ProgressView() + } + } + .onAppear { + getAllMetroStops() + } + .onReceive(timer) { _ in + getStopDepartures() + } + } + + func findClosestStop(to location: CLLocation, stops: [ApiStop]) -> ApiStop? { + var closestStop: ApiStop? + var closestDistance: CLLocationDistance? - @Published var location: CLLocation? + 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 + } - override init() { - super.init() - locationManager.delegate = self - locationManager.desiredAccuracy = kCLLocationAccuracyBest - locationManager.requestWhenInUseAuthorization() - locationManager.startUpdatingLocation() + if distance < closestDistance! { + closestStop = stop + closestDistance = distance + } + } + + return closestStop } - func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - if let location = locations.first { + func getAllMetroStops() { + NetworkManager.shared.getMetroStops { result in DispatchQueue.main.async { - self.location = location + switch result { + case let .success(stops): + + self.stops = stops + + case let .failure(error): + print(error.localizedDescription) + } } } } -} -struct ContentView: View { - @StateObject private var locationManager = LocationManager() + func getStopDepartures() { + guard + let location = locationManager.location, + let stops, + let closestStop = findClosestStop(to: location, stops: stops) + else { + return + } - var body: some View { - VStack { - if let location = locationManager.location { - Text("Latitude: \(location.coordinate.latitude)") - Text("Longitude: \(location.coordinate.longitude)") - } else { - Text("Fetching location...") + 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) + } + } } - } - .padding() } } diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item-placeholder.view.swift b/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item-placeholder.view.swift new file mode 100644 index 00000000..09987464 --- /dev/null +++ b/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item-placeholder.view.swift @@ -0,0 +1,63 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI + +struct ClosestStopPageListItemPlaceholderView: View { + let routeLabel: String + let routeLabelBackground: Color + + var body: some View { + ClosestStopPageListItemView( + routeLabel: routeLabel, + routeLabelBackground: routeLabelBackground, + headsign: "Loading...", + departure: .now + 10 * 60, + nextHeadsign: "Loading..", + nextDeparture: .now + 15 * 60 + ) + .redacted(reason: .placeholder) + } +} + +#Preview("signle metro line") { + List { + ClosestStopPageListItemPlaceholderView( + routeLabel: "A", + routeLabelBackground: .green + ) + ClosestStopPageListItemPlaceholderView( + routeLabel: "A", + routeLabelBackground: .green + ) + ClosestStopPageListItemPlaceholderView( + routeLabel: "C", + routeLabelBackground: .red + ) + ClosestStopPageListItemPlaceholderView( + routeLabel: "C", + routeLabelBackground: .red + ) + } +} + +#Preview("multiple metro lines") { + List { + ClosestStopPageListItemPlaceholderView( + routeLabel: "A", + routeLabelBackground: .green + ) + ClosestStopPageListItemPlaceholderView( + routeLabel: "A", + routeLabelBackground: .green + ) + ClosestStopPageListItemPlaceholderView( + routeLabel: "C", + routeLabelBackground: .red + ) + ClosestStopPageListItemPlaceholderView( + routeLabel: "C", + routeLabelBackground: .red + ) + } +} diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item.view.swift b/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item.view.swift new file mode 100644 index 00000000..59115bb6 --- /dev/null +++ b/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item.view.swift @@ -0,0 +1,73 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI + +struct ClosestStopPageListItemView: View { + let routeLabel: String + let routeLabelBackground: Color? + + let headsign: String + let departure: Date + + let nextHeadsign: String? + let nextDeparture: Date? + + var body: some View { + HStack( + alignment: .top, + spacing: 8 + ) { + RouteNameIconView( + label: routeLabel, + background: routeLabelBackground ?? Color.black + ) + + VStack(alignment: .trailing, spacing: 4) { + HStack { + Text(headsign) + Spacer() + CountdownView(targetDate: departure) + } + + if let nextHeadsign, let nextDeparture { + HStack { + if headsign != nextHeadsign { + Text(nextHeadsign) + } + Spacer() + CountdownView( + targetDate: nextDeparture + ) { + headsign == nextHeadsign ? $0 : "Also in \($0)" + } + } + .foregroundStyle(.secondary) + .font(.footnote) + } + } + .fontWeight(.semibold) + } + } +} + +#Preview("single metro line") { + List { + ClosestStopPageListItemView( + routeLabel: "A", + routeLabelBackground: .green, + headsign: "Nemocnice Motol", + departure: .now + 10 * 60, + nextHeadsign: "Nemocnice Motol", + nextDeparture: .now + 15 * 60 + ) + ClosestStopPageListItemView( + routeLabel: "A", + routeLabelBackground: .green, + headsign: "Depo Hostivaƙ", + departure: .now + 10 * 60, + nextHeadsign: "Skalka", + nextDeparture: .now + 15 * 60 + ) + } +} diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page.view.swift b/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page.view.swift new file mode 100644 index 00000000..4965b70d --- /dev/null +++ b/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page.view.swift @@ -0,0 +1,35 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI + +struct ClosestStopPageView: View { + let closestStop: ApiStop + let departures: [ApiDeparture]? + + var body: some View { + List(closestStop.platforms, id: \.id) { platform in + let routeLabel: String = platform.routes[0].name + let routeLabelBackground: Color = getMetroLineColor(routeLabel) ?? .black + let platformDepartures: [ApiDeparture]? = departures?.filter { departure in + departure.platformId == platform.id + } + + if let platformDepartures, platformDepartures.count > 0 { + ClosestStopPageListItemView( + routeLabel: routeLabel, + routeLabelBackground: routeLabelBackground, + headsign: platformDepartures[0].headsign, + departure: platformDepartures[0].departure.predicted, + nextHeadsign: platformDepartures[1].headsign, + nextDeparture: platformDepartures[1].departure.scheduled + ) + } else { + ClosestStopPageListItemPlaceholderView( + routeLabel: routeLabel, + routeLabelBackground: routeLabelBackground + ) + } + } + } +}