Skip to content

Instantly share code, notes, and snippets.

@wingovers
Last active January 16, 2022 13:45
Show Gist options
  • Save wingovers/5897dda804a8f2e6280ec52a79340a5e to your computer and use it in GitHub Desktop.
Save wingovers/5897dda804a8f2e6280ec52a79340a5e to your computer and use it in GitHub Desktop.
Guess iOS Device Owner Name

Goal: Predict an iOS user's name

To personalize an onboarding process, I'd like to prepopulate the response for "What's your name?".

Existing Approaches

  1. In iOS, the "Me" card is not public, unlike macOS.
  2. UIDevice.current.name could be a localized "Name's iPhone"
  3. Proposals found online all parse that string using Regex. Examples:
  1. Apparently Square and Twitter exact-match that string to a CNContact name to auto-fill sign-up forms, but that fails with a single mismatched character.
  2. 0xced proposed an undocumented API for pulling an email address to match against contacts, falling back to UIDevice.current.name. I'm not sure if that's still available.

Drawbacks

  • Regex implementations adapt poorly across languages, particularly ones you don't know :)
  • Example: all errantly assumed "de" or two-letter words are not part of a name, but that is not true of Italian, Spanish, or say surnames like Ng in Vietnamese or Yu in Chinese
  • Maintainability of complex regex is a bit more delicate than other Swift 5 string parsing functions
  • Duplicate device tags (e.g., "iPhone (2)") slip through
  • For macOS Me card, a permissions request for the user's name alone is unecessarily intrusive

My Approach

  • Parse UIDevice.current.name using string components (e.g., string.components(separatedBy: "iPhone de ")), which in two operations can delete both posessive patterns and yet-unknown variants (e.g., the "'s iPhone " and "20" in "Ryan's iPhone 20")
  • Cover more languages respectfully by feeding a prefix or suffix-stripped substring into PersonNameComponentsFormatter. It does not require specifying the string's language
  • For Chinese, separately process string components to remove English (e.g. 冯锐的iPhone) and appropriate posessive or shorthand (e.g., renaming of iPhone to Chinese for generic mobile phone)

My Deficits

  • Non-apostrophe posessive forms aren't stripped out (e.g., Swedish: Johannes iPhone -> Johannes)
  • I fixed that for Chinese, including removing "手机“ (generic for cell phone), but manual work is needed for other languages that I don't speak. Perhaps a combination of NSLinguisticTagger.dominantLanguage(for: text) and list of translations for "phone" and removable posessives would be a somewhat faster way to cover more situations.
  • Sometimes non-name words can slip through
// Ryan Ferrell say(hello) { github.com/wingovers }
class Autofiller {
enum NameComponent {
case givenName
case familyName
case fullNameInCurrentPersonNameComponentsFormatterStyle
}
/// Proposes a localized name based on UIDevice.current.name (under the assumption that it contains a name).
/// - Returns: A user's probable first, last, or full name — or a default if detection fails.
///
/// Be aware that:
/// * Non-name words may slip through
/// ```
/// Paul The Great // Paul the Great
/// Paul's Really Old iPhone // Paul
/// ```
/// * This is only tested for romance languages and Chinese.
/// * Chinese names return full name in `givenName` only mode. Options require uncommenting internal code.
///
/// - Parameter name: Choose between given, family, and full name
/// - Parameter style: Options for [PersonNameComponentsFormatter](https://developer.apple.com/documentation/foundation/personnamecomponentsformatter)
/// - Parameter defaultUponFailure: Specify your default string should guessing fail
func guessNameOfDeviceOwner(name: NameComponent,
style: PersonNameComponentsFormatter.Style = .default,
placeholderUponFailure: String = "Good Looking") -> String {
let deviceName = UIDevice.current.name
let nameFormatter = PersonNameComponentsFormatter()
nameFormatter.style = style
if let chineseName = extractNameComponentsInChinese(from: deviceName) {
switch name {
case .givenName:
return nameFormatter.string(from: chineseName)
// DEFAULT: RETURN FULL NAME (EVEN WHEN OTHER LANGUAGES RETURN GIVEN ONLY)
// OPTION: CUTESY INFORMAL GIVEN NAME
// if let givenName = chineseName.givenName {
// return String("小").appending(givenName)
case .familyName:
if let familyName = chineseName.familyName {
return familyName
}
// OPTION: RESPECTFUL FAMILY NAME
// if let familyName = chineseName.familyName {
// return String("老").appending(familyName)
case .fullNameInCurrentPersonNameComponentsFormatterStyle:
return nameFormatter.string(from: chineseName)
}
}
if let latinName = extractNameComponentsByPrefixOrSuffix(from: deviceName) {
switch name {
case .givenName:
if let givenName = latinName.givenName {
return givenName
}
case .familyName:
if let familyName = latinName.familyName {
return familyName
}
case .fullNameInCurrentPersonNameComponentsFormatterStyle:
return nameFormatter.string(from: latinName)
}
}
return placeholderUponFailure
}
/// Process common styles for English (Ryan's iPhone), Swedish (Ryan iPhone), French (iPhone de Ryan)
private func extractNameComponentsByPrefixOrSuffix(from input: String) -> PersonNameComponents? {
let formatter = PersonNameComponentsFormatter()
let prefixes = ["iPhone de ",
"iPad de ",
"iPod de "
]
for prefix in prefixes {
guard input.contains(prefix) else { continue }
var inputComponents = input.components(separatedBy: prefix)
// First element is either empty or assumed to be extraneous
inputComponents.removeFirst()
let possibleName = inputComponents.joined()
// Note: .personNameComponents(from:) will ignore brackets, parentheses
guard let nameComponents = formatter.personNameComponents(from: possibleName) else { return nil }
return nameComponents
}
let suffixes = ["'s iPhone",
"'s iPad'",
"'s iPod",
"'s ", // Capture if user removed "i" or has a descriptor (e.g., Paul's Really Old iPhone)
"iPhone", // For Swedish style, reached if posessive language not present
"iPad",
"iPod",
"Phone", // Latter iterations, if reached, cover an edge case like me, a nerd who named his phone "RyPhone"
"Pad",
"Pod"
]
for suffix in suffixes {
guard input.contains(suffix) else { continue }
var inputComponents = input.components(separatedBy: suffix)
// The last component is either emptty, contains the model (e.g., "XS"), or duplicate device number (e.g., "(2)")
inputComponents.removeLast()
let possibleName = inputComponents.joined()
guard let nameComponents = formatter.personNameComponents(from: possibleName) else { return nil }
return nameComponents
}
// If no prefix/suffix matches, attempt to parse a name. Otherwise return nil to indicate failure.
guard let possibleName = formatter.personNameComponents(from: input) else { return nil }
return possibleName
}
/// Process for Chinese name apart from neighboring English (e.g., "某人的iPhone")
private func extractNameComponentsInChinese(from input: String) -> PersonNameComponents? {
guard let range = input.range(of: "\\p{Han}*\\p{Han}", options: .regularExpression) else { return nil }
// Extract of only Chinese characters, ignoring "iPhone" etc
var possibleName = input[range]
// Remove possible instance of "cell phone"
possibleName = Substring(String(possibleName).replacingOccurrences(of: "手机", with: ""))
// Remove possible posessive referring to iPhone or cell phone
if possibleName.last == "的" { possibleName.removeLast(1) }
let formatter = PersonNameComponentsFormatter()
guard let nameComponents = formatter.personNameComponents(from: String(possibleName)) else { return nil }
return nameComponents
}
}
@vdhamer
Copy link

vdhamer commented Jan 15, 2022

I appreciate the nicely readable code. But... lines 92-95 don't work.
This is the code that extracts, for example, "Peter" from "Peter's iPad".
Firstly, line 93 says "'s iPad'" instead of "'s iPad" (a matched pair of single quotes)
Secondly, the text on my iPad uses a RightSingleQuotationMark (U+2019) instead of Apostrophe (U+0027).
My Apple Watch and iMac also go with RightSingleQuotationMark.
My iPhone (which has been manually edited) uses a straightforward ASCII Apostrophe.

Could you confirm and fix the GIST (I am not really a Git user)?

I will fix my local copy to support both types of single quotes.

Here are both types of quotes if you need to check in a hex editor:
abc'abc U+0027 Apostophe (single byte ASCII)
abc’abc U+2019 Right Single Quotation Mark (3-byte Unicode character)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment