Fix emojionlytextfield regression by @rcgv1

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
Garth Vander Houwen 2026-04-05 08:43:44 -07:00 committed by GitHub
parent e50d793454
commit c5ceca2aa8
3 changed files with 222 additions and 272 deletions

View file

@ -233,12 +233,6 @@
"value" : ": %@"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : " : %@"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@ -280,12 +274,6 @@
"value" : ": %d"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : " : %d"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@ -7774,6 +7762,7 @@
}
},
"Bearing: %@" : {
"extractionState" : "stale",
"localizations" : {
"ru" : {
"stringUnit" : {
@ -7784,6 +7773,7 @@
}
},
"Bearing: N/A" : {
"extractionState" : "stale",
"localizations" : {
"ru" : {
"stringUnit" : {
@ -17895,6 +17885,7 @@
}
},
"Distance: %@" : {
"extractionState" : "stale",
"localizations" : {
"es" : {
"stringUnit" : {
@ -31390,10 +31381,6 @@
"comment" : "A description of the read-only mode feature in TAK Server.",
"isCommentAutoGenerated" : true
},
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : {
"comment" : "Privacy policy text for Meshtastic.",
"isCommentAutoGenerated" : true
},
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : {
"localizations" : {
"es" : {
@ -52718,7 +52705,6 @@
}
}
}
},
"TAK Cannot Be Used on Public Channel" : {
"comment" : "A warning displayed when the user's primary channel is public.",
@ -62963,88 +62949,6 @@
}
}
}
},
": %@" : {
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %@"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %@"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %@"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %@"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %@"
}
}
},
"shouldTranslate" : false
},
": %d" : {
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %d"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %d"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %d"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %d"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %d"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : ": %d"
}
}
},
"shouldTranslate" : false
}
},
"version" : "1.1"

View file

