Compare commits
10 Commits
c759d5e3b3
...
f2aa710a5e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2aa710a5e | ||
|
|
a64ef8d469 | ||
|
|
bf7175bdd6 | ||
|
|
171e181387 | ||
|
|
8ec74c1012 | ||
|
|
546bdc4b2b | ||
|
|
b72ca2dd3d | ||
| 17cc4190b8 | |||
| c97a50cd4b | |||
| e0d7c72117 |
@@ -6,8 +6,33 @@
|
|||||||
objectVersion = 77;
|
objectVersion = 77;
|
||||||
objects = {
|
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 */
|
/* Begin PBXFileReference section */
|
||||||
6C5D06452CF209960006CDE9 /* StepMap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StepMap.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
@@ -33,6 +58,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
6C5D06472CF209960006CDE9 /* StepMap */,
|
6C5D06472CF209960006CDE9 /* StepMap */,
|
||||||
|
6C6E6C342D009628003D3BA9 /* Frameworks */,
|
||||||
6C5D06462CF209960006CDE9 /* Products */,
|
6C5D06462CF209960006CDE9 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -45,6 +71,15 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
6C6E6C342D009628003D3BA9 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
6C6E6C352D009628003D3BA9 /* WidgetKit.framework */,
|
||||||
|
6C6E6C372D009628003D3BA9 /* SwiftUI.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -55,6 +90,8 @@
|
|||||||
6C5D06412CF209960006CDE9 /* Sources */,
|
6C5D06412CF209960006CDE9 /* Sources */,
|
||||||
6C5D06422CF209960006CDE9 /* Frameworks */,
|
6C5D06422CF209960006CDE9 /* Frameworks */,
|
||||||
6C5D06432CF209960006CDE9 /* Resources */,
|
6C5D06432CF209960006CDE9 /* Resources */,
|
||||||
|
6C6E6C442D00962A003D3BA9 /* Embed Foundation Extensions */,
|
||||||
|
6C6E6C5C2D00A2EA003D3BA9 /* Embed Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
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>
|
<key>SchemeUserState</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>StepMap.xcscheme_^#shared#^_</key>
|
<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>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</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>
|
</dict>
|
||||||
</plist>
|
</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 locationManager = LocationManager()
|
||||||
@StateObject var healthKitManager = HealthKitManager()
|
@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
|
// TODO: create a map
|
||||||
// Add navigation to the 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
|
// 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
|
// if user clicks on the place, display better view and then calculate route there
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Map(position: $position) {
|
MapView(locationManager: locationManager, viewModel: viewModel)
|
||||||
ForEach(0..<directions.count) { i in
|
.ignoresSafeArea()
|
||||||
if destination != nil {
|
.onAppear {
|
||||||
Marker(item: destination!)
|
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? {
|
func getLastPointFor(route: MKRoute) -> CLLocationCoordinate2D? {
|
||||||
let pointCount = route.polyline.pointCount
|
let pointCount = route.polyline.pointCount
|
||||||
if pointCount > 0 {
|
if pointCount > 0 {
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ class HealthKitManager: ObservableObject {
|
|||||||
let allTypes: Set = [
|
let allTypes: Set = [
|
||||||
HKQuantityType(.walkingSpeed),
|
HKQuantityType(.walkingSpeed),
|
||||||
HKQuantityType(.walkingStepLength),
|
HKQuantityType(.walkingStepLength),
|
||||||
|
HKQuantityType(.stepCount)
|
||||||
]
|
]
|
||||||
|
|
||||||
var stepLength: Double?
|
var stepLength: Double?
|
||||||
|
|
||||||
|
var stepCount: Int?
|
||||||
|
|
||||||
func requestAccess() async {
|
func requestAccess() async {
|
||||||
do {
|
do {
|
||||||
if HKHealthStore.isHealthDataAvailable() {
|
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 CoreLocation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
|
public class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
|
||||||
let manager = CLLocationManager()
|
let manager = CLLocationManager()
|
||||||
|
|
||||||
@Published var location: CLLocationCoordinate2D?
|
@Published var location: CLLocationCoordinate2D?
|
||||||
@@ -26,13 +26,13 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|||||||
manager.requestWhenInUseAuthorization()
|
manager.requestWhenInUseAuthorization()
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
location = locations.first?.coordinate
|
location = locations.first?.coordinate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocationManager {
|
extension LocationManager {
|
||||||
func locationManager(
|
public func locationManager(
|
||||||
_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus
|
_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus
|
||||||
) {
|
) {
|
||||||
switch status {
|
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)
|
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 {
|
struct SearchItemView: View {
|
||||||
var location: MKMapItem
|
var location: MKMapItem
|
||||||
@State var distance: CLLocationDistance?
|
@State var distance: CLLocationDistance?
|
||||||
@Binding var directions: [MKRoute]
|
|
||||||
@Binding var stepLength: Double?
|
|
||||||
@Binding var showSteps: Bool
|
@Binding var showSteps: Bool
|
||||||
@State var localDirections: [MKRoute] = []
|
@State var localDirections: [MKRoute] = []
|
||||||
@Binding var destination: MKMapItem?
|
@ObservedObject var viewModel: ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
action: {
|
action: {
|
||||||
if localDirections == [] {
|
if localDirections == [] {
|
||||||
|
print("finding directions")
|
||||||
findDirections()
|
findDirections()
|
||||||
}
|
}
|
||||||
directions = localDirections
|
viewModel.directions = localDirections
|
||||||
|
print("Directions set")
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -86,7 +86,7 @@ struct SearchItemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func formatDistance(distance: CLLocationDistance) -> String {
|
func formatDistance(distance: CLLocationDistance) -> String {
|
||||||
let steps = distance / (stepLength ?? 1)
|
let steps = distance / (viewModel.stepLength ?? 1)
|
||||||
if steps != 0 && showSteps {
|
if steps != 0 && showSteps {
|
||||||
let formatter = NumberFormatter()
|
let formatter = NumberFormatter()
|
||||||
formatter.maximumFractionDigits = 0
|
formatter.maximumFractionDigits = 0
|
||||||
@@ -118,7 +118,7 @@ struct SearchItemView: View {
|
|||||||
}
|
}
|
||||||
self.localDirections = response.routes
|
self.localDirections = response.routes
|
||||||
self.distance = response.routes.first?.distance
|
self.distance = response.routes.first?.distance
|
||||||
self.destination = location
|
viewModel.destination = location
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,56 +11,56 @@ import SwiftUI
|
|||||||
struct SearchView: View {
|
struct SearchView: View {
|
||||||
@State private var query: String = ""
|
@State private var query: String = ""
|
||||||
@State private var locations: [MKMapItem] = []
|
@State private var locations: [MKMapItem] = []
|
||||||
@Binding var directions: [MKRoute]
|
|
||||||
@Binding var stepLength: Double?
|
|
||||||
@State var showSteps = true
|
@State var showSteps = true
|
||||||
var locationManager: LocationManager
|
var locationManager: LocationManager
|
||||||
@Binding var destination: MKMapItem?
|
@ObservedObject var viewModel: ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
ZStack {
|
||||||
HStack {
|
Rectangle()
|
||||||
Image(systemName: "magnifyingglass")
|
.fill(.thinMaterial)
|
||||||
TextField("Search for any location", text: $query)
|
.ignoresSafeArea()
|
||||||
.autocorrectionDisabled()
|
VStack {
|
||||||
.onChange(of: self.query) {
|
HStack {
|
||||||
if query.count > 0 {
|
Image(systemName: "magnifyingglass")
|
||||||
search(for: self.query)
|
TextField("Search for any location", text: $query)
|
||||||
} else {
|
.autocorrectionDisabled()
|
||||||
self.locations = []
|
.onChange(of: self.query) {
|
||||||
|
if query.count > 0 {
|
||||||
|
search(for: self.query)
|
||||||
|
} else {
|
||||||
|
self.locations = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// .onAppear {
|
// .onAppear {
|
||||||
// // TODO: delete this, it's for debug only
|
// // TODO: delete this, it's for debug only
|
||||||
// search(for: self.query)
|
// search(for: self.query)
|
||||||
// }
|
// }
|
||||||
.overlay {
|
.overlay {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(systemName: "multiply.circle.fill")
|
Image(systemName: "multiply.circle.fill")
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
query = ""
|
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) {
|
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
|
import SwiftUI
|
||||||
|
|
||||||
struct Defaults {
|
struct Defaults {
|
||||||
static let routeColor: [Color] = [
|
static let routeColor: [UIColor] = [
|
||||||
.blue,
|
.blue,
|
||||||
.red,
|
// .red,
|
||||||
.yellow,
|
// .yellow,
|
||||||
]
|
]
|
||||||
static let routeWidth: CGFloat = 8
|
static let routeWidth: CGFloat = 8
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import MapKit
|
||||||
|
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
@Published var test: String = UserDefaults.standard.string(forKey: "test") ?? ""
|
@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) {
|
func saveValue(_ value: String) {
|
||||||
UserDefaults.standard.set(value, forKey: "test")
|
UserDefaults.standard.set(value, forKey: "test")
|
||||||
|
|||||||
Reference in New Issue
Block a user