Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/Shared/PresetsDisplay/TagKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// TagKey.swift
// Go Map!!
//
// Copyright © 2026 Bryce Cogswell. All rights reserved.
//

import UIKit

/// OSM tag key helpers shared by the POI editor.
enum TagKey {
private static let exactNameLikeKeys: Set<String> = ["name", "alt_name", "old_name"]

/// Keys that carry human-readable names and should use the same keyboard traits as `name`.
static func isNameLike(_ key: String) -> Bool {
guard !key.isEmpty else { return false }
if exactNameLikeKeys.contains(key) {
return true
}
return key.hasPrefix("name:")
}

static func autocapitalizationType(matchingNamePresetIn presets: [PresetDisplayKey])
-> UITextAutocapitalizationType
{
presets.first(where: { $0.tagKey == "name" })?.autocapitalizationType ?? .words
}

static func autocorrectType(matchingNamePresetIn presets: [PresetDisplayKey]) -> UITextAutocorrectionType {
presets.first(where: { $0.tagKey == "name" })?.autocorrectType ?? .no
}

static func applyNameLikeTraits(to textField: UITextField, presets: [PresetDisplayKey]) {
textField.autocapitalizationType = autocapitalizationType(matchingNamePresetIn: presets)
textField.autocorrectionType = autocorrectType(matchingNamePresetIn: presets)
textField.spellCheckingType = textField.autocorrectionType == .no ? .no : .default
}

static func applyNameLikeTraits(to textView: UITextView, presets: [PresetDisplayKey]) {
textView.autocapitalizationType = autocapitalizationType(matchingNamePresetIn: presets)
textView.autocorrectionType = autocorrectType(matchingNamePresetIn: presets)
textView.spellCheckingType = textView.autocorrectionType == .no ? .no : .default
}

/// Configure a free-form key/value value field (All Tags, Common Tags extras).
static func configureKeyValueField(_ textField: UITextField, key: String, presets: [PresetDisplayKey]) {
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.spellCheckingType = .no
if isNameLike(key) {
applyNameLikeTraits(to: textField, presets: presets)
}
}

/// Configure a preset-driven value field, overriding `.none` for name-like keys.
static func configurePresetValueField(_ textField: UITextField,
key: String,
preset: PresetDisplayKey,
presets: [PresetDisplayKey])
{
textField.autocapitalizationType = preset.autocapitalizationType
textField.autocorrectionType = preset.autocorrectType
textField.spellCheckingType = preset.autocorrectType == .no ? .no : .default
if isNameLike(key), preset.autocapitalizationType == .none {
applyNameLikeTraits(to: textField, presets: presets)
}
}

/// Apply name-like traits when editing, if schema did not already specify capitalization.
static func applyNameLikeOverrideIfNeeded(to textField: UITextField,
key: String,
preset: PresetDisplayKey?,
presets: [PresetDisplayKey])
{
guard isNameLike(key) else { return }
if preset == nil || preset?.autocapitalizationType == .none {
applyNameLikeTraits(to: textField, presets: presets)
}
}
}
8 changes: 8 additions & 0 deletions src/iOS/Go Map!!.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
021BC2222D7625FB004631C5 /* Panoramax.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 021BC2212D7625FB004631C5 /* Panoramax.storyboard */; };
021C6AB3168768C800FB17B0 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 021C6AB2168768C800FB17B0 /* MessageUI.framework */; };
021FADBB258591D000F6E1C0 /* PresetDisplayKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021FADBA258591D000F6E1C0 /* PresetDisplayKey.swift */; };
C8E2A0012F5C000100000001 /* TagKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E2A0022F5C000100000001 /* TagKey.swift */; };
021FADC0258594EE00F6E1C0 /* PresetDisplayValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021FADBF258594EE00F6E1C0 /* PresetDisplayValue.swift */; };
021FADC52585951E00F6E1C0 /* PresetDisplayGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021FADC42585951E00F6E1C0 /* PresetDisplayGroup.swift */; };
021FADCD25873C8200F6E1C0 /* PresetsDatabase+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021FADCC25873C8200F6E1C0 /* PresetsDatabase+Display.swift */; };
Expand Down Expand Up @@ -238,6 +239,7 @@
647F46CE2253EA4C00CEC482 /* MeasureDirectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F46CD2253EA4C00CEC482 /* MeasureDirectionViewModel.swift */; };
647F46D12253F08200CEC482 /* HeadingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F46D02253F08200CEC482 /* HeadingProvider.swift */; };
64C072FA226227D500598078 /* PresetKeyTagCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C072F9226227D500598078 /* PresetKeyTagCase.swift */; };
C8E2A0032F5C000100000001 /* TagKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E2A0042F5C000100000001 /* TagKeyTests.swift */; };
64D74BF32253DF49004FFD20 /* DirectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D74BF12253DF49004FFD20 /* DirectionViewController.swift */; };
64E21EB522651C06004605D7 /* OSMMapDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E21EB422651C06004605D7 /* OSMMapDataTestCase.swift */; };
64E21EB822651F2D004605D7 /* XCTestCase+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E21EB722651F2D004605D7 /* XCTestCase+UserDefaults.swift */; };
Expand Down Expand Up @@ -392,6 +394,7 @@
021BC2212D7625FB004631C5 /* Panoramax.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Panoramax.storyboard; sourceTree = "<group>"; };
021C6AB2168768C800FB17B0 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
021FADBA258591D000F6E1C0 /* PresetDisplayKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetDisplayKey.swift; sourceTree = "<group>"; };
C8E2A0022F5C000100000001 /* TagKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagKey.swift; sourceTree = "<group>"; };
021FADBF258594EE00F6E1C0 /* PresetDisplayValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetDisplayValue.swift; sourceTree = "<group>"; };
021FADC42585951E00F6E1C0 /* PresetDisplayGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetDisplayGroup.swift; sourceTree = "<group>"; };
021FADCC25873C8200F6E1C0 /* PresetsDatabase+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PresetsDatabase+Display.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -600,6 +603,7 @@
647F46CD2253EA4C00CEC482 /* MeasureDirectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasureDirectionViewModel.swift; sourceTree = "<group>"; };
647F46D02253F08200CEC482 /* HeadingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadingProvider.swift; sourceTree = "<group>"; };
64C072F9226227D500598078 /* PresetKeyTagCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetKeyTagCase.swift; sourceTree = "<group>"; };
C8E2A0042F5C000100000001 /* TagKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagKeyTests.swift; sourceTree = "<group>"; };
64D74BF12253DF49004FFD20 /* DirectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionViewController.swift; sourceTree = "<group>"; };
64E21EB422651C06004605D7 /* OSMMapDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMMapDataTestCase.swift; sourceTree = "<group>"; };
64E21EB722651F2D004605D7 /* XCTestCase+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+UserDefaults.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1088,6 +1092,7 @@
021FADD625873D0100F6E1C0 /* PresetDisplayForFeature.swift */,
021FADC42585951E00F6E1C0 /* PresetDisplayGroup.swift */,
021FADBA258591D000F6E1C0 /* PresetDisplayKey.swift */,
C8E2A0022F5C000100000001 /* TagKey.swift */,
021FADD125873CC000F6E1C0 /* PresetDisplayKeyUserDefined.swift */,
021FADBF258594EE00F6E1C0 /* PresetDisplayValue.swift */,
021FADCC25873C8200F6E1C0 /* PresetsDatabase+Display.swift */,
Expand Down Expand Up @@ -1185,6 +1190,7 @@
children = (
64E21EB622651F0D004605D7 /* Helpers */,
64C072F9226227D500598078 /* PresetKeyTagCase.swift */,
C8E2A0042F5C000100000001 /* TagKeyTests.swift */,
64348CFA225E867800ADE7FB /* MeasureDirectionViewModelTestCase.swift */,
64348CF6225E867800ADE7FB /* Mocks */,
64348D02225E8E4300ADE7FB /* OsmNode_DirectionTestCase.swift */,
Expand Down Expand Up @@ -1640,6 +1646,7 @@
02CE3CE929919FC400DDACE0 /* MapPinButton.swift in Sources */,
EDDBA55326130287001E7D5C /* GpxViewController.swift in Sources */,
021FADBB258591D000F6E1C0 /* PresetDisplayKey.swift in Sources */,
C8E2A0012F5C000100000001 /* TagKey.swift in Sources */,
C381C30B263FE518003142BA /* PushPinView.swift in Sources */,
02CB2407265DB7CB00835F32 /* LevenshteinDistance.swift in Sources */,
C3004F54263A99DD006BF313 /* POIAttributesViewController.swift in Sources */,
Expand Down Expand Up @@ -1807,6 +1814,7 @@
files = (
64348CFE225E867800ADE7FB /* MeasureDirectionViewModelTestCase.swift in Sources */,
64C072FA226227D500598078 /* PresetKeyTagCase.swift in Sources */,
C8E2A0032F5C000100000001 /* TagKeyTests.swift in Sources */,
64348CFC225E867800ADE7FB /* MeasureDirectionViewModelDelegateMock.swift in Sources */,
64348CED225E7CD900ADE7FB /* GoMapTests.swift in Sources */,
64305F7423E723B200232BB9 /* LocationURLParserTestCase.swift in Sources */,
Expand Down
74 changes: 74 additions & 0 deletions src/iOS/GoMapTests/TagKeyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// TagKeyTests.swift
// GoMapTests
//
// Copyright © 2026 Bryce Cogswell. All rights reserved.
//

@testable import Go_Map__
import XCTest

class TagKeyTests: XCTestCase {
func testIsNameLikePositiveCases() {
let positive = ["name", "name:en", "name:zh-Hans", "alt_name", "old_name"]
for key in positive {
XCTAssertTrue(TagKey.isNameLike(key), "expected name-like: \(key)")
}
}

func testIsNameLikeNegativeCases() {
let negative = ["namesake", "name_source", ""]
for key in negative {
XCTAssertFalse(TagKey.isNameLike(key), "expected not name-like: \"\(key)\"")
}
}

func testConfigureKeyValueFieldAppliesNameTraits() {
let field = UITextField()
let namePreset = PresetDisplayKey(name: "Name",
type: .text,
tagKey: "name",
defaultValue: nil,
placeholder: nil,
keyboard: .default,
capitalize: .words,
autocorrect: .no,
presetValues: nil)
TagKey.configureKeyValueField(field, key: "name:de", presets: [namePreset])
XCTAssertEqual(field.autocapitalizationType, .words)
XCTAssertEqual(field.autocorrectionType, .no)
XCTAssertEqual(field.spellCheckingType, .no)
}

func testConfigureKeyValueFieldResetsNonNameKeys() {
let field = UITextField()
field.autocapitalizationType = .words
TagKey.configureKeyValueField(field, key: "ref", presets: [])
XCTAssertEqual(field.autocapitalizationType, .none)
XCTAssertEqual(field.autocorrectionType, .no)
}

func testConfigurePresetValueFieldOverridesNoneForNameLikeKeys() {
let field = UITextField()
let namePreset = PresetDisplayKey(name: "Name",
type: .text,
tagKey: "name",
defaultValue: nil,
placeholder: nil,
keyboard: .default,
capitalize: .words,
autocorrect: .no,
presetValues: nil)
let altPreset = PresetDisplayKey(name: "Alt Name",
type: .text,
tagKey: "alt_name",
defaultValue: nil,
placeholder: nil,
keyboard: .default,
capitalize: .none,
autocorrect: .no,
presetValues: nil)
TagKey.configurePresetValueField(field, key: "alt_name", preset: altPreset, presets: [namePreset])
XCTAssertEqual(field.autocapitalizationType, .words)
}
}
8 changes: 7 additions & 1 deletion src/iOS/POI/KeyValueTableCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,15 @@ class KeyValueTableCell: TextPairTableCell, PresetValueTextFieldOwner, UITextFie

func selectTextViewFor(key: String) {
// set text formatting options for text field
if let preset = keyValueCellOwner?.allPresetKeys.first(where: { key == $0.tagKey }) {
let presets = keyValueCellOwner?.allPresetKeys ?? []
if let preset = presets.first(where: { key == $0.tagKey }) {
if preset.type == .textarea {
useTextView()
if TagKey.isNameLike(key) {
if let textView = textView {
TagKey.applyNameLikeTraits(to: textView, presets: presets)
}
}
} else {
useTextField()
}
Expand Down
1 change: 1 addition & 0 deletions src/iOS/POI/POIAllTagsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ class POIAllTagsViewController: UITableViewController, POIFeaturePickerDelegate,
cell.text1.autocapitalizationType = .none
cell.text1.spellCheckingType = .no
cell.text2.defaultInputAccessoryView = prevNextToolbar
TagKey.configureKeyValueField(cell.text2, key: kv.k, presets: allPresetKeys)

cell.isSet.backgroundColor = kv.k == "" || kv.v == "" ? nil : UIColor.systemBlue
return cell
Expand Down
6 changes: 5 additions & 1 deletion src/iOS/POI/POICommonTagsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ class POICommonTagsViewController: UITableViewController, UITextFieldDelegate, U
cell.text1?.text = extraTags[indexPath.row].k
cell.text2?.text = extraTags[indexPath.row].v
cell.text2.key = cell.text1?.text ?? ""
TagKey.configureKeyValueField(cell.text2, key: cell.text2.key, presets: allPresetKeys)
cell.isSet.backgroundColor = cell.value == "" ? nil : Self.isSetHighlight
return cell
}
Expand Down Expand Up @@ -434,7 +435,10 @@ class POICommonTagsViewController: UITableViewController, UITextFieldDelegate, U
cell.valueField.presetKey = presetKey
cell.presetKey = .key(presetKey)
cell.valueField.keyboardType = presetKey.keyboardType
cell.valueField.autocapitalizationType = presetKey.autocapitalizationType
TagKey.configurePresetValueField(cell.valueField,
key: presetKey.tagKey,
preset: presetKey,
presets: allPresetKeys)

cell.valueField.removeTarget(self, action: nil, for: .allEvents)
cell.valueField.addTarget(self, action: #selector(textFieldReturn(_:)), for: .editingDidEndOnExit)
Expand Down
3 changes: 3 additions & 0 deletions src/iOS/POI/PresetValueTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ class PresetValueTextField: AutocompleteTextField, PanoramaxDelegate {
{
inputAccessoryView = TelephoneToolbar(forTextField: self, frame: frame)
}

TagKey.applyNameLikeOverrideIfNeeded(to: self, key: key, preset: preset, presets: owner.allPresetKeys)
} else {
TagKey.applyNameLikeOverrideIfNeeded(to: self, key: key, preset: nil, presets: owner.allPresetKeys)
switch key {
case "note", "comment", "description", "fixme", "inscription", "source":
autocapitalizationType = .sentences
Expand Down