Compare commits

...

10 Commits

14 changed files with 619 additions and 103 deletions

View File

@@ -6,8 +6,33 @@
objectVersion = 77;
objects = {
/* Begin PBXCopyFilesBuildPhase section */
6C6E6C442D00962A003D3BA9 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
6C6E6C5C2D00A2EA003D3BA9 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6C5D06452CF209960006CDE9 /* StepMap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StepMap.app; sourceTree = BUILT_PRODUCTS_DIR; };
6C6E6C352D009628003D3BA9 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
6C6E6C372D009628003D3BA9 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -33,6 +58,7 @@
isa = PBXGroup;
children = (
6C5D06472CF209960006CDE9 /* StepMap */,
6C6E6C342D009628003D3BA9 /* Frameworks */,
6C5D06462CF209960006CDE9 /* Products */,
);
sourceTree = "<group>";
@@ -45,6 +71,15 @@
name = Products;
sourceTree = "<group>";
};
6C6E6C342D009628003D3BA9 /* Frameworks */ = {
isa = PBXGroup;
children = (
6C6E6C352D009628003D3BA9 /* WidgetKit.framework */,
6C6E6C372D009628003D3BA9 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -55,6 +90,8 @@
6C5D06412CF209960006CDE9 /* Sources */,
6C5D06422CF209960006CDE9 /* Frameworks */,
6C5D06432CF209960006CDE9 /* Resources */,
6C6E6C442D00962A003D3BA9 /* Embed Foundation Extensions */,
6C6E6C5C2D00A2EA003D3BA9 /* Embed Frameworks */,
);
buildRules = (
);

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C5D06442CF209960006CDE9"
BuildableName = "StepMap.app"
BlueprintName = "StepMap"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C5D06442CF209960006CDE9"
BuildableName = "StepMap.app"
BlueprintName = "StepMap"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C5D06442CF209960006CDE9"
BuildableName = "StepMap.app"
BlueprintName = "StepMap"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C6E6C322D009628003D3BA9"
BuildableName = "StepMapWidgetsExtension.appex"
BlueprintName = "StepMapWidgetsExtension"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C5D06442CF209960006CDE9"
BuildableName = "StepMap.app"
BlueprintName = "StepMap"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.springboard">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C6E6C322D009628003D3BA9"
BuildableName = "StepMapWidgetsExtension.appex"
BlueprintName = "StepMapWidgetsExtension"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C5D06442CF209960006CDE9"
BuildableName = "StepMap.app"
BlueprintName = "StepMap"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C5D06442CF209960006CDE9"
BuildableName = "StepMap.app"
BlueprintName = "StepMap"
ReferencedContainer = "container:StepMap.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -5,10 +5,33 @@
<key>SchemeUserState</key>
<dict>
<key>StepMap.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>StepMapUtils.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>StepMapWidgetsExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>6C5D06442CF209960006CDE9</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>6C6E6C322D009628003D3BA9</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,56 @@
//
// AnnotationView.swift
// StepMap
//
// Created by Oliver Hnat on 16/05/2025.
//
import SwiftUI
import MapKit
struct AnnotationView: View {
var pm: CLPlacemark
var title: String?
var coordinate: CLLocation
@ObservedObject var viewModel: ViewModel
var body: some View {
ZStack {
Rectangle()
.fill(.thinMaterial)
.ignoresSafeArea()
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text((title ?? pm.areasOfInterest?.first ?? pm.name) ?? "\(coordinate.coordinate.latitude.description)º, \(coordinate.coordinate.longitude.description)")
.font(.title)
.bold()
Spacer()
Button(action: {
viewModel.showDetails = false
}, label: {
Image(systemName: "multiply.circle")
})
}
.padding(.horizontal)
.padding(.top, 20)
Spacer()
Text(pm.locality ?? "")
Text(pm.name ?? "")
Text("Name: \(pm.name ?? "")")
Text("Street: \(pm.thoroughfare ?? "")")
Text("City: \(pm.locality ?? "")")
Text("Postal Code: \(pm.postalCode ?? "")")
Text("Country: \(pm.country ?? "")")
ForEach(pm.areasOfInterest ?? [], id: \.self) { area in
Text("Area: \(area)")
}
if let coordinate = pm.location?.coordinate {
let mkPlacemark = MKPlacemark(coordinate: coordinate)
Image(systemName: Defaults.getIconFor(pointOfInterest: MKMapItem(placemark: mkPlacemark).pointOfInterestCategory))
}
}
.frame(maxWidth: .infinity)
.ignoresSafeArea()
}
}
}

