Compare commits
	
		
			10 Commits
		
	
	
		
			c759d5e3b3
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f2aa710a5e | ||
|   | a64ef8d469 | ||
|   | bf7175bdd6 | ||
|   | 171e181387 | ||
|   | 8ec74c1012 | ||
|   | 546bdc4b2b | ||
|   | b72ca2dd3d | ||
| 17cc4190b8 | |||
| c97a50cd4b | |||
| e0d7c72117 | 
| @@ -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 = ( | ||||
| 			); | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								StepMap.xcodeproj/project.xcworkspace/xcuserdata/oliverhnat.xcuserdatad/UserInterfaceState.xcuserstate
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								StepMap.xcodeproj/project.xcworkspace/xcuserdata/oliverhnat.xcuserdatad/UserInterfaceState.xcuserstate
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										78
									
								
								StepMap.xcodeproj/xcshareddata/xcschemes/StepMap.xcscheme
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								StepMap.xcodeproj/xcshareddata/xcschemes/StepMap.xcscheme
									
									
									
									
									
										Normal 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> | ||||
| @@ -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> | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										56
									
								
								StepMap/AnnotationView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								StepMap/AnnotationView.swift
									
									
									
									
									
										Normal 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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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)" | ||||
| //            ) | ||||
| //        } | ||||
| //    } | ||||
| 
 | ||||
| } | ||||
| @@ -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 | ||||
|     } | ||||
|      | ||||
|      | ||||
| } | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										206
									
								
								StepMap/UIKitMapView.swift
									
									
									
									
									
										Normal 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 | ||||
|     } | ||||
|      | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user