diff --git a/src/Shared/PresetsDisplay/TagKey.swift b/src/Shared/PresetsDisplay/TagKey.swift new file mode 100644 index 000000000..77a57fd78 --- /dev/null +++ b/src/Shared/PresetsDisplay/TagKey.swift @@ -0,0 +1,44 @@ +// +// 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 = ["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 + } +} diff --git a/src/iOS/Go Map!!.xcodeproj/project.pbxproj b/src/iOS/Go Map!!.xcodeproj/project.pbxproj index 62c74711e..ff6c58d36 100644 --- a/src/iOS/Go Map!!.xcodeproj/project.pbxproj +++ b/src/iOS/Go Map!!.xcodeproj/project.pbxproj @@ -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 */; }; @@ -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 */; }; @@ -392,6 +394,7 @@ 021BC2212D7625FB004631C5 /* Panoramax.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Panoramax.storyboard; sourceTree = ""; }; 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 = ""; }; + C8E2A0022F5C000100000001 /* TagKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagKey.swift; sourceTree = ""; }; 021FADBF258594EE00F6E1C0 /* PresetDisplayValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetDisplayValue.swift; sourceTree = ""; }; 021FADC42585951E00F6E1C0 /* PresetDisplayGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetDisplayGroup.swift; sourceTree = ""; }; 021FADCC25873C8200F6E1C0 /* PresetsDatabase+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PresetsDatabase+Display.swift"; sourceTree = ""; }; @@ -600,6 +603,7 @@ 647F46CD2253EA4C00CEC482 /* MeasureDirectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasureDirectionViewModel.swift; sourceTree = ""; }; 647F46D02253F08200CEC482 /* HeadingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadingProvider.swift; sourceTree = ""; }; 64C072F9226227D500598078 /* PresetKeyTagCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetKeyTagCase.swift; sourceTree = ""; }; + C8E2A0042F5C000100000001 /* TagKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagKeyTests.swift; sourceTree = ""; }; 64D74BF12253DF49004FFD20 /* DirectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionViewController.swift; sourceTree = ""; }; 64E21EB422651C06004605D7 /* OSMMapDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMMapDataTestCase.swift; sourceTree = ""; }; 64E21EB722651F2D004605D7 /* XCTestCase+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+UserDefaults.swift"; sourceTree = ""; }; @@ -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 */, @@ -1185,6 +1190,7 @@ children = ( 64E21EB622651F0D004605D7 /* Helpers */, 64C072F9226227D500598078 /* PresetKeyTagCase.swift */, + C8E2A0042F5C000100000001 /* TagKeyTests.swift */, 64348CFA225E867800ADE7FB /* MeasureDirectionViewModelTestCase.swift */, 64348CF6225E867800ADE7FB /* Mocks */, 64348D02225E8E4300ADE7FB /* OsmNode_DirectionTestCase.swift */, @@ -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 */, @@ -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 */, diff --git a/src/iOS/GoMapTests/TagKeyTests.swift b/src/iOS/GoMapTests/TagKeyTests.swift new file mode 100644 index 000000000..22a8f984b --- /dev/null +++ b/src/iOS/GoMapTests/TagKeyTests.swift @@ -0,0 +1,25 @@ +// +// 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)\"") + } + } +} diff --git a/src/iOS/POI/KeyValueTableCell.swift b/src/iOS/POI/KeyValueTableCell.swift index 8a195652e..61ba2dc1e 100644 --- a/src/iOS/POI/KeyValueTableCell.swift +++ b/src/iOS/POI/KeyValueTableCell.swift @@ -166,12 +166,20 @@ 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() } + } else if TagKey.isNameLike(key) { + useTextField() } else { switch key { case "note", "comment", "description", "fixme", "inscription", "source": diff --git a/src/iOS/POI/POIAllTagsViewController.swift b/src/iOS/POI/POIAllTagsViewController.swift index 5066327d7..ece671348 100644 --- a/src/iOS/POI/POIAllTagsViewController.swift +++ b/src/iOS/POI/POIAllTagsViewController.swift @@ -322,6 +322,9 @@ class POIAllTagsViewController: UITableViewController, POIFeaturePickerDelegate, cell.text1.autocapitalizationType = .none cell.text1.spellCheckingType = .no cell.text2.defaultInputAccessoryView = prevNextToolbar + if TagKey.isNameLike(kv.k) { + TagKey.applyNameLikeTraits(to: cell.text2, presets: allPresetKeys) + } cell.isSet.backgroundColor = kv.k == "" || kv.v == "" ? nil : UIColor.systemBlue return cell diff --git a/src/iOS/POI/POICommonTagsViewController.swift b/src/iOS/POI/POICommonTagsViewController.swift index cd25d81d5..38078a80d 100644 --- a/src/iOS/POI/POICommonTagsViewController.swift +++ b/src/iOS/POI/POICommonTagsViewController.swift @@ -435,6 +435,9 @@ class POICommonTagsViewController: UITableViewController, UITextFieldDelegate, U cell.presetKey = .key(presetKey) cell.valueField.keyboardType = presetKey.keyboardType cell.valueField.autocapitalizationType = presetKey.autocapitalizationType + if TagKey.isNameLike(presetKey.tagKey), presetKey.autocapitalizationType == .none { + TagKey.applyNameLikeTraits(to: cell.valueField, presets: allPresetKeys) + } cell.valueField.removeTarget(self, action: nil, for: .allEvents) cell.valueField.addTarget(self, action: #selector(textFieldReturn(_:)), for: .editingDidEndOnExit) diff --git a/src/iOS/POI/PresetValueTextField.swift b/src/iOS/POI/PresetValueTextField.swift index d7d5a8e2b..e0e615a14 100644 --- a/src/iOS/POI/PresetValueTextField.swift +++ b/src/iOS/POI/PresetValueTextField.swift @@ -83,6 +83,12 @@ class PresetValueTextField: AutocompleteTextField, PanoramaxDelegate { { inputAccessoryView = TelephoneToolbar(forTextField: self, frame: frame) } + + if TagKey.isNameLike(key), preset.autocapitalizationType == .none { + TagKey.applyNameLikeTraits(to: self, presets: owner.allPresetKeys) + } + } else if TagKey.isNameLike(key) { + TagKey.applyNameLikeTraits(to: self, presets: owner.allPresetKeys) } else { switch key { case "note", "comment", "description", "fixme", "inscription", "source":