View File

@@ -15,14 +15,6 @@ struct ContentView: View {
@StateObject var locationManager = LocationManager()
@StateObject var healthKitManager = HealthKitManager()
@State private var position = MapCameraPosition.automatic
@State private var showSearch: Bool = true
@State private var directions: [MKRoute] = []
@State private var destination: MKMapItem?
@State var healthKitAccess = false
@State var stepLength: Double?
// TODO: create a map
// Add navigation to the map
@@ -42,51 +34,15 @@ struct ContentView: View {
// calculate only the distance between the start and end instead of getting directions for everything
// if user clicks on the place, display better view and then calculate route there
var body: some View {
Map(position: $position) {
ForEach(0..<directions.count) { i in
if destination != nil {
Marker(item: destination!)
MapView(locationManager: locationManager, viewModel: viewModel)
.ignoresSafeArea()
.onAppear {
Task {
await healthKitManager.requestAccess()
viewModel.stepLength = await healthKitManager.getStepLength()
}
MapPolyline(directions[i].polyline)
.stroke(Defaults.routeColor[i], lineWidth: Defaults.routeWidth)
}
}
.sheet(
isPresented: $showSearch,
content: {
SearchView(
directions: $directions, stepLength: $stepLength,
locationManager: locationManager, destination: $destination
)
.ignoresSafeArea()
}
)
.mapControls {
// TODO: make sure the user location stays on the map even if camera moves
MapUserLocationButton()
.onTapGesture {
locationManager.requestAuthorization()
locationManager.requestLocation()
if let userLocation = locationManager.location {
position = .camera(
MapCamera(centerCoordinate: userLocation, distance: 1000))
}
}
MapCompass()
}
.onAppear {
locationManager.requestAuthorization()
locationManager.requestLocation()
if let userLocation = locationManager.location {
position = .camera(MapCamera(centerCoordinate: userLocation, distance: 1000))
}
Task {
await healthKitManager.requestAccess()
stepLength = await healthKitManager.getStepLength()
}
}
}
func getLastPointFor(route: MKRoute) -> CLLocationCoordinate2D? {
let pointCount = route.polyline.pointCount
if pointCount > 0 {

View File

@@ -13,10 +13,13 @@ class HealthKitManager: ObservableObject {
let allTypes: Set = [
HKQuantityType(.walkingSpeed),
HKQuantityType(.walkingStepLength),
HKQuantityType(.stepCount)
]
var stepLength: Double?
var stepCount: Int?
func requestAccess() async {
do {
if HKHealthStore.isHealthDataAvailable() {
@@ -50,4 +53,22 @@ class HealthKitManager: ObservableObject {
}
}
// func getCurrentStepCount() async -> Double? {
// let stepCountType = HKQuantityType(.stepCount)
//
// let query = HKStatisticsQueryDescriptor(
// predicate: HKSamplePredicate.quantitySample(type: stepCountType),
// options: .cumulativeSum)
//
// do {
// let results = try await query.result(for: healthStore)
// stepCount = results?.sumQuantity().val
// return stepCount
// } catch {
// fatalError(
// "Something went wrong while getting step length from healthKit: \(error.localizedDescription)"
// )
// }
// }
}

View File

@@ -8,7 +8,7 @@
import CoreLocation
import Foundation
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
public class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
@Published var location: CLLocationCoordinate2D?
@@ -26,13 +26,13 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
manager.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location = locations.first?.coordinate
}
}
extension LocationManager {
func locationManager(
public func locationManager(
_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus
) {
switch status {
@@ -51,7 +51,16 @@ extension LocationManager {
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
public func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
print(error)
}
}
extension CLLocationCoordinate2D: Equatable {
public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
}

View File

@@ -13,20 +13,20 @@ import SwiftUI
struct SearchItemView: View {
var location: MKMapItem
@State var distance: CLLocationDistance?
@Binding var directions: [MKRoute]
@Binding var stepLength: Double?
@Binding var showSteps: Bool
@State var localDirections: [MKRoute] = []
@Binding var destination: MKMapItem?
@ObservedObject var viewModel: ViewModel
var body: some View {
Button(
action: {
if localDirections == [] {
print("finding directions")
findDirections()
}
directions = localDirections
viewModel.directions = localDirections
print("Directions set")
},
label: {
HStack {
@@ -86,7 +86,7 @@ struct SearchItemView: View {
}
func formatDistance(distance: CLLocationDistance) -> String {
let steps = distance / (stepLength ?? 1)
let steps = distance / (viewModel.stepLength ?? 1)
if steps != 0 && showSteps {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 0
@@ -118,7 +118,7 @@ struct SearchItemView: View {
}
self.localDirections = response.routes
self.distance = response.routes.first?.distance
self.destination = location
viewModel.destination = location
}
}
}

View File

@@ -11,56 +11,56 @@ import SwiftUI
struct SearchView: View {
@State private var query: String = ""
@State private var locations: [MKMapItem] = []
@Binding var directions: [MKRoute]
@Binding var stepLength: Double?
@State var showSteps = true
var locationManager: LocationManager
@Binding var destination: MKMapItem?
@ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
HStack {
Image(systemName: "magnifyingglass")
TextField("Search for any location", text: $query)
.autocorrectionDisabled()
.onChange(of: self.query) {
if query.count > 0 {
search(for: self.query)
} else {
self.locations = []
ZStack {
Rectangle()
.fill(.thinMaterial)
.ignoresSafeArea()
VStack {
HStack {
Image(systemName: "magnifyingglass")
TextField("Search for any location", text: $query)
.autocorrectionDisabled()
.onChange(of: self.query) {
if query.count > 0 {
search(for: self.query)
} else {
self.locations = []
}
}
}
// .onAppear {
// // TODO: delete this, it's for debug only
// search(for: self.query)
// }
.overlay {
HStack {
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundStyle(.gray)
.onTapGesture {
query = ""
}
.overlay {
HStack {
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundStyle(.gray)
.onTapGesture {
query = ""
viewModel.destination = nil
viewModel.directions = []
}
}
}
}
.modifier(TextFieldGrayBackgroudColor())
Spacer()
ScrollView {
ForEach(self.locations, id: \.identifier) { location in
SearchItemView(location: location, showSteps: $showSteps, viewModel: viewModel)
}
}
.modifier(TextFieldGrayBackgroudColor())
Spacer()
ScrollView {
ForEach(self.locations, id: \.identifier) { location in
SearchItemView(
location: location, directions: $directions, stepLength: $stepLength,
showSteps: $showSteps, destination: $destination)
}
}
.padding()
.interactiveDismissDisabled()
.ignoresSafeArea()
}
.padding()
.interactiveDismissDisabled()
.presentationDetents([.height(200), .large])
.presentationBackground(.regularMaterial)
.presentationBackgroundInteraction(.enabled(upThrough: .large))
}
func search(for text: String) {

206
StepMap/UIKitMapView.swift Normal file
View File

@@ -0,0 +1,206 @@
//
// UIKitMapView.swift
// StepMap
//
// Created by Oliver Hnat on 13/05/2025.
//
import UIKit
import MapKit
import SwiftUI
import Combine
class UIKitMapView: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
var locationManager: LocationManager
var viewModel: ViewModel
var oldDirections: [MKRoute] = []
var oldDestination: MKMapItemAnnotation?
private var cancellables = Set<AnyCancellable>()
let searchViewConctroller: UIHostingController<SearchView>
var annotationViewController: UIHostingController<AnnotationView>?
let mapView : MKMapView = {
let map = MKMapView()
map.showsUserTrackingButton = true
map.showsUserLocation = true
map.selectableMapFeatures = .pointsOfInterest
map.pitchButtonVisibility = .adaptive
return map
}()
init(locationManager: LocationManager, viewModel: ViewModel) {
self.locationManager = locationManager
self.viewModel = viewModel
self.searchViewConctroller = UIHostingController(rootView: SearchView(locationManager: locationManager, viewModel: viewModel))
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
mapView.delegate = self
setMapConstraints()
setLocation()
}
override func viewDidAppear(_ animated: Bool) {
showSearchView()
}
private func setLocation() {
locationManager.requestAuthorization()
locationManager.requestLocation()
if let userLocation = locationManager.location {
let viewRegion = MKCoordinateRegion(center: userLocation, latitudinalMeters: 2000, longitudinalMeters: 2000)
mapView.setRegion(viewRegion, animated: true)
}
mapView.setUserTrackingMode(.follow, animated: true)
}
private func setMapConstraints() {
view.addSubview(mapView)
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
mapView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
private func showSearchView() {
searchViewConctroller.view.backgroundColor = .clear
searchViewConctroller.modalPresentationStyle = .pageSheet
searchViewConctroller.edgesForExtendedLayout = [.top, .bottom, .left, .right]
if let sheet = searchViewConctroller.sheetPresentationController {
let smallDetentId = UISheetPresentationController.Detent.Identifier("small")
let smallDetent = UISheetPresentationController.Detent.custom(identifier: smallDetentId) { context in
return 200
}
sheet.detents = [smallDetent, .large()]
sheet.largestUndimmedDetentIdentifier = .large
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.prefersGrabberVisible = true
sheet.prefersEdgeAttachedInCompactHeight = true
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
}
self.present(searchViewConctroller, animated: true, completion: nil)
}
private func refreshRoute() {
DispatchQueue.main.async {
for route in self.oldDirections {
self.mapView.removeOverlay(route.polyline)
}
self.oldDirections = self.viewModel.directions
for route in self.viewModel.directions {
self.mapView.addOverlay(route.polyline, level: .aboveRoads)
}
if let destination = self.oldDestination {
self.mapView.removeAnnotation(destination)
}
if let destination = self.viewModel.destination {
self.oldDestination = MKMapItemAnnotation(mapItem: destination)
self.mapView.addAnnotation(self.oldDestination!)
}
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: any MKOverlay) -> MKOverlayRenderer {
let renderer = MKGradientPolylineRenderer(overlay: overlay)
renderer.setColors(Defaults.routeColor, locations: [])
renderer.lineCap = .round
renderer.lineWidth = Defaults.routeWidth
return renderer
}
func mapView(_ mapView: MKMapView, didSelect annotation: any MKAnnotation) {
hideSearchView()
hideAnnotationView()
showAnnotation(annotation: annotation)
}
func showAnnotation(annotation: MKAnnotation) {
viewModel.showDetails = true
let location = CLLocation(latitude: annotation.coordinate.latitude,
longitude: annotation.coordinate.longitude)
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
guard let placemark = placemarks?.first, error == nil else { return }
self.annotationViewController = UIHostingController(rootView: AnnotationView(pm: placemark, title: annotation.title as? String, coordinate: location, viewModel: self.viewModel))
if let avc = self.annotationViewController {
avc.view.backgroundColor = .clear
avc.modalPresentationStyle = .pageSheet
avc.edgesForExtendedLayout = [.top, .bottom, .left, .right]
if let sheet = avc.sheetPresentationController {
let smallDetentId = UISheetPresentationController.Detent.Identifier("small")
let smallDetent = UISheetPresentationController.Detent.custom(identifier: smallDetentId) { context in
return 350
}
sheet.detents = [smallDetent, .large()]
sheet.largestUndimmedDetentIdentifier = .large
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.prefersGrabberVisible = true
sheet.prefersEdgeAttachedInCompactHeight = true
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
}
self.present(avc, animated: true, completion: nil)
}
}
}
func mapView(_ mapView: MKMapView, didDeselect annotation: any MKAnnotation) {
if viewModel.showDetails {
viewModel.showDetails = false
}
}
func hideSearchView() {
searchViewConctroller.dismiss(animated: true)
}
func hideAnnotationView() {
if let avc = self.annotationViewController {
avc.dismiss(animated: true)
}
}
private func bindViewModel() {
viewModel.$directions
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.refreshRoute()
}
.store(in: &cancellables)
viewModel.$showDetails
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
print(value)
if !value {
self?.hideAnnotationView()
self?.showSearchView()
// self?.mapView.selectedAnnotations = []
}
}
.store(in: &cancellables)
}
}
struct MapView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIKitMapView
@StateObject var locationManager: LocationManager
@ObservedObject var viewModel: ViewModel
func makeUIViewController(context: Context) -> UIKitMapView {
return UIKitMapView(locationManager: locationManager, viewModel: viewModel)
}
func updateUIViewController(_ uiViewController: UIKitMapView, context: Context) {
// pass
}
}

View File

@@ -10,10 +10,10 @@ import MapKit
import SwiftUI
struct Defaults {
static let routeColor: [Color] = [
static let routeColor: [UIColor] = [
.blue,
.red,
.yellow,
// .red,
// .yellow,
]
static let routeWidth: CGFloat = 8

View File

@@ -6,9 +6,15 @@
//
import Foundation
import MapKit
class ViewModel: ObservableObject {
@Published var test: String = UserDefaults.standard.string(forKey: "test") ?? ""
@Published var directions: [MKRoute] = []
@Published var stepLength: Double?
@Published var destination: MKMapItem?
@Published var showDetails = false
func saveValue(_ value: String) {
UserDefaults.standard.set(value, forKey: "test")