From 5e24538963b8310dec8e04ed15a5030f8e095878 Mon Sep 17 00:00:00 2001 From: hilmyveradin Date: Sun, 4 Aug 2024 23:34:36 +0700 Subject: [PATCH 1/3] modify trip planner view --- OTPKit/Controls/PageHeaderView.swift | 1 - OTPKit/Controls/SectionHeaderView.swift | 1 - .../OriginDestination/FavoriteView.swift | 3 +- .../OriginDestinationView.swift | 4 +- .../Sheets/AddFavoriteLocationsSheet.swift | 4 +- .../Sheets/FavoriteLocationsSheet.swift | 2 +- .../TripPlanner/TripPlannerSheetView.swift | 109 ++++++++++++++++-- .../TripPlanner/TripPlannerView.swift | 4 +- OTPKit/Miscellaneous/FlowLayout.swift | 45 ++++++++ 9 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 OTPKit/Miscellaneous/FlowLayout.swift diff --git a/OTPKit/Controls/PageHeaderView.swift b/OTPKit/Controls/PageHeaderView.swift index 2e6b358..1fb5742 100644 --- a/OTPKit/Controls/PageHeaderView.swift +++ b/OTPKit/Controls/PageHeaderView.swift @@ -9,7 +9,6 @@ import SwiftUI /// Appears at the top of UI pages with a title and close button. struct PageHeaderView: View { - private let text: String private let action: VoidBlock? diff --git a/OTPKit/Controls/SectionHeaderView.swift b/OTPKit/Controls/SectionHeaderView.swift index 3a9a33c..317f5e0 100644 --- a/OTPKit/Controls/SectionHeaderView.swift +++ b/OTPKit/Controls/SectionHeaderView.swift @@ -10,7 +10,6 @@ import SwiftUI /// The view that appears above a section on the `OriginDestinationSheetView`. /// For instance, the header for the Recents and Favorites sections. struct SectionHeaderView: View { - private let text: String private let action: VoidBlock? diff --git a/OTPKit/Features/OriginDestination/FavoriteView.swift b/OTPKit/Features/OriginDestination/FavoriteView.swift index 7a6c181..c8d47d6 100644 --- a/OTPKit/Features/OriginDestination/FavoriteView.swift +++ b/OTPKit/Features/OriginDestination/FavoriteView.swift @@ -18,6 +18,7 @@ struct FavoriteView: View { self.imageName = imageName self.action = action } + var body: some View { Button(action: { action?() @@ -35,7 +36,7 @@ struct FavoriteView: View { .truncationMode(.tail) } .padding(.all, 4) - .foregroundStyle(.black) + .foregroundStyle(.foreground) }) } } diff --git a/OTPKit/Features/OriginDestination/OriginDestinationView.swift b/OTPKit/Features/OriginDestination/OriginDestinationView.swift index 77e6623..0c964c7 100644 --- a/OTPKit/Features/OriginDestination/OriginDestinationView.swift +++ b/OTPKit/Features/OriginDestination/OriginDestinationView.swift @@ -27,7 +27,6 @@ public struct OriginDestinationView: View { }, label: { HStack(spacing: 16) { Image(systemName: "paperplane.fill") - .foregroundColor(.white) .background( Circle() .fill(Color.green) @@ -36,6 +35,7 @@ public struct OriginDestinationView: View { Text(locationManagerService.originName) } }) + .foregroundStyle(.foreground) Button(action: { sheetEnvironment.isSheetOpened.toggle() @@ -43,7 +43,6 @@ public struct OriginDestinationView: View { }, label: { HStack(spacing: 16) { Image(systemName: "mappin") - .foregroundColor(.white) .background( Circle() .fill(Color.green) @@ -52,6 +51,7 @@ public struct OriginDestinationView: View { Text(locationManagerService.destinationName) } }) + .foregroundStyle(.foreground) } .frame(height: 135) .scrollContentBackground(.hidden) diff --git a/OTPKit/Features/OriginDestination/Sheets/AddFavoriteLocationsSheet.swift b/OTPKit/Features/OriginDestination/Sheets/AddFavoriteLocationsSheet.swift index 958f05e..402d2cb 100644 --- a/OTPKit/Features/OriginDestination/Sheets/AddFavoriteLocationsSheet.swift +++ b/OTPKit/Features/OriginDestination/Sheets/AddFavoriteLocationsSheet.swift @@ -60,7 +60,7 @@ public struct AddFavoriteLocationsSheet: View { Text(userLocation.title) .font(.headline) Text(userLocation.subTitle) - }.foregroundStyle(Color.black) + }.foregroundStyle(.foreground) Spacer() @@ -91,7 +91,7 @@ public struct AddFavoriteLocationsSheet: View { Text(location.title) .font(.headline) Text(location.subTitle) - }.foregroundStyle(Color.black) + }.foregroundStyle(.foreground) Spacer() diff --git a/OTPKit/Features/OriginDestination/Sheets/FavoriteLocationsSheet.swift b/OTPKit/Features/OriginDestination/Sheets/FavoriteLocationsSheet.swift index 518bd23..8f0b146 100644 --- a/OTPKit/Features/OriginDestination/Sheets/FavoriteLocationsSheet.swift +++ b/OTPKit/Features/OriginDestination/Sheets/FavoriteLocationsSheet.swift @@ -33,7 +33,7 @@ public struct FavoriteLocationsSheet: View { .font(.headline) Text(location.subTitle) } - .foregroundStyle(Color.black) + .foregroundStyle(.foreground) }) } } diff --git a/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift b/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift index d0eb52a..bcf2f0f 100644 --- a/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift +++ b/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift @@ -15,41 +15,130 @@ public struct TripPlannerSheetView: View { private func formatTimeDuration(_ duration: Int) -> String { if duration < 60 { - return "Total duration: \(duration) second\(duration > 1 ? "s" : "")" + return "\(duration) second\(duration > 1 ? "s" : "")" } else if duration < 3600 { let minutes = Double(duration) / 60 - return String(format: "Total duration: %.1f minutes", minutes) + return String(format: "%.1f min", minutes) } else { let hours = Double(duration) / 3600 - return String(format: "Total duration: %.1f hours", hours) + return String(format: "%.1f hours", hours) } } private func formatDistance(_ distance: Int) -> String { if distance < 1000 { - return "Total distance: \(distance) meters" + return "\(distance) meters" } else { let miles = Double(distance) / 1609.34 - return String(format: "Total distance: %.1f miles", miles) + return String(format: "%.1f miles", miles) } } + private func formatBusSchedule(_ date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "h:mm a" + dateFormatter.amSymbol = "AM" + dateFormatter.pmSymbol = "PM" + + let formattedTime = dateFormatter.string(from: date) + + return "Bus scheduled at \(formattedTime)" + } + public var body: some View { VStack { if let itineraries = locationManagerService.planResponse?.plan?.itineraries { List(itineraries, id: \.self) { itinerary in - let distance = itinerary.legs.map(\.distance).reduce(0, +) Button(action: { locationManagerService.selectedItinerary = itinerary locationManagerService.planResponse = nil dismiss() }, label: { - VStack(alignment: .leading) { - Text(formatTimeDuration(itinerary.duration)) - Text(formatDistance(Int(distance))) + HStack(spacing: 20) { + VStack(alignment: .leading) { + Text(formatTimeDuration(itinerary.duration)) + .font(.title) + .fontWeight(.bold) + .foregroundStyle(.foreground) + Text(formatBusSchedule(itinerary.startTime)) + .foregroundStyle(.gray) + + FlowLayout { + ForEach(Array(zip(itinerary.legs.indices, itinerary.legs)), id: \.1) { index, leg in + switch leg.mode { + case "TRAM": + VStack { + HStack(spacing: 4) { + Text(leg.route ?? "") + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.blue) + .foregroundStyle(.foreground) + .font(.caption) + .clipShape(RoundedRectangle(cornerRadius: 4)) + + Image(systemName: "tram") + .foregroundStyle(.foreground) + } + }.frame(height: 40) + + case "BUS": + HStack(spacing: 4) { + Text(leg.route ?? "") + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.green) + .foregroundStyle(.foreground) + .font(.caption) + .clipShape(RoundedRectangle(cornerRadius: 4)) + + Image(systemName: "bus") + .foregroundStyle(.foreground) + }.frame(height: 40) + + case "WALK": + HStack(spacing: 4) { + Image(systemName: "figure.walk") + Text(formatTimeDuration(leg.duration)) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.gray.opacity(0.2)) + .foregroundStyle(.gray) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .frame(height: 40) + + default: + Text("") + } + + if index < itinerary.legs.count - 1 { + VStack { + Image(systemName: "chevron.right.circle.fill") + .frame(width: 8, height: 16) + }.frame(height: 40) + } + } + } + } + + Button(action: { + locationManagerService.selectedItinerary = itinerary + locationManagerService.planResponse = nil + dismiss() + }, label: { + Text("Go") + .padding(30) + .background(Color.green) + .foregroundStyle(.foreground) + .font(.title) + .fontWeight(.bold) + .clipShape(RoundedRectangle(cornerRadius: 12)) + }) } }) + .foregroundStyle(.foreground) } } else { Text("Can't find trip planner. Please try another pin point") @@ -63,7 +152,7 @@ public struct TripPlannerSheetView: View { .frame(maxWidth: .infinity) .padding() .background(Color.gray) - .foregroundStyle(Color.white) + .foregroundStyle(.foreground) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal, 16) }) diff --git a/OTPKit/Features/TripPlanner/TripPlannerView.swift b/OTPKit/Features/TripPlanner/TripPlannerView.swift index da2ca31..8342453 100644 --- a/OTPKit/Features/TripPlanner/TripPlannerView.swift +++ b/OTPKit/Features/TripPlanner/TripPlannerView.swift @@ -22,7 +22,7 @@ public struct TripPlannerView: View { .frame(maxWidth: .infinity) .padding() .background(Color.gray) - .foregroundStyle(Color.white) + .foregroundStyle(.foreground) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal, 16) @@ -34,7 +34,7 @@ public struct TripPlannerView: View { .frame(maxWidth: .infinity) .padding() .background(Color.gray) - .foregroundStyle(Color.white) + .foregroundStyle(.foreground) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal, 16) } diff --git a/OTPKit/Miscellaneous/FlowLayout.swift b/OTPKit/Miscellaneous/FlowLayout.swift new file mode 100644 index 0000000..9944568 --- /dev/null +++ b/OTPKit/Miscellaneous/FlowLayout.swift @@ -0,0 +1,45 @@ +// +// FlowLayout.swift +// OTPKit +// +// Created by Hilmy Veradin on 04/08/24. +// + +import SwiftUI + +/// Extension to make adaptive layout +struct FlowLayout: Layout { + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) -> CGSize { + let sizes = subviews.map { $0.sizeThatFits(.unspecified) } + return layout(sizes: sizes, proposal: proposal).size + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) { + let sizes = subviews.map { $0.sizeThatFits(.unspecified) } + let offsets = layout(sizes: sizes, proposal: proposal).offsets + + for (offset, subview) in zip(offsets, subviews) { + subview.place(at: CGPoint(x: bounds.minX + offset.x, y: bounds.minY + offset.y), proposal: .unspecified) + } + } + + private func layout(sizes: [CGSize], proposal: ProposedViewSize) -> (offsets: [CGPoint], size: CGSize) { + let verticalSpacing: CGFloat = 4 + let horizontalSpacing: CGFloat = 8 + var result: [CGPoint] = [] + var currentPosition: CGPoint = .zero + var maxY: CGFloat = 0 + + for size in sizes { + if currentPosition.x + size.width > (proposal.width ?? .infinity) { + currentPosition.x = 0 + currentPosition.y = maxY + verticalSpacing + } + result.append(currentPosition) + currentPosition.x += size.width + horizontalSpacing + maxY = max(maxY, currentPosition.y + size.height) + } + + return (result, CGSize(width: proposal.width ?? .infinity, height: maxY)) + } +} From 5970460fac82576d1193e57b982634a9fdda0ea5 Mon Sep 17 00:00:00 2001 From: hilmyveradin Date: Mon, 5 Aug 2024 21:48:43 +0700 Subject: [PATCH 2/3] dry and add default leg view --- .../TripPlanner/TripPlannerSheetView.swift | 122 +++++++++++------- 1 file changed, 77 insertions(+), 45 deletions(-) diff --git a/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift b/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift index bcf2f0f..1e32501 100644 --- a/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift +++ b/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift @@ -45,6 +45,82 @@ public struct TripPlannerSheetView: View { return "Bus scheduled at \(formattedTime)" } + private func generateTramView(leg: Leg) -> some View { + HStack(spacing: 4) { + Text(leg.route ?? "") + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.blue) + .foregroundStyle(.foreground) + .font(.caption) + .clipShape(RoundedRectangle(cornerRadius: 4)) + + Image(systemName: "tram") + .foregroundStyle(.foreground) + }.frame(height: 40) + } + + private func generateBusView(leg: Leg) -> some View { + HStack(spacing: 4) { + Text(leg.route ?? "") + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.green) + .foregroundStyle(.foreground) + .font(.caption) + .clipShape(RoundedRectangle(cornerRadius: 4)) + + Image(systemName: "bus") + .foregroundStyle(.foreground) + } + .frame(height: 40) + } + + private func generateWalkView(leg: Leg) -> some View { + HStack(spacing: 4) { + Image(systemName: "figure.walk") + Text(formatTimeDuration(leg.duration)) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.gray.opacity(0.2)) + .foregroundStyle(.gray) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .frame(height: 40) + } + + private func generateDefaultView(leg: Leg) -> some View { + HStack(spacing: 4) { + Image(systemName: "questionmark.circle") + .foregroundStyle(.foreground) + Text(formatTimeDuration(leg.duration)) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.gray.opacity(0.2)) + .foregroundStyle(.gray) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .frame(height: 40) + } + + private func generateLegView(leg: Leg) -> some View { + Group { + switch leg.mode { + case "TRAM": + generateTramView(leg: leg) + + case "BUS": + generateBusView(leg: leg) + + case "WALK": + generateWalkView(leg: leg) + + default: + generateDefaultView(leg: leg) + } + } + } + public var body: some View { VStack { if let itineraries = locationManagerService.planResponse?.plan?.itineraries { @@ -65,52 +141,8 @@ public struct TripPlannerSheetView: View { FlowLayout { ForEach(Array(zip(itinerary.legs.indices, itinerary.legs)), id: \.1) { index, leg in - switch leg.mode { - case "TRAM": - VStack { - HStack(spacing: 4) { - Text(leg.route ?? "") - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue) - .foregroundStyle(.foreground) - .font(.caption) - .clipShape(RoundedRectangle(cornerRadius: 4)) - - Image(systemName: "tram") - .foregroundStyle(.foreground) - } - }.frame(height: 40) - case "BUS": - HStack(spacing: 4) { - Text(leg.route ?? "") - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.green) - .foregroundStyle(.foreground) - .font(.caption) - .clipShape(RoundedRectangle(cornerRadius: 4)) - - Image(systemName: "bus") - .foregroundStyle(.foreground) - }.frame(height: 40) - - case "WALK": - HStack(spacing: 4) { - Image(systemName: "figure.walk") - Text(formatTimeDuration(leg.duration)) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.gray.opacity(0.2)) - .foregroundStyle(.gray) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .frame(height: 40) - - default: - Text("") - } + generateLegView(leg: leg) if index < itinerary.legs.count - 1 { VStack { From 7c45a62c0b07f5b36576b0fe5a9756d437c9a736 Mon Sep 17 00:00:00 2001 From: Aaron Brethorst Date: Mon, 5 Aug 2024 14:30:31 -0700 Subject: [PATCH 3/3] DRY up some code and extract reusable functions into separate modules --- .../TripPlanner/ItineraryLegUnknownView.swift | 29 +++++ .../TripPlanner/ItineraryLegVehicleView.swift | 53 +++++++++ .../TripPlanner/ItineraryLegWalkView.swift | 30 +++++ .../TripPlanner/TripPlannerSheetView.swift | 107 +----------------- OTPKit/Miscellaneous/Formatters.swift | 43 +++++++ OTPKit/Previews/PreviewHelpers.swift | 29 +++++ 6 files changed, 190 insertions(+), 101 deletions(-) create mode 100644 OTPKit/Features/TripPlanner/ItineraryLegUnknownView.swift create mode 100644 OTPKit/Features/TripPlanner/ItineraryLegVehicleView.swift create mode 100644 OTPKit/Features/TripPlanner/ItineraryLegWalkView.swift create mode 100644 OTPKit/Miscellaneous/Formatters.swift create mode 100644 OTPKit/Previews/PreviewHelpers.swift diff --git a/OTPKit/Features/TripPlanner/ItineraryLegUnknownView.swift b/OTPKit/Features/TripPlanner/ItineraryLegUnknownView.swift new file mode 100644 index 0000000..a3867e7 --- /dev/null +++ b/OTPKit/Features/TripPlanner/ItineraryLegUnknownView.swift @@ -0,0 +1,29 @@ +// +// ItineraryLegUnknownView.swift +// OTPKit +// +// Created by Aaron Brethorst on 8/5/24. +// + +import SwiftUI + +/// Represents an itinerary leg that uses an unknown method of conveyance. +struct ItineraryLegUnknownView: View { + let leg: Leg + + var body: some View { + HStack(spacing: 4) { + Text("\(leg.mode): \(Formatters.formatTimeDuration(leg.duration))") + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.gray.opacity(0.2)) + .foregroundStyle(.gray) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .frame(height: 40) + } +} + +#Preview { + ItineraryLegUnknownView(leg: PreviewHelpers.buildLeg()) +} diff --git a/OTPKit/Features/TripPlanner/ItineraryLegVehicleView.swift b/OTPKit/Features/TripPlanner/ItineraryLegVehicleView.swift new file mode 100644 index 0000000..efd9c59 --- /dev/null +++ b/OTPKit/Features/TripPlanner/ItineraryLegVehicleView.swift @@ -0,0 +1,53 @@ +// +// ItineraryLegVehicleView.swift +// OTPKit +// +// Created by Aaron Brethorst on 8/5/24. +// + +import SwiftUI + +/// Represents an itinerary leg that uses a vehicular method of conveyance. +struct ItineraryLegVehicleView: View { + let leg: Leg + + var body: some View { + HStack(spacing: 4) { + Text(leg.route ?? "") + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(backgroundColor) + .foregroundStyle(.foreground) + .font(.caption) + .clipShape(RoundedRectangle(cornerRadius: 4)) + + Image(systemName: imageName) + .foregroundStyle(.foreground) + }.frame(height: 40) + } + + private var imageName: String { + if leg.mode == "TRAM" { + return "tram" + } else if leg.mode == "BUS" { + return "bus" + } else { + return "" + } + } + + private var backgroundColor: Color { + if leg.mode == "TRAM" { + return Color.blue + } else if leg.mode == "BUS" { + return Color.green + } else { + return Color.pink + } + } + +} + +#Preview { + ItineraryLegVehicleView(leg: PreviewHelpers.buildLeg()) +} diff --git a/OTPKit/Features/TripPlanner/ItineraryLegWalkView.swift b/OTPKit/Features/TripPlanner/ItineraryLegWalkView.swift new file mode 100644 index 0000000..3217183 --- /dev/null +++ b/OTPKit/Features/TripPlanner/ItineraryLegWalkView.swift @@ -0,0 +1,30 @@ +// +// ItineraryLegWalkView.swift +// OTPKit +// +// Created by Aaron Brethorst on 8/5/24. +// + +import SwiftUI + +/// Represents an itinerary leg that uses a walking method of conveyance. +struct ItineraryLegWalkView: View { + let leg: Leg + + var body: some View { + HStack(spacing: 4) { + Image(systemName: "figure.walk") + Text(Formatters.formatTimeDuration(leg.duration)) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.gray.opacity(0.2)) + .foregroundStyle(.gray) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .frame(height: 40) + } +} + +#Preview { + ItineraryLegWalkView(leg: PreviewHelpers.buildLeg()) +} diff --git a/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift b/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift index 1e32501..3a6e02c 100644 --- a/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift +++ b/OTPKit/Features/TripPlanner/TripPlannerSheetView.swift @@ -13,110 +13,15 @@ public struct TripPlannerSheetView: View { public init() {} - private func formatTimeDuration(_ duration: Int) -> String { - if duration < 60 { - return "\(duration) second\(duration > 1 ? "s" : "")" - } else if duration < 3600 { - let minutes = Double(duration) / 60 - return String(format: "%.1f min", minutes) - } else { - let hours = Double(duration) / 3600 - return String(format: "%.1f hours", hours) - } - } - - private func formatDistance(_ distance: Int) -> String { - if distance < 1000 { - return "\(distance) meters" - } else { - let miles = Double(distance) / 1609.34 - return String(format: "%.1f miles", miles) - } - } - - private func formatBusSchedule(_ date: Date) -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "h:mm a" - dateFormatter.amSymbol = "AM" - dateFormatter.pmSymbol = "PM" - - let formattedTime = dateFormatter.string(from: date) - - return "Bus scheduled at \(formattedTime)" - } - - private func generateTramView(leg: Leg) -> some View { - HStack(spacing: 4) { - Text(leg.route ?? "") - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue) - .foregroundStyle(.foreground) - .font(.caption) - .clipShape(RoundedRectangle(cornerRadius: 4)) - - Image(systemName: "tram") - .foregroundStyle(.foreground) - }.frame(height: 40) - } - - private func generateBusView(leg: Leg) -> some View { - HStack(spacing: 4) { - Text(leg.route ?? "") - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.green) - .foregroundStyle(.foreground) - .font(.caption) - .clipShape(RoundedRectangle(cornerRadius: 4)) - - Image(systemName: "bus") - .foregroundStyle(.foreground) - } - .frame(height: 40) - } - - private func generateWalkView(leg: Leg) -> some View { - HStack(spacing: 4) { - Image(systemName: "figure.walk") - Text(formatTimeDuration(leg.duration)) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.gray.opacity(0.2)) - .foregroundStyle(.gray) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .frame(height: 40) - } - - private func generateDefaultView(leg: Leg) -> some View { - HStack(spacing: 4) { - Image(systemName: "questionmark.circle") - .foregroundStyle(.foreground) - Text(formatTimeDuration(leg.duration)) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.gray.opacity(0.2)) - .foregroundStyle(.gray) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .frame(height: 40) - } - private func generateLegView(leg: Leg) -> some View { Group { switch leg.mode { - case "TRAM": - generateTramView(leg: leg) - - case "BUS": - generateBusView(leg: leg) - + case "BUS", "TRAM": + ItineraryLegVehicleView(leg: leg) case "WALK": - generateWalkView(leg: leg) - + ItineraryLegWalkView(leg: leg) default: - generateDefaultView(leg: leg) + ItineraryLegUnknownView(leg: leg) } } } @@ -132,11 +37,11 @@ public struct TripPlannerSheetView: View { }, label: { HStack(spacing: 20) { VStack(alignment: .leading) { - Text(formatTimeDuration(itinerary.duration)) + Text(Formatters.formatTimeDuration(itinerary.duration)) .font(.title) .fontWeight(.bold) .foregroundStyle(.foreground) - Text(formatBusSchedule(itinerary.startTime)) + Text(Formatters.formatBusSchedule(itinerary.startTime)) .foregroundStyle(.gray) FlowLayout { diff --git a/OTPKit/Miscellaneous/Formatters.swift b/OTPKit/Miscellaneous/Formatters.swift new file mode 100644 index 0000000..790bfb4 --- /dev/null +++ b/OTPKit/Miscellaneous/Formatters.swift @@ -0,0 +1,43 @@ +// +// Formatters.swift +// OTPKit +// +// Created by Aaron Brethorst on 8/5/24. +// + +import SwiftUI + +/// Reusable, commonly-used formatters for dates, durations, and distance. +class Formatters { + static func formatTimeDuration(_ duration: Int) -> String { + if duration < 60 { + return "\(duration) second\(duration > 1 ? "s" : "")" + } else if duration < 3600 { + let minutes = Double(duration) / 60 + return String(format: "%.1f min", minutes) + } else { + let hours = Double(duration) / 3600 + return String(format: "%.1f hours", hours) + } + } + + static func formatDistance(_ distance: Int) -> String { + if distance < 1000 { + return "\(distance) meters" + } else { + let miles = Double(distance) / 1609.34 + return String(format: "%.1f miles", miles) + } + } + + static func formatBusSchedule(_ date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "h:mm a" + dateFormatter.amSymbol = "AM" + dateFormatter.pmSymbol = "PM" + + let formattedTime = dateFormatter.string(from: date) + + return "Bus scheduled at \(formattedTime)" + } +} diff --git a/OTPKit/Previews/PreviewHelpers.swift b/OTPKit/Previews/PreviewHelpers.swift new file mode 100644 index 0000000..2b6dcbb --- /dev/null +++ b/OTPKit/Previews/PreviewHelpers.swift @@ -0,0 +1,29 @@ +// +// PreviewHelpers.swift +// OTPKit +// +// Created by Aaron Brethorst on 8/5/24. +// + +import SwiftUI + +class PreviewHelpers { + static func buildLeg() -> Leg { + return Leg( + startTime: Date(), + endTime: Date(), + mode: "TRAM", + route: nil, + agencyName: nil, + from: Place(name: "foo", lon: 47, lat: -122, vertexType: ""), + to: Place(name: "foo", lon: 47, lat: -122, vertexType: ""), + distance: 100, + transitLeg: false, + duration: 10, + realTime: true, + streetNames: nil, + pathway: nil, + steps: nil + ) + } +}