@ -13,9 +13,9 @@ struct CompassView: View {
/// Single waypoint parameter
let waypointLocation: CLLocationCoordinate2D?
let waypointName: String?
let color: Color
@ObservedObject private var locationsHandler = LocationsHandler.shared
@ -24,6 +24,8 @@ struct CompassView: View {
private let alignmentTolerance: Double = 5.0
@State private var inAlignment = false
private let dialRadius: CGFloat = 140
// Compute bearing from user waypoint
private func bearingToWaypoint() -> Double? {
guard
@ -39,10 +41,9 @@ struct CompassView: View {
// 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)
let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360)
let diff = min(rawDiff, 360 - rawDiff)
if diff <= alignmentTolerance {
if !inAlignment {
inAlignment = true
@ -53,124 +54,246 @@ struct CompassView: View {
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
formatter.numberFormatter.maximumFractionDigits = 1
return formatter.string(from: measurement)
}
var body: some View {
NavigationStack {
VStack(spacing: 15) {
VStack(spacing: 8) {
Text(waypointName ?? "Waypoint")
.font(.title2)
.bold()
.foregroundColor(color)
ZStack {
Color.black.ignoresSafeArea()
VStack(spacing: 0) {
// Top fixed heading indicator triangle
Image(systemName: "triangle.fill")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.rotationEffect(.degrees(180))
.padding(.bottom, 4)
// Rotating compass dial
ZStack {
// Outer bezel ring
Circle()
.stroke(Color.white.opacity(0.2), lineWidth: 1.5)
.frame(width: dialRadius * 2 + 20, height: dialRadius * 2 + 20)
// Tick marks
ForEach(0..<360, id: \.self) { degree in
CompassTickMark(degree: Double(degree), radius: dialRadius)
}
// Cardinal and intercardinal labels
ForEach(CompassLabel.allLabels, id: \.degrees) { label in
CompassLabelView(label: label, radius: dialRadius - 28)
.rotationEffect(.degrees(-locationsHandler.heading))
}
// North triangle indicator at 0°
CompassNorthIndicator(radius: dialRadius + 2)
// Degree readout at center
VStack(spacing: 4) {
Text(headingText())
.font(.system(size: 42, weight: .light, design: .rounded))
.foregroundColor(.white)
.monospacedDigit()
if let distance = distanceToWaypoint() {
Text(formatDistance(distance))
.font(.system(size: 18, weight: .semibold, design: .rounded))
.foregroundColor(color)
}
if waypointName != nil || waypointLocation != nil {
Text(waypointName ?? "Waypoint")
.font(.system(size: 13, weight: .medium))
.foregroundColor(color.opacity(0.8))
}
}
// Waypoint bearing indicator
if let bearing = bearingToWaypoint() {
WaypointMarkerView(
bearing: bearing,
radius: dialRadius + 14,
color: color
)
.onChange(of: locationsHandler.heading) { _, _ in
checkAlignment(bearing: bearing, heading: locationsHandler.heading)
}
}
}
.frame(width: dialRadius * 2 + 40, height: dialRadius * 2 + 40)
.rotationEffect(Angle(degrees: -locationsHandler.heading))
// Bottom info
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)
VStack(spacing: 6) {
HStack(spacing: 4) {
Image(systemName: "mappin")
.font(.system(size: 11))
Text("\(String(format: "%.4f", wp.latitude)), \(String(format: "%.4f", wp.longitude))")
.font(.system(size: 12, design: .monospaced))
}
}
HStack {
Image(systemName: "location.north")
.foregroundColor(.white.opacity(0.5))
if let bearing = bearingToWaypoint() {
Text("Bearing: \(String(format: "%.0f°", bearing))")
.font(.subheadline)
} else {
Text("Bearing: N/A")
.font(.subheadline)
HStack(spacing: 4) {
Image(systemName: "location.north.fill")
.font(.system(size: 11))
.rotationEffect(.degrees(bearing))
Text("\(String(format: "%.0f°", bearing))")
.font(.system(size: 12, weight: .medium, design: .monospaced))
}
.foregroundColor(color.opacity(0.7))
}
}
.padding(.top, 20)
}
}
.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")
}
.statusBar(hidden: true)
.onAppear {
locationsHandler.startHeadingUpdates()
locationsHandler.startLocationUpdates()
}
.onDisappear {
locationsHandler.stopHeadingUpdates()
locationsHandler.stopLocationUpdates()
}
.navigationTitle("Compass")
.toolbarColorScheme(.dark, for: .navigationBar)
}
}
private func headingText() -> String {
let h = Int(locationsHandler.heading.rounded()) % 360
return "\(h)°"
}
}
// MARK: - Compass Tick Mark
struct CompassTickMark: View {
let degree: Double
let radius: CGFloat
var body: some View {
let isCardinal = degree.truncatingRemainder(dividingBy: 90) == 0
let isMajor = degree.truncatingRemainder(dividingBy: 30) == 0
let isMinor = degree.truncatingRemainder(dividingBy: 10) == 0
let length: CGFloat = isCardinal ? 16 : (isMajor ? 12 : (isMinor ? 8 : 4))
let width: CGFloat = isCardinal ? 2.5 : (isMajor ? 1.5 : 1)
let tickColor: Color = isCardinal ? .white : (isMajor ? .white.opacity(0.7) : .white.opacity(0.3))
// Only draw ticks at 2° intervals
if Int(degree) % 2 == 0 {
Capsule()
.fill(tickColor)
.frame(width: width, height: length)
.offset(y: -(radius - length / 2))
.rotationEffect(.degrees(degree))
}
}
}
// MARK: - North Indicator
struct CompassNorthIndicator: View {
let radius: CGFloat
var body: some View {
Triangle()
.fill(Color.orange)
.frame(width: 12, height: 10)
.offset(y: -(radius + 8))
}
}
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.closeSubpath()
return path
}
}
// MARK: - Compass Label Model & View
struct CompassLabel {
let degrees: Double
let text: String
let isCardinal: Bool
static let allLabels: [CompassLabel] = [
CompassLabel(degrees: 0, text: "N", isCardinal: true),
CompassLabel(degrees: 45, text: "NE", isCardinal: false),
CompassLabel(degrees: 90, text: "E", isCardinal: true),
CompassLabel(degrees: 135, text: "SE", isCardinal: false),
CompassLabel(degrees: 180, text: "S", isCardinal: true),
CompassLabel(degrees: 225, text: "SW", isCardinal: false),
CompassLabel(degrees: 270, text: "W", isCardinal: true),
CompassLabel(degrees: 315, text: "NW", isCardinal: false)
]
}
struct CompassLabelView: View {
let label: CompassLabel
let radius: CGFloat
var body: some View {
Text(label.text)
.font(.system(size: label.isCardinal ? 18 : 13,
weight: label.isCardinal ? .bold : .medium))
.foregroundColor(label.degrees == 0 ? .orange : .white)
.rotationEffect(.degrees(-label.degrees))
.offset(y: -radius)
.rotationEffect(.degrees(label.degrees))
}
}
// MARK: - Waypoint Marker View
struct WaypointMarkerView: View {
let bearing: Double
let compassDegrees: Double
let radius: CGFloat
let color: Color
var body: some View {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(color)
.offset(y: -170)
.rotationEffect(Angle(degrees: bearing))
}
ZStack {
// Outer glow
Image(systemName: "arrowtriangle.up.fill")
.font(.system(size: 20, weight: .bold))
.foregroundColor(color.opacity(0.3))
.offset(y: -(radius + 4))
.rotationEffect(.degrees(bearing))
// Arrow
Image(systemName: "arrowtriangle.up.fill")
.font(.system(size: 16, weight: .bold))
.foregroundColor(color)
.offset(y: -(radius + 5))
.rotationEffect(.degrees(bearing))
}
}
}
// MARK: - Bearing Calculator
@ -199,78 +322,6 @@ struct BearingCalculator {
}
}
// 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 {

View file

@ -114,24 +114,19 @@ struct WaypointForm: View {
HStack {
Text("Icon")
Spacer()
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
TextField("Select an emoji", text: $icon)
.keyboardType(.emoji)
.font(.title)
.focused($iconIsFocused)
.onChange(of: icon) {
// If it contains non-emoji characters, clear it
if !icon.onlyEmojis() {
icon = ""
return
.onChange(of: icon) { _, value in
// If a second emoji is entered delete the first one
if value.count >= 1 {
if value.count > 1 {
let index = value.index(value.startIndex, offsetBy: 1)
icon = String(value[index])
}
}
// If multiple emojis are entered or pasted, keep only the last one
if icon.count > 1 {
icon = String(icon.suffix(1))
}
iconIsFocused = false
}
}
Toggle(isOn: $expires) {
Label("Expires", systemImage: "clock.badge.xmark")