diff --git a/.gitignore b/.gitignore index 0d7c7ab..18d6b11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.xcuserstate +*.xcuserstate diff --git a/eUberUI.xcodeproj/project.pbxproj b/eUberUI.xcodeproj/project.pbxproj index 1f33a9b..8a2353c 100644 --- a/eUberUI.xcodeproj/project.pbxproj +++ b/eUberUI.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ C91BB1442A16BDC000C0B971 /* RideRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91BB1432A16BDC000C0B971 /* RideRequestView.swift */; }; C97221BD2A169FD40014EABA /* MapViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97221BC2A169FD40014EABA /* MapViewState.swift */; }; + C9AA5E762A1C113E00824737 /* RideType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9AA5E752A1C113E00824737 /* RideType.swift */; }; + C9AA5E782A1CFA5600824737 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9AA5E772A1CFA5600824737 /* Double.swift */; }; + C9AA5E7A2A1CFCD900824737 /* EUberLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9AA5E792A1CFCD900824737 /* EUberLocation.swift */; }; C9CFFE442A15179E00708A15 /* eUberUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CFFE432A15179E00708A15 /* eUberUIApp.swift */; }; C9CFFE482A1517A200708A15 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9CFFE472A1517A200708A15 /* Assets.xcassets */; }; C9CFFE4B2A1517A200708A15 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9CFFE4A2A1517A200708A15 /* Preview Assets.xcassets */; }; @@ -24,6 +27,7 @@ C9CFFE8B2A15410900708A15 /* LocationSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CFFE8A2A15410900708A15 /* LocationSearchView.swift */; }; C9CFFE8D2A15447800708A15 /* LocationSearchResultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CFFE8C2A15447800708A15 /* LocationSearchResultCell.swift */; }; C9CFFE8F2A154F1900708A15 /* LocationSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CFFE8E2A154F1900708A15 /* LocationSearchViewModel.swift */; }; + C9DD5C842A1D0B4500B357DB /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD5C832A1D0B4500B357DB /* Color.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -46,6 +50,9 @@ /* Begin PBXFileReference section */ C91BB1432A16BDC000C0B971 /* RideRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RideRequestView.swift; sourceTree = ""; }; C97221BC2A169FD40014EABA /* MapViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MapViewState.swift; path = ../../../../../../../Users/lucashubert/Downloads/MapViewState.swift; sourceTree = ""; }; + C9AA5E752A1C113E00824737 /* RideType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RideType.swift; path = eUberUI/Core/Trips/View/RideType.swift; sourceTree = SOURCE_ROOT; }; + C9AA5E772A1CFA5600824737 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Double.swift; path = eUberUI/Core/Trips/View/Double.swift; sourceTree = SOURCE_ROOT; }; + C9AA5E792A1CFCD900824737 /* EUberLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EUberLocation.swift; path = eUberUI/Core/Trips/View/EUberLocation.swift; sourceTree = SOURCE_ROOT; }; C9CFFE402A15179E00708A15 /* eUberUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = eUberUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; C9CFFE432A15179E00708A15 /* eUberUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = eUberUIApp.swift; sourceTree = ""; }; C9CFFE472A1517A200708A15 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -65,6 +72,7 @@ C9CFFE8A2A15410900708A15 /* LocationSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchView.swift; sourceTree = ""; }; C9CFFE8C2A15447800708A15 /* LocationSearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchResultCell.swift; sourceTree = ""; }; C9CFFE8E2A154F1900708A15 /* LocationSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchViewModel.swift; sourceTree = ""; }; + C9DD5C832A1D0B4500B357DB /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Color.swift; path = eUberUI/Core/Trips/View/Color.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -181,6 +189,8 @@ isa = PBXGroup; children = ( C97221BC2A169FD40014EABA /* MapViewState.swift */, + C9AA5E752A1C113E00824737 /* RideType.swift */, + C9AA5E792A1CFCD900824737 /* EUberLocation.swift */, ); path = Models; sourceTree = ""; @@ -188,6 +198,8 @@ C9CFFE772A151CB500708A15 /* Extensions */ = { isa = PBXGroup; children = ( + C9AA5E772A1CFA5600824737 /* Double.swift */, + C9DD5C832A1D0B4500B357DB /* Color.swift */, ); path = Extensions; sourceTree = ""; @@ -394,10 +406,14 @@ buildActionMask = 2147483647; files = ( C9CFFE8B2A15410900708A15 /* LocationSearchView.swift in Sources */, + C9AA5E7A2A1CFCD900824737 /* EUberLocation.swift in Sources */, + C9AA5E762A1C113E00824737 /* RideType.swift in Sources */, C9CFFE7D2A151D2400708A15 /* HomeView.swift in Sources */, C9CFFE812A151F7400708A15 /* LocationManager.swift in Sources */, + C9DD5C842A1D0B4500B357DB /* Color.swift in Sources */, C9CFFE502A1517A200708A15 /* eUberUI.xcdatamodeld in Sources */, C9CFFE8F2A154F1900708A15 /* LocationSearchViewModel.swift in Sources */, + C9AA5E782A1CFA5600824737 /* Double.swift in Sources */, C97221BD2A169FD40014EABA /* MapViewState.swift in Sources */, C9CFFE8D2A15447800708A15 /* LocationSearchResultCell.swift in Sources */, C9CFFE442A15179E00708A15 /* eUberUIApp.swift in Sources */, diff --git a/eUberUI/Assets.xcassets/Colors/BackgroundColor.colorset/Contents.json b/eUberUI/Assets.xcassets/Colors/BackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..aa42afa --- /dev/null +++ b/eUberUI/Assets.xcassets/Colors/BackgroundColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.130", + "green" : "0.130", + "red" : "0.130" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/eUberUI/Assets.xcassets/Colors/Contents.json b/eUberUI/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/eUberUI/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/eUberUI/Assets.xcassets/Colors/PrimaryTextColor.colorset/Contents.json b/eUberUI/Assets.xcassets/Colors/PrimaryTextColor.colorset/Contents.json new file mode 100644 index 0000000..8e26632 --- /dev/null +++ b/eUberUI/Assets.xcassets/Colors/PrimaryTextColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.130", + "green" : "0.130", + "red" : "0.130" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/eUberUI/Assets.xcassets/Colors/SecondaryBackgroundColor.colorset/Contents.json b/eUberUI/Assets.xcassets/Colors/SecondaryBackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..6ee398c --- /dev/null +++ b/eUberUI/Assets.xcassets/Colors/SecondaryBackgroundColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.921", + "green" : "0.921", + "red" : "0.921" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.261", + "green" : "0.261", + "red" : "0.261" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/eUberUI/Assets.xcassets/hatchback.imageset/Contents.json b/eUberUI/Assets.xcassets/hatchback.imageset/Contents.json new file mode 100644 index 0000000..ed9916f --- /dev/null +++ b/eUberUI/Assets.xcassets/hatchback.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hatchback.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/eUberUI/Assets.xcassets/hatchback.imageset/hatchback.png b/eUberUI/Assets.xcassets/hatchback.imageset/hatchback.png new file mode 100644 index 0000000..98e9f09 Binary files /dev/null and b/eUberUI/Assets.xcassets/hatchback.imageset/hatchback.png differ diff --git a/eUberUI/Assets.xcassets/motorbike.imageset/Contents.json b/eUberUI/Assets.xcassets/motorbike.imageset/Contents.json new file mode 100644 index 0000000..de7b6d8 --- /dev/null +++ b/eUberUI/Assets.xcassets/motorbike.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "motorbike.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/eUberUI/Assets.xcassets/motorbike.imageset/motorbike.png b/eUberUI/Assets.xcassets/motorbike.imageset/motorbike.png new file mode 100644 index 0000000..fefd65f Binary files /dev/null and b/eUberUI/Assets.xcassets/motorbike.imageset/motorbike.png differ diff --git a/eUberUI/Assets.xcassets/suv.imageset/Contents.json b/eUberUI/Assets.xcassets/suv.imageset/Contents.json new file mode 100644 index 0000000..9bf8087 --- /dev/null +++ b/eUberUI/Assets.xcassets/suv.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "suv.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/eUberUI/Assets.xcassets/suv.imageset/suv.png b/eUberUI/Assets.xcassets/suv.imageset/suv.png new file mode 100644 index 0000000..19352db Binary files /dev/null and b/eUberUI/Assets.xcassets/suv.imageset/suv.png differ diff --git a/eUberUI/Core/Home/View/HomeView.swift b/eUberUI/Core/Home/View/HomeView.swift index dca10e3..61b9022 100644 --- a/eUberUI/Core/Home/View/HomeView.swift +++ b/eUberUI/Core/Home/View/HomeView.swift @@ -9,28 +9,40 @@ import SwiftUI struct HomeView: View { @State private var mapState = MapViewState.noInput + @EnvironmentObject var locationViewModel: LocationSearchViewModel var body: some View { - ZStack(alignment: .top) { - UberMapViewRepresentable(mapState: $mapState) - .ignoresSafeArea() - - if mapState == .searchingForLocation { - LocationSearchView(mapState: $mapState) - } else if mapState == .noInput{ - LocationSearchActivationView() - .padding(.vertical, 72) - .onTapGesture { - withAnimation(.spring()) { - mapState = .searchingForLocation + ZStack(alignment: .bottom) { + ZStack(alignment: .top) { + UberMapViewRepresentable(mapState: $mapState) + .ignoresSafeArea() + + if mapState == .searchingForLocation { + LocationSearchView(mapState: $mapState) + } else if mapState == .noInput{ + LocationSearchActivationView() + .padding(.vertical, 72) + .onTapGesture { + withAnimation(.spring()) { + mapState = .searchingForLocation + } } - } + } + + MapViewActionButton(mapState: $mapState) + .padding(.leading) + .padding(.top, 4) + } + if mapState == .locationSelected || mapState == .polylineAdded { + RideRequestView() + .transition(.move(edge: .bottom)) + } + }.edgesIgnoringSafeArea(.bottom) + .onReceive(LocationManager.shared.$userLocation) { location in + if let location = location { + locationViewModel.userLocation = location + } } - - MapViewActionButton(mapState: $mapState) - .padding(.leading) - .padding(.top, 4) - } } } diff --git a/eUberUI/Core/Home/View/MapViewActionButton.swift b/eUberUI/Core/Home/View/MapViewActionButton.swift index 811e816..2b85f98 100644 --- a/eUberUI/Core/Home/View/MapViewActionButton.swift +++ b/eUberUI/Core/Home/View/MapViewActionButton.swift @@ -9,6 +9,7 @@ import SwiftUI struct MapViewActionButton: View { @Binding var mapState: MapViewState + @EnvironmentObject var viewModel: LocationSearchViewModel var body: some View { Button { @@ -35,7 +36,8 @@ struct MapViewActionButton: View { print("DEBUG: No input") case .searchingForLocation: mapState = .noInput - case .locationSelected: + case .locationSelected, .polylineAdded: + viewModel.selectedEUberLocation = nil mapState = .noInput } } @@ -44,7 +46,7 @@ struct MapViewActionButton: View { switch state { case .noInput: return "line.3.horizontal" - case .searchingForLocation, .locationSelected: + case .searchingForLocation, .locationSelected, .polylineAdded: return "arrow.left" } } diff --git a/eUberUI/Core/Home/View/UberMapViewRepresentable.swift b/eUberUI/Core/Home/View/UberMapViewRepresentable.swift index 931a9bb..b6f8b5c 100644 --- a/eUberUI/Core/Home/View/UberMapViewRepresentable.swift +++ b/eUberUI/Core/Home/View/UberMapViewRepresentable.swift @@ -10,7 +10,7 @@ import MapKit struct UberMapViewRepresentable: UIViewRepresentable { let mapView = MKMapView() - let locationManager = LocationManager() + let locationManager = LocationManager.shared @Binding var mapState: MapViewState @EnvironmentObject var locationViewModel: LocationSearchViewModel @@ -30,13 +30,14 @@ struct UberMapViewRepresentable: UIViewRepresentable { case .noInput: context.coordinator.clearMapViewAndRecenterOnUserLocation() break - case .searchingForLocation: + case .searchingForLocation, .polylineAdded: break case .locationSelected: - if let coordinate = locationViewModel.selectedLocationCoordinate { + if let coordinate = locationViewModel.selectedEUberLocation?.coordinate { context.coordinator.addAndSelectAnnotation(withCoordinate: coordinate) context.coordinator.configurePolyline(withDestinationCoordinate: coordinate) } + break } } @@ -92,36 +93,24 @@ extension UberMapViewRepresentable { anno.coordinate = coordinate self.parent.mapView.addAnnotation(anno) self.parent.mapView.selectAnnotation(anno, animated: true) - - parent.mapView.showAnnotations(parent.mapView.annotations, animated: true) } func configurePolyline(withDestinationCoordinate coordinate: CLLocationCoordinate2D) { guard let userLocationCoordinate = self.userLocationCoordinate else { return } - getDestinationRoute(from: userLocationCoordinate, to: coordinate) { route in + self.parent.locationViewModel.getDestinationRoute(from: userLocationCoordinate, to: coordinate) { route in self.parent.mapView.addOverlay(route.polyline) - } - } - - func getDestinationRoute( - from userLocation: CLLocationCoordinate2D, - to destination: CLLocationCoordinate2D, - completion: @escaping(MKRoute) -> Void - ) { - let userPlacemark = MKPlacemark(coordinate: userLocation) - let userDestination = MKPlacemark(coordinate: destination) - let request = MKDirections.Request() - request.source = MKMapItem(placemark: userPlacemark) - request.destination = MKMapItem(placemark: userDestination) - let direction = MKDirections(request: request) - - direction.calculate { response, error in - if let error = error { - print("DEBUG: Failed to get direction with error \(error.localizedDescription)") - } + self.parent.mapState = .polylineAdded + let rect = self.parent.mapView.mapRectThatFits( + route.polyline.boundingMapRect, + edgePadding: .init( + top: 64, + left: 32, + bottom: 500, + right: 32 + ) + ) + self.parent.mapView.setRegion(MKCoordinateRegion(rect), animated: true) - guard let route = response?.routes.first else { return } - completion(route) } } diff --git a/eUberUI/Core/LocationSearch/View/LocationSearchView.swift b/eUberUI/Core/LocationSearch/View/LocationSearchView.swift index c641cae..80103a7 100644 --- a/eUberUI/Core/LocationSearch/View/LocationSearchView.swift +++ b/eUberUI/Core/LocationSearch/View/LocationSearchView.swift @@ -58,14 +58,16 @@ struct LocationSearchView: View { ForEach(viewModel.resuls, id: \.self) { result in LocationSearchResultCell(title: result.title, subtitle: result.subtitle) .onTapGesture { - mapState = .locationSelected - viewModel.selectLocation(result) + withAnimation { + mapState = .locationSelected + viewModel.selectLocation(result) + } } } } } } - .background(.white) + .background(Color.theme.backgroundColor) .opacity(0.9) } } diff --git a/eUberUI/Core/LocationSearch/ViewModel/LocationSearchViewModel.swift b/eUberUI/Core/LocationSearch/ViewModel/LocationSearchViewModel.swift index 33aad5e..515266a 100644 --- a/eUberUI/Core/LocationSearch/ViewModel/LocationSearchViewModel.swift +++ b/eUberUI/Core/LocationSearch/ViewModel/LocationSearchViewModel.swift @@ -11,7 +11,9 @@ import MapKit class LocationSearchViewModel: NSObject, ObservableObject { @Published var resuls = [MKLocalSearchCompletion]() - @Published var selectedLocationCoordinate: CLLocationCoordinate2D? + @Published var selectedEUberLocation: EUberLocation? + @Published var pickupTime: String? + @Published var dropOffTime: String? private let searchCompleter = MKLocalSearchCompleter() @@ -21,6 +23,8 @@ class LocationSearchViewModel: NSObject, ObservableObject { } } + var userLocation: CLLocationCoordinate2D? + override init() { super.init() searchCompleter.delegate = self @@ -37,7 +41,7 @@ class LocationSearchViewModel: NSObject, ObservableObject { } guard let item = response?.mapItems.first else { return } let coordinate = item.placemark.coordinate - self.selectedLocationCoordinate = coordinate + self.selectedEUberLocation = EUberLocation(title: localSearch.title, coordinate: coordinate) print("DEBUG: Location coordinates \(coordinate)") } @@ -51,6 +55,55 @@ class LocationSearchViewModel: NSObject, ObservableObject { search.start(completionHandler: completion) } + func computeRidePrice(forType type: RideType) -> Double { + guard let destCoordinate = selectedEUberLocation?.coordinate else { return 0.0 } + guard let userCoordinate = self.userLocation else { return 0.0 } + + let userLocation = CLLocation( + latitude: userCoordinate.latitude, + longitude: userCoordinate.longitude + ) + + let destination = CLLocation( + latitude: destCoordinate.latitude, + longitude: destCoordinate.longitude + ) + + let tripDistanceInMeters = userLocation.distance(from: destination) + return type.computePrice(for: tripDistanceInMeters) + } + + func getDestinationRoute( + from userLocation: CLLocationCoordinate2D, + to destination: CLLocationCoordinate2D, + completion: @escaping(MKRoute) -> Void + ) { + let userPlacemark = MKPlacemark(coordinate: userLocation) + let userDestination = MKPlacemark(coordinate: destination) + let request = MKDirections.Request() + request.source = MKMapItem(placemark: userPlacemark) + request.destination = MKMapItem(placemark: userDestination) + let direction = MKDirections(request: request) + + direction.calculate { response, error in + if let error = error { + print("DEBUG: Failed to get direction with error \(error.localizedDescription)") + } + + guard let route = response?.routes.first else { return } + self.configurePickupAndDropOffTimes(with: route.expectedTravelTime) + completion(route) + } + } + + func configurePickupAndDropOffTimes(with expectedTravelTime: Double) { + let formatter = DateFormatter() + formatter.dateFormat = "hh:mm a" + + pickupTime = formatter.string(from: Date()) + dropOffTime = formatter.string(from: Date() + expectedTravelTime) + } + } // MARK: - MKLocalSearchCompleterDelegate diff --git a/eUberUI/Core/Trips/View/Color.swift b/eUberUI/Core/Trips/View/Color.swift new file mode 100644 index 0000000..9e293b5 --- /dev/null +++ b/eUberUI/Core/Trips/View/Color.swift @@ -0,0 +1,18 @@ +// +// Color.swift +// eUberUI +// +// Created by Lucas Hubert on 23/05/23. +// + +import SwiftUI + +extension Color { + static let theme = ColorTheme() +} + +struct ColorTheme { + let backgroundColor = Color("BackgroundColor") + let secondaryBackgroundColor = Color("SecondaryBackgroundColor") + let primaryTextColor = Color("PrimaryTextColor") +} diff --git a/eUberUI/Core/Trips/View/Double.swift b/eUberUI/Core/Trips/View/Double.swift new file mode 100644 index 0000000..fba1230 --- /dev/null +++ b/eUberUI/Core/Trips/View/Double.swift @@ -0,0 +1,22 @@ +// +// Double.swift +// eUberUI +// +// Created by Lucas Hubert on 23/05/23. +// + +import Foundation + +extension Double { + private var currencyFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + return formatter + } + + func toCurrency() -> String { + return currencyFormatter.string(for: self) ?? "" + } +} diff --git a/eUberUI/Core/Trips/View/EUberLocation.swift b/eUberUI/Core/Trips/View/EUberLocation.swift new file mode 100644 index 0000000..8431276 --- /dev/null +++ b/eUberUI/Core/Trips/View/EUberLocation.swift @@ -0,0 +1,13 @@ +// +// EUberLocation.swift +// eUberUI +// +// Created by Lucas Hubert on 23/05/23. +// + +import CoreLocation + +struct EUberLocation { + let title: String + let coordinate: CLLocationCoordinate2D +} diff --git a/eUberUI/Core/Trips/View/RideRequestView.swift b/eUberUI/Core/Trips/View/RideRequestView.swift index be2b976..dac5eee 100644 --- a/eUberUI/Core/Trips/View/RideRequestView.swift +++ b/eUberUI/Core/Trips/View/RideRequestView.swift @@ -8,12 +8,17 @@ import SwiftUI struct RideRequestView: View { + + @State private var selectedRideType: RideType = .eUberX + @EnvironmentObject var locationViewModel: LocationSearchViewModel + var body: some View { VStack { Capsule() .foregroundColor((Color(.systemYellow))) .frame(width: 48, height: 8) - .opacity(0.6) + .opacity(0.3) + .padding(.vertical, 8) HStack { VStack { @@ -40,20 +45,22 @@ struct RideRequestView: View { Spacer() - Text("01:30 PM") + Text(locationViewModel.pickupTime ?? "-") .font(.system(size: 14, weight: .bold)) .foregroundColor(.gray) } .padding(.bottom, 10) HStack { - Text("Destination location") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.gray) + if let location = locationViewModel.selectedEUberLocation { + Text(location.title) + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.gray) + } Spacer() - Text("01:40 PM") + Text(locationViewModel.dropOffTime ?? "-") .font(.system(size: 14, weight: .bold)) .foregroundColor(.gray) } @@ -77,25 +84,32 @@ struct RideRequestView: View { ScrollView(.horizontal) { HStack(spacing: 12) { - ForEach(0 ..< 3, id: \.self) { _ in - VStack(alignment: .leading) { - Image(systemName: "bolt.car.fill") + ForEach(RideType.allCases, id: \.self) { rideType in + VStack { + Image(rideType.imageName) .resizable() .scaledToFit() .frame(width: 48, height: 48) - VStack(spacing: 4) { - Text("Smart X") + VStack(alignment: .leading, spacing: 4) { + Text(rideType.description) .font(.system(size: 14, weight: .semibold)) - Text("$ 5.80") + Text(locationViewModel.computeRidePrice(forType: rideType).toCurrency()) .font(.system(size: 14, weight: .semibold)) } - .padding(8) } + .padding(.top, 8) .frame(width: 112, height: 140) - .background(Color(.systemGroupedBackground)) + .foregroundColor(rideType == selectedRideType ? .black : Color.theme.primaryTextColor) + .background(rideType == selectedRideType ? .yellow : Color.theme.secondaryBackgroundColor) + .scaleEffect(rideType == selectedRideType ? 1.1 : 1.0) .cornerRadius(10) + .onTapGesture { + withAnimation(.linear) { + selectedRideType = rideType + } + } } } }.padding(.horizontal) @@ -138,7 +152,9 @@ struct RideRequestView: View { } } - .background(.white) + .padding(.bottom, 24) + .background(Color.theme.backgroundColor) + .cornerRadius(16) } } diff --git a/eUberUI/Core/Trips/View/RideType.swift b/eUberUI/Core/Trips/View/RideType.swift new file mode 100644 index 0000000..2466812 --- /dev/null +++ b/eUberUI/Core/Trips/View/RideType.swift @@ -0,0 +1,49 @@ +// +// RideType.swift +// eUberUI +// +// Created by Lucas Hubert on 22/05/23. +// + +import Foundation + +enum RideType: Int, CaseIterable, Identifiable { + case eUberX + case eUberPlus + case eUberBike + + var id: Int { return rawValue } + + var description: String { + switch self { + case .eUberX: return "eUber X" + case .eUberPlus: return "eUber Plus" + case .eUberBike: return "eUber Bike" + } + } + + var imageName: String { + switch self { + case .eUberX: return "hatchback" + case .eUberPlus: return "suv" + case .eUberBike: return "motorbike" + } + } + + var baseFare: Double { + switch self { + case .eUberBike: return 5 + case .eUberPlus: return 20 + case .eUberX: return 10 + } + } + + func computePrice(for distanceInMeters: Double) -> Double { + let distanceInMiles = distanceInMeters / 1600 + switch self { + case .eUberBike: return distanceInMiles * 1.5 + baseFare + case .eUberX: return distanceInMiles * 2.0 + baseFare + case .eUberPlus: return distanceInMiles * 2.3 + baseFare + } + } +} diff --git a/eUberUI/Managers/LocationManager.swift b/eUberUI/Managers/LocationManager.swift index a097f18..7ceace5 100644 --- a/eUberUI/Managers/LocationManager.swift +++ b/eUberUI/Managers/LocationManager.swift @@ -9,6 +9,8 @@ import CoreLocation class LocationManager: NSObject, ObservableObject { private let locationManager = CLLocationManager() + static let shared = LocationManager() + @Published var userLocation: CLLocationCoordinate2D? override init() { super.init() @@ -21,7 +23,8 @@ class LocationManager: NSObject, ObservableObject { extension LocationManager: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard !locations.isEmpty else { return } + guard let loc = locations.first else { return } + self.userLocation = loc.coordinate locationManager.stopUpdatingLocation() } }