mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Compass view (#1521)
* Added compass view * Added Compass View * Node colors in compass * Update Muzi R1 Neo to actively supported * Update PositionPopover.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
865e5e950b
commit
8346fb8073
7 changed files with 398 additions and 6 deletions
|
|
@ -5135,6 +5135,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bearing: %@" : {
|
||||
|
||||
},
|
||||
"Bearing: N/A" : {
|
||||
|
||||
},
|
||||
"Biking" : {
|
||||
"localizations" : {
|
||||
|
|
@ -8085,6 +8091,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Compass" : {
|
||||
|
||||
},
|
||||
"Config" : {
|
||||
"localizations" : {
|
||||
|
|
@ -11995,6 +12004,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Distance: %@" : {
|
||||
|
||||
},
|
||||
"Documentation" : {
|
||||
"localizations" : {
|
||||
|
|
@ -24767,6 +24779,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Open Compass" : {
|
||||
|
||||
},
|
||||
"Open Settings" : {
|
||||
"localizations" : {
|
||||
|
|
@ -42306,4 +42321,4 @@
|
|||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@
|
|||
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; };
|
||||
BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */; };
|
||||
BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; };
|
||||
BCA9A82C2EC802CF00166292 /* CompassView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCA9A82B2EC802CF00166292 /* CompassView.swift */; };
|
||||
BCB35B4F2E5FC42500B04F60 /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB35B4E2E5FC41E00B04F60 /* MessageNodeIntent.swift */; };
|
||||
BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; };
|
||||
BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; };
|
||||
|
|
@ -409,6 +410,7 @@
|
|||
B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = "<group>"; };
|
||||
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = "<group>"; };
|
||||
BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = "<group>"; };
|
||||
BCA9A82B2EC802CF00166292 /* CompassView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassView.swift; sourceTree = "<group>"; };
|
||||
BCB35B4E2E5FC41E00B04F60 /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -1274,6 +1276,7 @@
|
|||
8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */,
|
||||
237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */,
|
||||
23A1AFB62E42BD2500E46C96 /* RXTXIndicatorView.swift */,
|
||||
BCA9A82B2EC802CF00166292 /* CompassView.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1814,6 +1817,7 @@
|
|||
233E99BE2D849D3200CC3A77 /* RadiationCompactWidget.swift in Sources */,
|
||||
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,
|
||||
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */,
|
||||
BCA9A82C2EC802CF00166292 /* CompassView.swift in Sources */,
|
||||
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */,
|
||||
23D316932E5618D2002FA4FB /* AsyncGate.swift in Sources */,
|
||||
23FF00B62E323C75001DF095 /* AccessoryManager+Connect.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "fd71b247ba909b0eb360db5530e1068363839c5e169dea6f6a9974b2d98276f4",
|
||||
"originHash" : "0fb5b226f8ca0b357ce7816ebac09d017cbe0ad253452876e5e2ff8e555c7124",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "cocoamqtt",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import OSLog
|
|||
@Published var recordingStarted: Date?
|
||||
@Published var distanceTraveled = 0.0
|
||||
@Published var elevationGain = 0.0
|
||||
@Published var heading: Double = 0.0 // Current heading in degrees
|
||||
@Published var headingUpdatesStarted: Bool = false // Track heading updates state
|
||||
|
||||
@Published
|
||||
var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
|
||||
|
|
@ -131,6 +133,10 @@ import OSLog
|
|||
self.manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
// Set the distance filter to only receive updates when the device has moved a certain distance.
|
||||
self.manager.distanceFilter = kCLDistanceFilterNone // Receive all updates initially
|
||||
if CLLocationManager.headingAvailable() {
|
||||
self.manager.headingFilter = 1 // Update heading when it changes by 1 degree
|
||||
self.manager.headingOrientation = .portrait // Adjust based on device orientation
|
||||
}
|
||||
}
|
||||
|
||||
func startLocationUpdates() {
|
||||
|
|
@ -178,6 +184,39 @@ import OSLog
|
|||
// The Task completes implicitly here.
|
||||
}
|
||||
}
|
||||
|
||||
// New method to start heading updates
|
||||
func startHeadingUpdates() {
|
||||
guard CLLocationManager.headingAvailable() else {
|
||||
Logger.services.warning("📍 [App] Heading updates not available on this device.")
|
||||
return
|
||||
}
|
||||
|
||||
guard manager.authorizationStatus == .authorizedAlways || manager.authorizationStatus == .authorizedWhenInUse else {
|
||||
Logger.services.warning("📍 [App] Cannot start heading updates: insufficient authorization status.")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.services.info("📍 [App] Starting heading updates")
|
||||
manager.startUpdatingHeading()
|
||||
headingUpdatesStarted = true
|
||||
}
|
||||
|
||||
// New method to stop heading updates
|
||||
func stopHeadingUpdates() {
|
||||
Logger.services.info("🛑 [App] Stopping heading updates")
|
||||
manager.stopUpdatingHeading()
|
||||
headingUpdatesStarted = false
|
||||
}
|
||||
|
||||
// Implement the CLLocationManagerDelegate method for heading updates
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
|
||||
// Update heading on the main thread
|
||||
Task { @MainActor in
|
||||
self.heading = newHeading.trueHeading >= 0 ? newHeading.trueHeading : newHeading.magneticHeading
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops receiving live location updates.
|
||||
func stopLocationUpdates() {
|
||||
Logger.services.info("🛑 [App] Stopping location updates")
|
||||
|
|
|
|||
295
Meshtastic/Views/Helpers/CompassView.swift
Normal file
295
Meshtastic/Views/Helpers/CompassView.swift
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
//
|
||||
// CompassView.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 11/14/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
import UIKit
|
||||
|
||||
struct CompassView: View {
|
||||
|
||||
/// Single waypoint parameter
|
||||
let waypointLocation: CLLocationCoordinate2D?
|
||||
|
||||
let waypointName: String?
|
||||
|
||||
let color: Color
|
||||
|
||||
@ObservedObject private var locationsHandler = LocationsHandler.shared
|
||||
|
||||
// Haptic alignment tracking
|
||||
private let alignmentTolerance: Double = 5.0
|
||||
@State private var inAlignment = false
|
||||
|
||||
// Compute bearing from user → waypoint
|
||||
private func bearingToWaypoint() -> Double? {
|
||||
guard
|
||||
let waypoint = waypointLocation,
|
||||
let user = LocationsHandler.currentLocation
|
||||
else { return nil }
|
||||
|
||||
return BearingCalculator.bearingBetween(
|
||||
userLocation: user,
|
||||
waypoint: waypoint
|
||||
)
|
||||
}
|
||||
|
||||
// Trigger a vibration if aligned with waypoint
|
||||
private func checkAlignment(bearing: Double,heading: Double) {
|
||||
// Compute minimal angular difference between heading and bearing in [0, 180]
|
||||
let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360)
|
||||
let diff = min(rawDiff, 360 - rawDiff)
|
||||
|
||||
if diff <= alignmentTolerance {
|
||||
if !inAlignment {
|
||||
inAlignment = true
|
||||
let generator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
} else {
|
||||
inAlignment = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func distanceToWaypoint() -> CLLocationDistance? {
|
||||
guard
|
||||
let waypoint = waypointLocation,
|
||||
let user = LocationsHandler.currentLocation
|
||||
else { return nil }
|
||||
|
||||
let userLocation = CLLocation(latitude: user.latitude, longitude: user.longitude)
|
||||
let waypointLocation = CLLocation(latitude: waypoint.latitude, longitude: waypoint.longitude)
|
||||
|
||||
return userLocation.distance(from: waypointLocation)
|
||||
}
|
||||
|
||||
// Format distance with localization
|
||||
private func formatDistance(_ distance: CLLocationDistance) -> String {
|
||||
let measurement = Measurement(value: distance, unit: UnitLength.meters)
|
||||
let formatter = MeasurementFormatter()
|
||||
formatter.unitOptions = .naturalScale
|
||||
formatter.numberFormatter.maximumFractionDigits = 2
|
||||
return formatter.string(from: measurement)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 15) {
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(waypointName ?? "Waypoint")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
.foregroundColor(color)
|
||||
|
||||
if let wp = waypointLocation {
|
||||
HStack{
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
Text("\(String(format: "%.4f", wp.latitude)), \(String(format: "%.4f", wp.longitude))")
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
if let distance = distanceToWaypoint() {
|
||||
HStack{
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
Text("Distance: \(formatDistance(distance))")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "location.north")
|
||||
if let bearing = bearingToWaypoint() {
|
||||
Text("Bearing: \(String(format: "%.0f°", bearing))")
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
Text("Bearing: N/A")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
Capsule()
|
||||
.frame(width: 5, height: 50)
|
||||
ZStack {
|
||||
|
||||
// Cardinal/degree markers
|
||||
ForEach(Marker.markers(), id: \.self) { marker in
|
||||
CompassMarkerView(
|
||||
marker: marker,
|
||||
compassDegrees: -locationsHandler.heading
|
||||
)
|
||||
}
|
||||
|
||||
// Waypoint bearing indicator
|
||||
if let bearing = bearingToWaypoint() {
|
||||
WaypointMarkerView(
|
||||
bearing: bearing,
|
||||
compassDegrees: locationsHandler.heading,
|
||||
color: color
|
||||
)
|
||||
// Move waypoint marker outside compass
|
||||
.onChange(of: locationsHandler.heading) { _, _ in
|
||||
checkAlignment(bearing: bearing,heading:locationsHandler.heading)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.frame(width: 300, height: 300)
|
||||
.rotationEffect(Angle(degrees: -locationsHandler.heading))
|
||||
.statusBar(hidden: true)
|
||||
.onAppear {
|
||||
locationsHandler.startHeadingUpdates()
|
||||
locationsHandler.startLocationUpdates()
|
||||
}
|
||||
.onDisappear {
|
||||
locationsHandler.stopHeadingUpdates()
|
||||
locationsHandler.stopLocationUpdates()
|
||||
}
|
||||
.navigationTitle("Compass")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Waypoint Marker View
|
||||
|
||||
struct WaypointMarkerView: View {
|
||||
let bearing: Double
|
||||
let compassDegrees: Double
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(color)
|
||||
.offset(y: -170)
|
||||
.rotationEffect(Angle(degrees: bearing))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Bearing Calculator
|
||||
|
||||
struct BearingCalculator {
|
||||
|
||||
static func bearingBetween(
|
||||
userLocation: CLLocationCoordinate2D,
|
||||
waypoint: CLLocationCoordinate2D
|
||||
) -> Double {
|
||||
|
||||
let lat1 = userLocation.latitude * .pi / 180
|
||||
let lon1 = userLocation.longitude * .pi / 180
|
||||
let lat2 = waypoint.latitude * .pi / 180
|
||||
let lon2 = waypoint.longitude * .pi / 180
|
||||
|
||||
let dLon = lon2 - lon1
|
||||
|
||||
let y = sin(dLon) * cos(lat2)
|
||||
let x = cos(lat1) * sin(lat2)
|
||||
- sin(lat1) * cos(lat2) * cos(dLon)
|
||||
|
||||
var bearing = atan2(y, x) * 180 / .pi
|
||||
if bearing < 0 { bearing += 360 }
|
||||
|
||||
return bearing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Marker Model
|
||||
|
||||
struct Marker: Hashable {
|
||||
let degrees: Double
|
||||
let label: String
|
||||
|
||||
init(degrees: Double, label: String = "") {
|
||||
self.degrees = degrees
|
||||
self.label = label
|
||||
}
|
||||
|
||||
func degreeText() -> String {
|
||||
return String(format: "%.0f", self.degrees)
|
||||
}
|
||||
|
||||
static func markers() -> [Marker] {
|
||||
return [
|
||||
Marker(degrees: 0, label: "N"),
|
||||
Marker(degrees: 30),
|
||||
Marker(degrees: 60),
|
||||
Marker(degrees: 90, label: "E"),
|
||||
Marker(degrees: 120),
|
||||
Marker(degrees: 150),
|
||||
Marker(degrees: 180, label: "S"),
|
||||
Marker(degrees: 210),
|
||||
Marker(degrees: 240),
|
||||
Marker(degrees: 270, label: "W"),
|
||||
Marker(degrees: 300),
|
||||
Marker(degrees: 330)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Compass Marker View
|
||||
|
||||
struct CompassMarkerView: View {
|
||||
let marker: Marker
|
||||
let compassDegrees: Double
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(marker.degreeText())
|
||||
.fontWeight(.light)
|
||||
.rotationEffect(textAngle())
|
||||
|
||||
Capsule()
|
||||
.frame(width: capsuleWidth(), height: capsuleHeight())
|
||||
.foregroundColor(capsuleColor())
|
||||
|
||||
Text(marker.label)
|
||||
.fontWeight(.bold)
|
||||
.rotationEffect(textAngle())
|
||||
.padding(.bottom, 180)
|
||||
}
|
||||
.rotationEffect(Angle(degrees: marker.degrees))
|
||||
}
|
||||
|
||||
private func capsuleWidth() -> CGFloat {
|
||||
marker.degrees == 0 ? 7 : 3
|
||||
}
|
||||
|
||||
private func capsuleHeight() -> CGFloat {
|
||||
marker.degrees == 0 ? 45 : 30
|
||||
}
|
||||
|
||||
private func capsuleColor() -> Color {
|
||||
marker.degrees == 0 ? .red : .gray
|
||||
}
|
||||
|
||||
private func textAngle() -> Angle {
|
||||
Angle(degrees: -compassDegrees - marker.degrees)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
struct CompassView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CompassView(
|
||||
waypointLocation: CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090),
|
||||
waypointName: "Apple Park",
|
||||
color: Color.orange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,9 @@ struct PositionPopover: View {
|
|||
var position: PositionEntity
|
||||
var popover: Bool = true
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
|
||||
@State private var detentSelection: PresentationDetent = .fraction(0.65)
|
||||
@State private var navigateToCompass = false
|
||||
|
||||
var body: some View {
|
||||
// Node Color from node.num
|
||||
|
|
@ -42,6 +45,19 @@ struct PositionPopover: View {
|
|||
Divider()
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
detentSelection = .large
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
navigateToCompass = true
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "safari")
|
||||
Text("Open Compass")
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
|
||||
/// Time
|
||||
Label {
|
||||
if idiom != .phone {
|
||||
|
|
@ -131,6 +147,7 @@ struct PositionPopover: View {
|
|||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
|
||||
/// Heading
|
||||
let degrees = Angle.degrees(Double(position.heading))
|
||||
Label {
|
||||
|
|
@ -234,10 +251,17 @@ struct PositionPopover: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.65), .large], selection: $detentSelection)
|
||||
.presentationContentInteraction(.scrolls)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .large))
|
||||
.navigationDestination(isPresented: $navigateToCompass) {
|
||||
CompassView(
|
||||
waypointLocation: position.coordinate,
|
||||
waypointName: position.nodePosition?.user?.longName ?? "Unknown node",
|
||||
color: (position.nodePosition?.user?.num != nil && position.nodePosition?.user?.num != 0) ? Color(UIColor(hex: UInt32(position.nodePosition!.user!.num))) : .orange
|
||||
)
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.65), .large])
|
||||
.presentationContentInteraction(.scrolls)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .large))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ struct NodeDetail: View {
|
|||
var connectedNode: NodeInfoEntity?
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@State private var environmentSectionHeight: CGFloat = 0
|
||||
@State var showingCompassSheet = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
|
@ -473,6 +474,17 @@ struct NodeDetail: View {
|
|||
)
|
||||
}
|
||||
if node.hasPositions {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
Button {
|
||||
showingCompassSheet = true
|
||||
} label: {
|
||||
Label {
|
||||
Text("Open Compass")
|
||||
} icon: {
|
||||
Image(systemName: "safari")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
NavigateToButton(node: node)
|
||||
}
|
||||
IgnoreNodeButton(
|
||||
|
|
@ -559,6 +571,9 @@ struct NodeDetail: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingCompassSheet) {
|
||||
CompassView(waypointLocation: node.latestPosition?.nodeCoordinate ?? nil, waypointName: node.user?.longName ?? nil, color: Color(UIColor(hex: UInt32(node.num))))
|
||||
}
|
||||
.onAppear {
|
||||
scrollView.scrollTo("topOfList", anchor: .top)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue