Formatting Phone number in Swift - ios
I'm formatting my textfiled text once the user start typing the phone number into this format type 0 (555) 444 66 77 and it is working fine but once I get the number from the server I get it like this 05554446677 So please could you tell me how I can edit it in the same format once I get it fro the server?
My code once I start typing:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if textField == phoneNumberTextField{
var newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
var components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet)
var decimalString = "".join(components) as NSString
var length = decimalString.length
var hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)
if length == 0 || (length > 11 && !hasLeadingOne) || length > 12{
var newLength = (textField.text as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 11) ? false : true
}
var index = 0 as Int
var formattedString = NSMutableString()
if hasLeadingOne{
formattedString.appendString("1 ")
index += 1
}
if (length - index) > 1{
var zeroNumber = decimalString.substringWithRange(NSMakeRange(index, 1))
formattedString.appendFormat("%# ", zeroNumber)
index += 1
}
if (length - index) > 3{
var areaCode = decimalString.substringWithRange(NSMakeRange(index, 3))
formattedString.appendFormat("(%#) ", areaCode)
index += 3
}
if (length - index) > 3{
var prefix = decimalString.substringWithRange(NSMakeRange(index, 3))
formattedString.appendFormat("%# ", prefix)
index += 3
}
if (length - index) > 3{
var prefix = decimalString.substringWithRange(NSMakeRange(index, 2))
formattedString.appendFormat("%# ", prefix)
index += 2
}
var remainder = decimalString.substringFromIndex(index)
formattedString.appendString(remainder)
textField.text = formattedString as String
return false
}else{
return true
}
}
Masked number typing
/// mask example: `+X (XXX) XXX-XXXX`
func format(with mask: String, phone: String) -> String {
let numbers = phone.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
var result = ""
var index = numbers.startIndex // numbers iterator
// iterate over the mask characters until the iterator of numbers ends
for ch in mask where index < numbers.endIndex {
if ch == "X" {
// mask requires a number in this place, so take the next one
result.append(numbers[index])
// move numbers iterator to the next index
index = numbers.index(after: index)
} else {
result.append(ch) // just append a mask character
}
}
return result
}
Call the above function from the UITextField delegate method:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return false }
let newString = (text as NSString).replacingCharacters(in: range, with: string)
textField.text = format(with: "+X (XXX) XXX-XXXX", phone: newString)
return false
}
So, that works better.
"" => ""
"0" => "+0"
"412" => "+4 (12"
"12345678901" => "+1 (234) 567-8901"
"a1_b2-c3=d4 e5&f6|g7h8" => "+1 (234) 567-8"
Really simple solution:
extension String {
func applyPatternOnNumbers(pattern: String, replacementCharacter: Character) -> String {
var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
for index in 0 ..< pattern.count {
guard index < pureNumber.count else { return pureNumber }
let stringIndex = String.Index(utf16Offset: index, in: pattern)
let patternCharacter = pattern[stringIndex]
guard patternCharacter != replacementCharacter else { continue }
pureNumber.insert(patternCharacter, at: stringIndex)
}
return pureNumber
}
}
Usage:
guard let text = textField.text else { return }
textField.text = text.applyPatternOnNumbers(pattern: "+# (###) ###-####", replacmentCharacter: "#")
Swift 3 & 4
This solution removes any non-numeric characters before applying formatting. It returns nil if the source phone number cannot be formatted according to assumptions.
Swift 4
The Swift 4 solution accounts for the deprecation of CharacterView and Sting becoming a collection of characters as the CharacterView is.
import Foundation
func format(phoneNumber sourcePhoneNumber: String) -> String? {
// Remove any character that is not a number
let numbersOnly = sourcePhoneNumber.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let length = numbersOnly.count
let hasLeadingOne = numbersOnly.hasPrefix("1")
// Check for supported phone number length
guard length == 7 || (length == 10 && !hasLeadingOne) || (length == 11 && hasLeadingOne) else {
return nil
}
let hasAreaCode = (length >= 10)
var sourceIndex = 0
// Leading 1
var leadingOne = ""
if hasLeadingOne {
leadingOne = "1 "
sourceIndex += 1
}
// Area code
var areaCode = ""
if hasAreaCode {
let areaCodeLength = 3
guard let areaCodeSubstring = numbersOnly.substring(start: sourceIndex, offsetBy: areaCodeLength) else {
return nil
}
areaCode = String(format: "(%#) ", areaCodeSubstring)
sourceIndex += areaCodeLength
}
// Prefix, 3 characters
let prefixLength = 3
guard let prefix = numbersOnly.substring(start: sourceIndex, offsetBy: prefixLength) else {
return nil
}
sourceIndex += prefixLength
// Suffix, 4 characters
let suffixLength = 4
guard let suffix = numbersOnly.substring(start: sourceIndex, offsetBy: suffixLength) else {
return nil
}
return leadingOne + areaCode + prefix + "-" + suffix
}
extension String {
/// This method makes it easier extract a substring by character index where a character is viewed as a human-readable character (grapheme cluster).
internal func substring(start: Int, offsetBy: Int) -> String? {
guard let substringStartIndex = self.index(startIndex, offsetBy: start, limitedBy: endIndex) else {
return nil
}
guard let substringEndIndex = self.index(startIndex, offsetBy: start + offsetBy, limitedBy: endIndex) else {
return nil
}
return String(self[substringStartIndex ..< substringEndIndex])
}
}
Swift 3
import Foundation
func format(phoneNumber sourcePhoneNumber: String) -> String? {
// Remove any character that is not a number
let numbersOnly = sourcePhoneNumber.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let length = numbersOnly.characters.count
let hasLeadingOne = numbersOnly.hasPrefix("1")
// Check for supported phone number length
guard length == 7 || (length == 10 && !hasLeadingOne) || (length == 11 && hasLeadingOne) else {
return nil
}
let hasAreaCode = (length >= 10)
var sourceIndex = 0
// Leading 1
var leadingOne = ""
if hasLeadingOne {
leadingOne = "1 "
sourceIndex += 1
}
// Area code
var areaCode = ""
if hasAreaCode {
let areaCodeLength = 3
guard let areaCodeSubstring = numbersOnly.characters.substring(start: sourceIndex, offsetBy: areaCodeLength) else {
return nil
}
areaCode = String(format: "(%#) ", areaCodeSubstring)
sourceIndex += areaCodeLength
}
// Prefix, 3 characters
let prefixLength = 3
guard let prefix = numbersOnly.characters.substring(start: sourceIndex, offsetBy: prefixLength) else {
return nil
}
sourceIndex += prefixLength
// Suffix, 4 characters
let suffixLength = 4
guard let suffix = numbersOnly.characters.substring(start: sourceIndex, offsetBy: suffixLength) else {
return nil
}
return leadingOne + areaCode + prefix + "-" + suffix
}
extension String.CharacterView {
/// This method makes it easier extract a substring by character index where a character is viewed as a human-readable character (grapheme cluster).
internal func substring(start: Int, offsetBy: Int) -> String? {
guard let substringStartIndex = self.index(startIndex, offsetBy: start, limitedBy: endIndex) else {
return nil
}
guard let substringEndIndex = self.index(startIndex, offsetBy: start + offsetBy, limitedBy: endIndex) else {
return nil
}
return String(self[substringStartIndex ..< substringEndIndex])
}
}
Example
func testFormat(sourcePhoneNumber: String) -> String {
if let formattedPhoneNumber = format(phoneNumber: sourcePhoneNumber) {
return "'\(sourcePhoneNumber)' => '\(formattedPhoneNumber)'"
}
else {
return "'\(sourcePhoneNumber)' => nil"
}
}
print(testFormat(sourcePhoneNumber: "1 800 222 3333"))
print(testFormat(sourcePhoneNumber: "18002223333"))
print(testFormat(sourcePhoneNumber: "8002223333"))
print(testFormat(sourcePhoneNumber: "2223333"))
print(testFormat(sourcePhoneNumber: "18002223333444"))
print(testFormat(sourcePhoneNumber: "Letters8002223333"))
print(testFormat(sourcePhoneNumber: "1112223333"))
Example Output
'1 800 222 3333' => '1 (800) 222-3333'
'18002223333' => '1 (800) 222-3333'
'8002223333' => '(800) 222-3333'
'2223333' => '222-3333'
'18002223333444' => nil
'Letters8002223333' => '(800) 222-3333'
'1112223333' => nil
Manipulations with characters in String are not very straightforward. You need following:
Swift 2.1
let s = "05554446677"
let s2 = String(format: "%# (%#) %# %# %#", s.substringToIndex(s.startIndex.advancedBy(1)),
s.substringWithRange(s.startIndex.advancedBy(1) ... s.startIndex.advancedBy(3)),
s.substringWithRange(s.startIndex.advancedBy(4) ... s.startIndex.advancedBy(6)),
s.substringWithRange(s.startIndex.advancedBy(7) ... s.startIndex.advancedBy(8)),
s.substringWithRange(s.startIndex.advancedBy(9) ... s.startIndex.advancedBy(10))
)
Swift 2.0
let s = "05554446677"
let s2 = String(format: "%# (%#) %# %# %#", s.substringToIndex(advance(s.startIndex, 1)),
s.substringWithRange(advance(s.startIndex, 1) ... advance(s.startIndex, 3)),
s.substringWithRange(advance(s.startIndex, 4) ... advance(s.startIndex, 6)),
s.substringWithRange(advance(s.startIndex, 7) ... advance(s.startIndex, 8)),
s.substringWithRange(advance(s.startIndex, 9) ... advance(s.startIndex, 10))
)
Code will print
0 (555) 444 66 77
Swift 4
Create this function and call on text field event Editing Changed
private func formatPhone(_ number: String) -> String {
let cleanNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let format: [Character] = ["X", "X", "X", "-", "X", "X", "X", "-", "X", "X", "X", "X"]
var result = ""
var index = cleanNumber.startIndex
for ch in format {
if index == cleanNumber.endIndex {
break
}
if ch == "X" {
result.append(cleanNumber[index])
index = cleanNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
Swift 5.1 Update on Дарія Прокопович great solution
extension String {
func applyPatternOnNumbers(pattern: String, replacmentCharacter: Character) -> String {
var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
for index in 0 ..< pattern.count {
guard index < pureNumber.count else { return pureNumber }
let stringIndex = String.Index(utf16Offset: index, in: self)
let patternCharacter = pattern[stringIndex]
guard patternCharacter != replacmentCharacter else { continue }
pureNumber.insert(patternCharacter, at: stringIndex)
}
return pureNumber
}
}
Usage:
let formattedText = text.applyPatternOnNumbers(pattern: "+# (###) ###-####", replacmentCharacter: "#")
You can use this library https://github.com/luximetr/AnyFormatKit
Example
let phoneFormatter = DefaultTextFormatter(textPattern: "### (###) ###-##-##")
phoneFormatter.format("+123456789012") // +12 (345) 678-90-12
Very simple to use.
Swift 3 but should also be translatable to Swift 4
ErrorHandling
enum PhoneNumberFormattingError: Error {
case wrongCharactersInPhoneNumber
case phoneNumberLongerThanPatternAllowes
}
Create Patterns
enum PhoneNumberFormattingPatterns: String {
case mobile = "+xx (yxx) xxxxxxxxxxx"
case home = "+xx (yxxx) xxxx-xxx"
}
Insert Function
/**
Formats a phone-number to correct format
- Parameter pattern: The pattern to format the phone-number.
- Example:
- x: Says that this should be a digit.
- y: Says that this digit cannot be a "0".
- The length of the pattern restricts also the length of allowed phone-number digits.
- phone-number: "+4306641234567"
- pattern: "+xx (yxx) xxxxxxxxxxx"
- result: "+43 (664) 1234567"
- Throws:
- PhoneNumberFormattingError
- wrongCharactersInPhoneNumber: if phone-number contains other characters than digits.
- phoneNumberLongerThanPatternAllowes: if phone-number is longer than pattern allows.
- Returns:
- The formatted phone-number due to the pattern.
*/
extension String {
func vpToFormattedPhoneNumber(withPattern pattern: PhoneNumberFormattingPatterns) throws -> String {
let phoneNumber = self.replacingOccurrences(of: "+", with: "")
var retVal: String = ""
var index = 0
for char in pattern.rawValue.lowercased().characters {
guard index < phoneNumber.characters.count else {
return retVal
}
if char == "x" {
let charIndex = phoneNumber.index(phoneNumber.startIndex, offsetBy: index)
let phoneChar = phoneNumber[charIndex]
guard "0"..."9" ~= phoneChar else {
throw PhoneNumberFormattingError.wrongCharactersInPhoneNumber
}
retVal.append(phoneChar)
index += 1
} else if char == "y" {
var charIndex = phoneNumber.index(phoneNumber.startIndex, offsetBy: index)
var indexTemp = 1
while phoneNumber[charIndex] == "0" {
charIndex = phoneNumber.index(phoneNumber.startIndex, offsetBy: index + indexTemp)
indexTemp += 1
}
let phoneChar = phoneNumber[charIndex]
guard "0"..."9" ~= phoneChar else {
throw PhoneNumberFormattingError.wrongCharactersInPhoneNumber
}
retVal.append(phoneChar)
index += indexTemp
} else {
retVal.append(char)
}
}
if phoneNumber.endIndex > phoneNumber.index(phoneNumber.startIndex, offsetBy: index) {
throw PhoneNumberFormattingError.phoneNumberLongerThanPatternAllowes
}
return retVal
}
}
Usage
let phoneNumber = "+4306641234567"
let phoneNumber2 = "4343211234567"
do {
print(try phoneNumber.vpToFormattedPhoneNumber(withPattern: .mobile))
print(try phoneNumber2.vpToFormattedPhoneNumber(withPattern: .home))
} catch let error as PhoneNumberFormattingError {
switch error {
case .wrongCharactersInPhoneNumber:
print("wrong characters in phone number")
case .phoneNumberLongerThanPatternAllowes:
print("too long phone number")
default:
print("unknown error")
}
} catch {
print("something other went wrong")
}
// output: +43 (664) 1234567
// output: +43 (4321) 1234-567
There are a number of good answers here but I took a completely different approach and thought I'd share in case it helps.
To start I broke up the formatting steps and components into their own separate responsibilities.
Phone number format can generally be broken down into local, domestic or international format types that vary by string length.
I defined the types:
/// Defines the three different types of formatting phone numbers use
///
/// - local: Numbers used locally.
/// - domestic: Numbers used locally including area codes.
/// - international: Numbers used internationally with country codes.
public enum PhoneFormatType {
case local
case domestic
case international
}
Then defined the separators available to format a phone number string:
// Defines separators that are available for use in formatting
// phone number strings.
public enum PhoneFormatSeparator {
case hyphen
case plus
case space
case parenthesisLH
case parenthesisRH
case slash
case backslash
case pipe
case asterisk
public var value: String {
switch self {
case .hyphen: return "-"
case .plus: return "+"
case .space: return " "
case .parenthesisLH: return "("
case .parenthesisRH: return ")"
case .slash: return "/"
case .backslash: return "\\"
case .pipe: return "|"
case .asterisk: return "*"
}
}
}
Next I defined formatting rules that specify the index (in a phone number string) where the separators like +,-,etc are inserted.
// defines the separators that should be inserted in a phone number string
// and the indexes where they should be applied
public protocol PhoneNumberFormatRule {
// the index in a phone number where this separator should be applied
var index: Int { get set }
// the priority in which this rule should be applied. Sorted in inverse, 0 is highest priority, higher numbers are lower priority
var priority: Int { get set }
// the separator to use at this index
var separator: PhoneFormatSeparator { get set }
}
/// Default implementation of PhoneNumberFormatRule
open class PNFormatRule: PhoneNumberFormatRule {
public var index: Int
public var priority: Int
public var separator: PhoneFormatSeparator
public init(_ index: Int, separator: PhoneFormatSeparator, priority: Int = 0) {
self.index = index
self.separator = separator
self.priority = priority
}
}
With these defined, I created rulesets that associate rules with a given format type.
/// Defines the rule sets associated with a given phone number type.
/// e.g. international/domestic/local
public protocol PhoneFormatRuleset {
/// The type of phone number formatting to which these rules apply
var type: PhoneFormatType { get set }
/// A collection of rules to apply for this phone number type.
var rules: [PhoneNumberFormatRule] { get set }
/// The maximum length a number using this format ruleset should be. (Inclusive)
var maxLength: Int { get set }
}
With everything defined this way, you can setup rulesets quickly to suit whatever format you need.
Here's an example of a ruleset that defines 3 rules for a hyphen formatted phone number string typically used in the US:
// Formats phone numbers:
// .local: 123-4567
// .domestic: 123-456-7890
// .international: +1 234-567-8901
static func usHyphen() -> [PhoneFormatRuleset] {
return [
PNFormatRuleset(.local, rules: [
PNFormatRule(3, separator: .hyphen)
], maxLength: 7),
PNFormatRuleset(.domestic, rules: [
PNFormatRule(3, separator: .hyphen),
PNFormatRule(6, separator: .hyphen)
], maxLength: 10),
PNFormatRuleset(.international, rules: [
PNFormatRule(0, separator: .plus),
PNFormatRule(1, separator: .space),
PNFormatRule(4, separator: .hyphen),
PNFormatRule(7, separator: .hyphen)
], maxLength: 11)
]
}
The (not so) heavy lifting of the formatting logic happens here:
// formats a string using the format rule provided at initialization
public func format(number: String) -> String {
// strip non numeric characters
let n = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
// bail if we have an empty string, or if no ruleset is defined to handle formatting
guard n.count > 0, let type = type(for: n.count), let ruleset = ruleset(for: type) else {
return n
}
// this is the string we'll return
var formatted = ""
// enumerate the numeric string
for (i,character) in n.enumerated() {
// bail if user entered more numbers than allowed for our formatting ruleset
guard i <= ruleset.maxLength else {
break
}
// if there is a separator defined to be inserted at this index then add it to the formatted string
if let separator = ruleset.separator(for: i) {
formatted+=separator
}
// now append the character
formatted+="\(character)"
}
return formatted
}
I've created a framework with a sample project you can look through here: https://github.com/appteur/phoneformat
Here is how it works as you type:
I also set it up so you can just import it with cocoapods.
pod 'SwiftPhoneFormat', '1.0.0'
Then use it:
import SwiftPhoneFormat
var formatter = PhoneFormatter(rulesets: PNFormatRuleset.usParethesis())
let formatted = formatter.format(number: numberString)
This is the extension which will full fill your requirement:
extension String {
func convertToInternationalFormat() -> String {
let isMoreThanTenDigit = self.count > 10
_ = self.startIndex
var newstr = ""
if isMoreThanTenDigit {
newstr = "\(self.dropFirst(self.count - 10))"
}
else if self.count == 10{
newstr = "\(self)"
}
else {
return "number has only \(self.count) digits"
}
if newstr.count == 10 {
let internationalString = "(\(newstr.dropLast(7))) \(newstr.dropLast(4).dropFirst(3)) \(newstr.dropFirst(6).dropLast(2)) \(newstr.dropFirst(8))"
newstr = internationalString
}
return newstr
}
}
INPUT :
var str1 = "9253248954"
var str2 = "+19253248954"
var str3 = "19253248954"
OUTPUT :
str1.convertToInternationalFormat() // "(925) 324 89 54"
str2.convertToInternationalFormat() // "(925) 324 89 54"
str3.convertToInternationalFormat() // "(925) 324 89 54"
If you rather to do it without using a library.
Here is a link to the best example or you can use the code below.
https://ivrodriguez.com/format-phone-numbers-in-swift/
A simple code snippet to format 10 digit phone numbers in Swift 5.0, instead of including a big library, just implement a delegate function and a formatting function:
The UITextFieldDelegate function
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var fullString = textField.text ?? ""
fullString.append(string)
if range.length == 1 {
textField.text = format(phoneNumber: fullString, shouldRemoveLastDigit: true)
} else {
textField.text = format(phoneNumber: fullString)
}
return false
}
The formatting function:
func format(phoneNumber: String, shouldRemoveLastDigit: Bool = false) -> String {
guard !phoneNumber.isEmpty else { return "" }
guard let regex = try? NSRegularExpression(pattern: "[\\s-\\(\\)]", options: .caseInsensitive) else { return "" }
let r = NSString(string: phoneNumber).range(of: phoneNumber)
var number = regex.stringByReplacingMatches(in: phoneNumber, options: .init(rawValue: 0), range: r, withTemplate: "")
if number.count > 10 {
let tenthDigitIndex = number.index(number.startIndex, offsetBy: 10)
number = String(number[number.startIndex..<tenthDigitIndex])
}
if shouldRemoveLastDigit {
let end = number.index(number.startIndex, offsetBy: number.count-1)
number = String(number[number.startIndex..<end])
}
if number.count < 7 {
let end = number.index(number.startIndex, offsetBy: number.count)
let range = number.startIndex..<end
number = number.replacingOccurrences(of: "(\\d{3})(\\d+)", with: "($1) $2", options: .regularExpression, range: range)
} else {
let end = number.index(number.startIndex, offsetBy: number.count)
let range = number.startIndex..<end
number = number.replacingOccurrences(of: "(\\d{3})(\\d{3})(\\d+)", with: "($1) $2-$3", options: .regularExpression, range: range)
}
return number
}
SwiftUI code for mobile number formatting textfield
struct MobileNumberTextFieldContainer: UIViewRepresentable {
private var placeholder : String
private var text : Binding<String>
init(_ placeholder:String, text:Binding<String>) {
self.placeholder = placeholder
self.text = text
}
func makeCoordinator() -> MobileNumberTextFieldContainer.Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<MobileNumberTextFieldContainer>) -> UITextField {
let innertTextField = UITextField(frame: .zero)
innertTextField.placeholder = placeholder
innertTextField.text = text.wrappedValue
innertTextField.delegate = context.coordinator
context.coordinator.setup(innertTextField)
return innertTextField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<MobileNumberTextFieldContainer>) {
uiView.text = self.text.wrappedValue
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: MobileNumberTextFieldContainer
init(_ textFieldContainer: MobileNumberTextFieldContainer) {
self.parent = textFieldContainer
}
func setup(_ textField:UITextField) {
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
#objc func textFieldDidChange(_ textField: UITextField) {
var isCursorLast = false
var cursorPosition = 0
let textLenght = textField.text?.count ?? 0
if let selectedRange = textField.selectedTextRange {
cursorPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
print("\(cursorPosition) lengh = \(textLenght)")
if cursorPosition < textLenght {
isCursorLast = true
}
}
textField.text = textField.text?.applyPatternOnNumbers(pattern: "+# (###) ###-####", replacementCharacter: "#")
//textField.text = textField.text ?? "".format(phoneNumber: textField.text ?? "")
self.parent.text.wrappedValue = textField.text ?? ""
if isCursorLast {
isCursorLast = false
let arbitraryValue: Int = cursorPosition
if let newPosition = textField.position(from: textField.beginningOfDocument, offset: arbitraryValue) {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}
}
}
}
I have use the same formatting function which is #Mark Wilson used
And simply we can add this in our view
MobileNumberTextFieldContainer("Phone Number", text: $phoneNumber)
SwiftUI
My answer tweaks and builds on Mobile Dan's answer and adapts it for a SwiftUI TextField. If formatting fails or it's less than 10 numbers, it will return the unformatted string. This works with the phone number suggestion feature, assuming a one digit country code. Should be easy to adapt for multi-digit country codes.
TextField("Phone", text: $phoneNumber)
.keyboardType(.numberPad)
.textContentType(.telephoneNumber)
.onChange(of: phoneNumber) { _ in
phoneNumber = phoneNumber.formatPhoneNumber()
}
String Extensions:
extension String {
func formatPhoneNumber() -> String {
// Remove any character that is not a number
let numbersOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let length = numbersOnly.count
// Check for supported phone number length
if length > 11 {
return String(numbersOnly.prefix(11)).formatPhoneNumber()
} else if length < 10 {
return numbersOnly
}
var sourceIndex = 0
// Leading Number
var leadingNumber = ""
if length == 11, let leadChar = numbersOnly.first {
leadingNumber = String(leadChar) + " "
sourceIndex += 1
}
// Area code
var areaCode = ""
let areaCodeLength = 3
guard let areaCodeSubstring = numbersOnly.substring(start: sourceIndex, offsetBy: areaCodeLength) else {
return numbersOnly
}
areaCode = String(format: "(%#) ", areaCodeSubstring)
sourceIndex += areaCodeLength
// Prefix, 3 characters
let prefixLength = 3
guard let prefix = numbersOnly.substring(start: sourceIndex, offsetBy: prefixLength) else {
return numbersOnly
}
sourceIndex += prefixLength
// Suffix, 4 characters
let suffixLength = 4
guard let suffix = numbersOnly.substring(start: sourceIndex, offsetBy: suffixLength) else {
return numbersOnly
}
return leadingNumber + areaCode + prefix + "-" + suffix
}
}
extension String {
func substring(start: Int, offsetBy: Int) -> String? {
guard let substringStartIndex = self.index(startIndex, offsetBy: start, limitedBy: endIndex) else {
return nil
}
guard let substringEndIndex = self.index(startIndex, offsetBy: start + offsetBy, limitedBy: endIndex) else {
return nil
}
return String(self[substringStartIndex ..< substringEndIndex])
}
}
Swift 5
String(
format: "(%#) %#-%#",
rawNumber.subString(from: 0, to: 2),
rawNumber.subString(from: 3, to: 5),
rawNumber.subString(from: 6, to: 9)
)
Related
Autoformat UITextfield in phone number format XXX-XXX-XXXX in iOS
I'm trying to autoformat my textfield in the format XXX-XXX-XXXX. The rules are that it should be in the format as mentioned and the first number should be greater than zero and should be of max 10 digits, the regex for this is already added in my function. Below are the methods I'm using #IBAction func validateAction(_ sender: Any) { guard let phoneNumber = phoneNumber.text else {return } if validatePhoneNumber(phoneNumber: phoneNumber) { errorMessage.text = "Validation successful" } else { errorMessage.text = "Validation failed" } } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let currentText = textField.text as NSString? else {return true} let textString = currentText.replacingCharacters(in: range, with: string) if textField == phoneNumber { return textField.updatePhoneNumber(string, textString) }else{ return true } } func validatePhoneNumber(phoneNumber: String) -> Bool { let phoneRegex: String = "^[2-9]\\d{2}-\\d{3}-\\d{4}$" return NSPredicate(format: "SELF MATCHES %#", phoneRegex).evaluate(with: phoneNumber) } extension UITextField { func updatePhoneNumber(_ replacementString: String?, _ textString: String?) -> Bool { guard let textCount = textString?.count else {return true} guard let currentString = self.text else {return true} if replacementString == "" { return true } else if textCount == 4 { self.text = currentString + "-" } else if textCount == 8 { self.text = currentString + "-" } else if textCount > 12 || replacementString == " " { return false } return true } } This works to some extent, now the issue is, user can manually intervene and disrupt the format for eg: if I entered, 234-567-8990, user can place the cursor just before 5 and backspace and type in at the end or between like 567-89900000 or 234567-8990. By validating the regular expression it will give an error but I want to re-adjust the format as user types in. For eg: in the earlier scenario if the user is on cursor before 5 and backspaces it should not remove the dash (-) but just removes 4 and re-adjust format like 235-678-990. Is there any simple way to do this? Any help is appreciated
I use this extension for String. It's small and real helpful. extension String { func applyPatternOnNumbers(pattern: String, replacmentCharacter: Character) -> String { var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression) for index in 0 ..< pattern.count { guard index < pureNumber.count else { return pureNumber } let stringIndex = String.Index(encodedOffset: index) let patternCharacter = pattern[stringIndex] guard patternCharacter != replacmentCharacter else { continue } pureNumber.insert(patternCharacter, at: stringIndex) } return pureNumber } just set a needed mask text.applyPatternOnNumbers(pattern: "+# (###) ###-##-##", replacmentCharacter: "#") and that's all
#SonuP very good question. I believe you want to format the phone and also keep the cursor in correct position. If so, then this task is slightly more complex than just formatting. You need to reformat the code and update the cursor position. Note that my solution follows the specific formatting and if it does not match yours, then tweak the code slightly: Swift 5 public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { var text = textField.text ?? "" text.replaceSubrange(range.toRange(string: text), with: string) if let phone = (textField.text ?? "").replacePhoneSubrange(range, with: string) { // update text in the field textField.text = text // update cursor position if text.count == range.location + string.count || text.hasSuffix(")") && text.count == range.location + string.count + 1 { // end if phone.hasSuffix(")") { textField.setCursor(phone.count - 1) } else { textField.setCursor(phone.count) } } else { textField.setCursor(min(range.location + string.count, phone.count-1)) } } return false } Also you will need the following extensions: // MARK: - Helpful methods extension NSRange { /// Convert to Range for given string /// /// - Parameter string: the string /// - Returns: range func toRange(string: String) -> Range<String.Index> { let range = string.index(string.startIndex, offsetBy: self.lowerBound)..<string.index(string.startIndex, offsetBy: self.upperBound) return range } static func fromRange(_ range: Range<String.Index>, inString string: String) -> NSRange { let s = string.distance(from: string.startIndex, to: range.lowerBound) let e = string.distance(from: string.startIndex, to: range.upperBound) return NSMakeRange(s, e-s) } } // MARK: - Helpful methods extension String { /// Raplace string in phone public func replacePhoneSubrange(_ range: NSRange, with string: String) -> String? { if let phone = self.phone { // +11-111-111-1111 (111) var numberString = phone.phoneNumber // 111111111111111 let newRange = self.toPhoneRange(range: range) numberString.replaceSubrange(newRange.toRange(string: phone), with: string) return numberString.phone } return nil } /// Phone number string public var phoneNumber: String { let components = self.components( separatedBy: CharacterSet.decimalDigits.inverted) var decimalString = NSString(string: components.joined(separator: "")) while decimalString.hasPrefix("0") { decimalString = decimalString.substring(from: 1) as NSString } return String(decimalString) } /// Get phone range public func toPhoneRange(range: NSRange) -> NSRange { let start = range.location let end = start + range.length let s2 = self.convertPhoneLocation(location: start) let e2 = self.convertPhoneLocation(location: end) return NSRange(location: s2, length: e2-s2) } /// Get cursor location for phone public func convertPhoneLocation(location: Int) -> Int { let substring = self[self.startIndex..<self.index(self.startIndex, offsetBy: location)] return String(substring).phoneNumber.count } } // MARK: - Helpful methods extension UITextField { /// Set cursor /// /// - Parameter position: the position to set func setCursor(_ position: Int) { if let startPosition = self.position(from: self.beginningOfDocument, offset: position) { let endPosition = startPosition self.selectedTextRange = self.textRange(from: startPosition, to: endPosition) } } } // MARK: - Helpful methods extension String { /// phone formatting public var phone: String? { let components = self.components( separatedBy: CharacterSet.decimalDigits.inverted) var decimalString = NSString(string: components.joined(separator: "")) while decimalString.hasPrefix("0") { decimalString = decimalString.substring(from: 1) as NSString } let length = decimalString.length let hasLeadingOne = length > 0 && length == 11 let hasLeadingTwo = length > 11 if length > 15 { return nil } var index = 0 as Int let formattedString = NSMutableString() if hasLeadingOne || hasLeadingTwo { let len = hasLeadingTwo ? 2 : 1 let areaCode = decimalString.substring(with: NSMakeRange(index, len)) formattedString.appendFormat("+%#-", areaCode) index += len } if (length - index) > 3 { let areaCode = decimalString.substring(with: NSMakeRange(index, 3)) formattedString.appendFormat("%#-", areaCode) index += 3 } if length - index == 4 && length == 7 { // xxx-xxxx let prefix = decimalString.substring(with: NSMakeRange(index, 4)) formattedString.append(prefix) index += 4 } else if length - index > 3 {// xxx-xxx-x... let prefix = decimalString.substring(with: NSMakeRange(index, 3)) formattedString.appendFormat("%#-", prefix) index += 3 } if length - index == 4 { // xxx-xxx-xxxx let prefix = decimalString.substring(with: NSMakeRange(index, 4)) formattedString.append(prefix) index += 4 } // format phone extenstion if length - index > 4 { let prefix = decimalString.substring(with: NSMakeRange(index, 4)) formattedString.appendFormat("%# ", prefix) index += 4 } let remainder = decimalString.substring(from: index) if length > 12 { formattedString.append("(\(remainder))") } else { formattedString.append(remainder) } return (formattedString as String).trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) } }
Use this in textfield delegate method : func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if range.length > 0 { return true } if string == "" { return false } if range.location > 11 { return false } var originalText = textField.text let replacementText = string.replacingOccurrences(of: " ", with: "") if !CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: replacementText)) { return false } if range.location == 3 || range.location == 7 { originalText?.append("-") textField.text = originalText } return true }
iOS: String. Get line and column number from absolute position and vice versa
Say I have a text which contains mixed CR, LF and CRLF newline separators. Like this: "\n \n Lorem \r Ipsum \n is \r\n simply \n dummy \r\n text of \n the printing \r and typesetting industry. \n \n". I'm loading this text into simple text editor (NSTextView/UITextView). Visually newline separators looks the same; just a new line. I can navigate through text in simple text editor, select text, cut, copy, paste, ... Question: How can I get line and column number from absolute character location (i.e selection NSRange)? And also, how can I get absolute character location from known line and column number? Thanks! UPDATE 1: line and column number - simple means cursor location. line and column number - has One-based numbering. absolute character location - has Zero-based numbering. Sample code of current solution. It is calculates line and column number from absolute character location and vice versa. But it is not recalculates mappings on text change. struct TextString { struct Cursor { let line: Int let column: Int } struct Mapping { let lineNumber: Int let lineLength: Int let absolutePosition: Int fileprivate var absoluteStart: Int { return absolutePosition - lineLength } } let string: String private (set) var mappings: [Mapping] = [] init(string: String) { self.string = string mappings = setupMappings() } } extension TextString { func cursor(from position: Int) -> Cursor? { guard position > 0 else { return nil } guard let mapping = mappings.first(where: { $0.absolutePosition >= position && $0.absoluteStart <= position }) else { return nil } let result = Cursor(line: mapping.lineNumber, column: position - mapping.absoluteStart) return result } func position(from cursor: Cursor) -> Int? { guard let line = mappings.element(at: cursor.line - 1) else { return nil } guard line.lineLength >= cursor.column else { return nil } let result = line.absoluteStart + cursor.column return result } } extension TextString { private func setupMappings() -> [Mapping] { var mappings: [Mapping] = [] var line = 1 var previousAbsolutePosition = 0 var delta = 0 let scanner = Scanner(string: string) scanner.charactersToBeSkipped = nil while !scanner.isAtEnd { if scanner.scanUpToCharacters(from: .newlines) != nil { let charactersLocation = scanner.scanLocation - delta if let newLines = scanner.scanCharacters(from: .newlines) { for index in 0..<newLines.count { let absolutePosition = charactersLocation + 1 + index // `+1` is newLine itself mappings.append(Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition, absolutePosition: absolutePosition)) previousAbsolutePosition = absolutePosition line += 1 } delta = scanner.scanLocation - previousAbsolutePosition } else { // Only happens when we at last line withot newline. let absolutePosition = charactersLocation mappings.append(Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition, absolutePosition: absolutePosition)) line += 1 previousAbsolutePosition = charactersLocation } } else if let newLines = scanner.scanCharacters(from: .newlines) { // Text begins with new lines. for index in 0..<newLines.count { let absolutePosition = 1 + index // `+1` is newLine itself mappings.append(Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition, absolutePosition: absolutePosition)) previousAbsolutePosition = absolutePosition line += 1 } delta = scanner.scanLocation - previousAbsolutePosition } } assert(previousAbsolutePosition == string.count) return mappings } } UPDATE 2: RegEx version. private func setupMappingsUsingRegex() throws -> [Mapping] { if string.isEmpty { return [] } var mappings: [Mapping] = [] let regex = try NSRegularExpression(pattern: "(\\r\\n)|(\\n)|(\\r)") let matches = regex.matches(in: string, range: NSRange(location: 0, length: string.unicodeScalars.count)) var line = 1 var previousAbsolutePosition = 0 var delta = 0 // String without any newline. if matches.isEmpty { let mapping = Mapping(lineNumber: 1, lineLength: string.count, absolutePosition: string.count) mappings.append(mapping) return mappings } for match in matches { let absolutePosition = match.range.location - delta + 1 let mapping = Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition, absolutePosition: absolutePosition) mappings.append(mapping) delta += match.range.length - 1 previousAbsolutePosition = absolutePosition line += 1 } // Rest of the string without newline at the end. if previousAbsolutePosition < string.count { let mapping = Mapping(lineNumber: line, lineLength: string.count - previousAbsolutePosition, absolutePosition: string.count) mappings.append(mapping) previousAbsolutePosition = string.count } assert(previousAbsolutePosition == string.count) return mappings } Performance: 22400 characters (200 lines) analysed 1000 times. RegEx: 5.120s Scanner: 6.603s
I suggest you to separate your string by using regex. Say you want to split a substring if you see \n, \r and \r\n, the regex will be something like var content: String = <Your text here> let regex = try! NSRegularExpression(pattern: "(\\n)|(\\r)|(\\r\\n)") let matchs = regex.matches(in: content, range: NSRange(location: 0, length: content.count)).map{(content as NSString).substring(with: $0.range)} Then you can loop within the matched results and get index & range, etc
This worked for me to get the line number: let content: String = <YourString> let selectionRange: NSRange = <YourTextRange> let regex = try! NSRegularExpression(pattern: "\n", options: []) let lineNumber = regex.numberOfMatches(in: content, options: [], range: NSMakeRange(0, nsRange.location)) + 1 You can customize the regex to match any kind of newline character you want. To get the column number you can get the line range and then subtract the start of it from the selection range: let lineRange = content.lineRange(for: selectionRange.location) let column = selectionRange.location - lineRange.location
Swift NSAttributedString Trim
I want to get ride of the white spaces in front and at the end of my NSAttributedString(Trimming it). I can't simply convert it to string and do trimming because there are images(attachments) in it. How can i do it?
Create extension of NSAttributedString as below. extension NSAttributedString { public func attributedStringByTrimmingCharacterSet(charSet: CharacterSet) -> NSAttributedString { let modifiedString = NSMutableAttributedString(attributedString: self) modifiedString.trimCharactersInSet(charSet: charSet) return NSAttributedString(attributedString: modifiedString) } } extension NSMutableAttributedString { public func trimCharactersInSet(charSet: CharacterSet) { var range = (string as NSString).rangeOfCharacter(from: charSet as CharacterSet) // Trim leading characters from character set. while range.length != 0 && range.location == 0 { replaceCharacters(in: range, with: "") range = (string as NSString).rangeOfCharacter(from: charSet) } // Trim trailing characters from character set. range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) while range.length != 0 && NSMaxRange(range) == length { replaceCharacters(in: range, with: "") range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) } } } and use in viewController where you want to use. like this let attstring = NSAttributedString(string: "this is test message. Please wait. ") let result = attstring.attributedStringByTrimmingCharacterSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
This works even with emoji in the text extension NSAttributedString { /** Will Trim space and new line from start and end of the text */ public func trimWhiteSpace() -> NSAttributedString { let invertedSet = CharacterSet.whitespacesAndNewlines.inverted let startRange = string.utf16.description.rangeOfCharacter(from: invertedSet) let endRange = string.utf16.description.rangeOfCharacter(from: invertedSet, options: .backwards) guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else { return NSAttributedString(string: string) } let location = string.utf16.distance(from: string.startIndex, to: startLocation) - 1 let length = string.utf16.distance(from: startLocation, to: endLocation) + 2 let range = NSRange(location: location, length: length) return attributedSubstring(from: range) } } USAGE let attributeString = NSAttributedString(string: "\n\n\n Hi 👋 👩👩👧👩👩👦👦👩👩👧👧👨👨👦👩👦👨👨👧👧👨👨👦👦👨👨👧👦👩👧👦👩👦👦👩👧👧👨👦 buddy. ") let result = attributeString.trimWhiteSpace().string // Hi 👋 👩👩👧👩👩👦👦👩👩👧👧👨👨👦👩👦👨👨👧👧👨👨👦👦👨👨👧👦👩👧👦👩👦👦👩👧👧👨👦 buddy.
Swift 4 and above extension NSMutableAttributedString { func trimmedAttributedString() -> NSAttributedString { let invertedSet = CharacterSet.whitespacesAndNewlines.inverted let startRange = string.rangeOfCharacter(from: invertedSet) let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards) guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else { return NSAttributedString(string: string) } let location = string.distance(from: string.startIndex, to: startLocation) - 1 let length = string.distance(from: startLocation, to: endLocation) + 2 let range = NSRange(location: location, length: length) return attributedSubstring(from: range) } } use: let string = "This is string with some space in the end. " let attributedText = NSMutableAttributedString(string: string).trimmedAttributedString()
It turns out that Unicode strings are hard hahaha! The other solutions posted here are a great starting point, but they crashed for me when using non-latin strings. Whenever using indexes or ranges in Swift Strings, we need to use String.Index instead of plain Int. Creating an NSRange from a Range<String.Index> has to be done with NSRange(swiftRange, in: String). That being said, this code builds on the other answers, but makes it unicode-proof: public extension NSMutableAttributedString { /// Trims new lines and whitespaces off the beginning and the end of attributed strings func trimmedAttributedString() -> NSAttributedString { let invertedSet = CharacterSet.whitespacesAndNewlines.inverted let startRange = string.rangeOfCharacter(from: invertedSet) let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards) guard let startLocation = startRange?.lowerBound, let endLocation = endRange?.lowerBound else { return NSAttributedString(string: string) } let trimmedRange = startLocation...endLocation return attributedSubstring(from: NSRange(trimmedRange, in: string)) } }
I made a swift 3 implementation, just in case anyone is interested: /** Trim an attributed string. Can for example be used to remove all leading and trailing spaces and line breaks. */ public func attributedStringByTrimmingCharactersInSet(set: CharacterSet) -> NSAttributedString { let invertedSet = set.inverted let rangeFromStart = string.rangeOfCharacter(from: invertedSet) let rangeFromEnd = string.rangeOfCharacter(from: invertedSet, options: .backwards) if let startLocation = rangeFromStart?.upperBound, let endLocation = rangeFromEnd?.lowerBound { let location = string.distance(from: string.startIndex, to: startLocation) - 1 let length = string.distance(from: startLocation, to: endLocation) + 2 let newRange = NSRange(location: location, length: length) return self.attributedSubstring(from: newRange) } else { return NSAttributedString() } }
Swift 3.2 Version: extension NSAttributedString { public func trimmingCharacters(in characterSet: CharacterSet) -> NSAttributedString { let modifiedString = NSMutableAttributedString(attributedString: self) modifiedString.trimCharacters(in: characterSet) return NSAttributedString(attributedString: modifiedString) } } extension NSMutableAttributedString { public func trimCharacters(in characterSet: CharacterSet) { var range = (string as NSString).rangeOfCharacter(from: characterSet) // Trim leading characters from character set. while range.length != 0 && range.location == 0 { replaceCharacters(in: range, with: "") range = (string as NSString).rangeOfCharacter(from: characterSet) } // Trim trailing characters from character set. range = (string as NSString).rangeOfCharacter(from: characterSet, options: .backwards) while range.length != 0 && NSMaxRange(range) == length { replaceCharacters(in: range, with: "") range = (string as NSString).rangeOfCharacter(from: characterSet, options: .backwards) } } }
Following code will work for your requirement. var attString: NSAttributedString = NSAttributedString(string: " this is att string") let trimmedString = attString.string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
How to Format Textfiled.text in Turkish Phone Format?
I've a textfield which only takes phone number and I'm trying to format it into Turkish format number which looks like this (555) 555 5555. How can I make that in Swift?
You can do something like this on EiditingChange method, this will change first three entries to this () format while user inputs text, you can follow same approach to remove format if user is deleting entries #IBAction func onEditingChanged(sender: UITextField!) { var string = sender.text as NSString if string.length > 3 { let range = string.rangeOfString("(") if range.location == NSNotFound { var firstPart = string.substringToIndex(3) var secondPart = string.substringFromIndex(3) var string = "(\(firstPart))\(secondPart)" sender.text = string } } }
Well if we take the assumption that all Turkish numbers are 10 digits long, we can do it as follows. First lets define a helper function to get the substrings: func sub(str: String, start: Int, end: Int) -> String { return str.substringWithRange(Range<String.Index>(start: advance(str.startIndex, start), end: advance(str.startIndex, end))) } Now we just apply the function to get the sections of the number: // Lets say this is the number we get from the textfield let number = "1234567890" let start = sub(number, 0, 3) // "123" let mid = sub(number, 3, 6) // "456" let end = sub(number, 6, 10) // "7890" And then we format this into a single string as desired. let formatNumber = "(\(start)) \(mid) \(end)" // "(123) 456 7890" Note that this would only work for numbers that have 10 digits (I doubt that all Turkish numbers are). You would need to modify this to format for numbers of different lengths, by specifying different substrings of the start mid and end above. If you wanted to limit the user to only using 10 digit numbers, you should perform validation on textfield.
var shouldAttemptFormat: Bool = true func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { if textField == self.phoneNumberTextField{ let resultString: String = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString:string) let oldString: String = self.phoneNumberTextField.text let oldCount = count(oldString) let newCount = count(resultString) shouldAttemptFormat = newCount > oldCount return true//newCount < 15 }else{ return true } // otherwise we should just let them continue } // MARK: - phone number formatting func formatPhoneNumber() { // this value is determined when textField shouldChangeCharactersInRange is called on a phone // number cell - if a user is deleting characters we don't want to try to format it, otherwise // using the current logic below certain deletions will have no effect if !shouldAttemptFormat { return } // here we are leveraging some of the objective-c NSString functions to help parse and modify // the phone number... first we strip anything that's not a number from the textfield, and then // depending on the current value we append formatting characters to make it pretty let currentValue: NSString = self.phoneNumberTextField.text let strippedValue: NSString = currentValue.stringByReplacingOccurrencesOfString("[^0-9]", withString: "", options: .RegularExpressionSearch, range: NSMakeRange(0, currentValue.length)) var formattedString: NSString = "" if strippedValue.length == 0 { formattedString = ""; } else if strippedValue.length < 3 { formattedString = "(" + (strippedValue as String) } else if strippedValue.length == 3 { formattedString = "(" + (strippedValue as String) + ") " } else if strippedValue.length < 6 { formattedString = "(" + strippedValue.substringToIndex(3) + ") " + strippedValue.substringFromIndex(3) } else if strippedValue.length == 6 { formattedString = "(" + strippedValue.substringToIndex(3) + ") " + strippedValue.substringFromIndex(3) + "-" } else if strippedValue.length <= 10 { formattedString = "(" + strippedValue.substringToIndex(3) + ") " + strippedValue.substringWithRange(NSMakeRange(3, 3)) + "-" + strippedValue.substringFromIndex(6) } else if strippedValue.length >= 11 { formattedString = "(" + strippedValue.substringToIndex(3) + ") " + strippedValue.substringWithRange(NSMakeRange(3, 3)) + "-" + strippedValue.substringWithRange(NSMakeRange(6, 4)) } self.phoneNumberTextField.text = formattedString as String } I use this code as it looks above, It works. When user type any character, formatPhoneNumber function works, for each char. self.phoneNumberTextField.addTarget(self, action: "formatPhoneNumber", forControlEvents: .EditingChanged) You must to add this line in viewDidLoad. Hopefully, it will work for you
How do I use subscript and superscript in Swift?
I want my UILabel to display text in following manner 6.022*1023. What functions does Swift have for subscript and superscript?
Most of the answers+examples are in ObjC, but this is how to do it in Swift. let font:UIFont? = UIFont(name: "Helvetica", size:20) let fontSuper:UIFont? = UIFont(name: "Helvetica", size:10) let attString:NSMutableAttributedString = NSMutableAttributedString(string: "6.022*1023", attributes: [.font:font!]) attString.setAttributes([.font:fontSuper!,.baselineOffset:10], range: NSRange(location:8,length:2)) labelVarName.attributedText = attString This gives me: In a more detailed explanation: Get UIFont you want for both the default and superscript style, superscript must be smaller. Create a NSMutableAttributedString with the full string and default font. Add an attribute to the characters you want to change (NSRange), with the smaller/subscript UIFont, and the NSBaselineOffsetAttributeName value is the amount you want to offset it vertically. Assign it to your UILabel Hopefully this helps other Swift devs as I needed this as well.
As a different approach, I wrote a function that takes in a string where the exponents are prepended with ^ such as 2^2•3•5^2 and returns 2²•3•5² func exponentize(str: String) -> String { let supers = [ "1": "\u{00B9}", "2": "\u{00B2}", "3": "\u{00B3}", "4": "\u{2074}", "5": "\u{2075}", "6": "\u{2076}", "7": "\u{2077}", "8": "\u{2078}", "9": "\u{2079}"] var newStr = "" var isExp = false for (_, char) in str.characters.enumerate() { if char == "^" { isExp = true } else { if isExp { let key = String(char) if supers.keys.contains(key) { newStr.append(Character(supers[key]!)) } else { isExp = false newStr.append(char) } } else { newStr.append(char) } } } return newStr } It's a bit of a brute force method, but it works if you don't want to deal with attributed strings or you want your string to be independent of a font.
If you can get along with text that doesn't look perfect, and only need a subset of characters you can make use of the unicode superscript and subscript numbers: ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ This has the advantage of being a lot less cumbersome.
I wrote the following extension or you can use it as a function, it is working well for me . you can modify it by skipping the parts that are not essential to you extension NSMutableAttributedString { enum scripting : Int { case aSub = -1 case aSuper = 1 } func characterSubscriptAndSuperscript(string:String, characters:[Character], type:scripting, fontSize:CGFloat, scriptFontSize:CGFloat, offSet:Int, length:[Int], alignment:NSTextAlignment)-> NSMutableAttributedString { let paraghraphStyle = NSMutableParagraphStyle() // Set The Paragraph aligmnet , you can ignore this part and delet off the function paraghraphStyle.alignment = alignment var scriptedCharaterLocation = Int() //Define the fonts you want to use and sizes let stringFont = UIFont.boldSystemFont(ofSize: fontSize) let scriptFont = UIFont.boldSystemFont(ofSize: scriptFontSize) // Define Attributes of the text body , this part can be removed of the function let attString = NSMutableAttributedString(string:string, attributes: [NSFontAttributeName:stringFont,NSForegroundColorAttributeName:UIColor.black,NSParagraphStyleAttributeName: paraghraphStyle]) // the enum is used here declaring the required offset let baseLineOffset = offSet * type.rawValue // enumerated the main text characters using a for loop for (i,c) in string.characters.enumerated() { // enumerated the array of first characters to subscript for (theLength,aCharacter) in characters.enumerated() { if c == aCharacter { // Get to location of the first character scriptedCharaterLocation = i //Now set attributes starting from the character above attString.setAttributes([NSFontAttributeName:scriptFont, // baseline off set from . the enum i.e. +/- 1 NSBaselineOffsetAttributeName:baseLineOffset, NSForegroundColorAttributeName:UIColor.black], // the range from above location range:NSRange(location:scriptedCharaterLocation, // you define the length in the length array // if subscripting at different location // you need to define the length for each one length:length[theLength])) } } } return attString} } examples: let attStr1 = NSMutableAttributedString().characterSubscriptAndSuperscript( string: "23 x 456", characters:["3","5"], type: .aSuper, fontSize: 20, scriptFontSize: 15, offSet: 10, length: [1,2], alignment: .left) let attStr2 = NSMutableAttributedString().characterSubscriptAndSuperscript( string: "H2SO4", characters: ["2","4"], type: .aSub, fontSize: 20, scriptFontSize: 15, offSet: 8, length: [1,1], alignment: .left)
My solution as an extension of String extension String { func setAsSuperscript(_ textToSuperscript: String) -> NSMutableAttributedString { let attributedString = NSMutableAttributedString(string: self) let foundRange = attributedString.mutableString.range(of: textToSuperscript) let font = UIFont.systemFont(ofSize: 12) if foundRange.location != NSNotFound { attributedString.addAttribute(.font, value: font, range: foundRange) attributedString.addAttribute(.baselineOffset, value: 3, range: foundRange) attributedString.addAttribute(.foregroundColor, value: UIColor.red, range: foundRange) } return attributedString } And usage: let placeholder = "Required value*".setAsSuperscript("*") myLabel.attributedText = placeholder
For a simple to use Swift solution, you might want to checkout HandyUIKit. After importing it into your project (e.g. via Carthage – see instructions in README) you can do something like this: import HandyUIKit "6.022*10^{23}".superscripted(font: UIFont.systemFont(ofSize: 20, weight: .medium)) This line will return an NSAttributedString which will look exactly like what you're looking for. Just assign it to a UILabels attributedText property and that's it! If you're looking for subscripting a text, simply use subscripted(font:) instead. It will recognize structures like CO_{2}. There's also superAndSubscripted(font:) if you want to combine both. See the docs for more information and additional examples.
Here is a simple version that has correct error handling and will compile in playground. import UIKit func setMyLabelText(myLabel: UILabel) { if let largeFont = UIFont(name: "Helvetica", size: 20), let superScriptFont = UIFont(name: "Helvetica", size:10) { let numberString = NSMutableAttributedString(string: "6.022*10", attributes: [.font: largeFont]) numberString.append(NSAttributedString(string: "23", attributes: [.font: superScriptFont, .baselineOffset: 10])) myLabel.attributedText = numberString } } let myLabel = UILabel() setMyLabelText(myLabel: myLabel)
Swift 4+ Version of #Atka's Answer import UIKit extension NSMutableAttributedString { enum Scripting : Int { case aSub = -1 case aSuper = 1 } func scripts(string: String, characters: [Character], type: Scripting, stringFont: UIFont, fontSize: CGFloat, scriptFont: UIFont, scriptFontSize: CGFloat, offSet: Int, length: [Int], alignment: NSTextAlignment) -> NSMutableAttributedString { let paraghraphStyle = NSMutableParagraphStyle() paraghraphStyle.alignment = alignment var scriptedCharaterLocation = Int() let attributes = [ NSAttributedStringKey.font: stringFont, NSAttributedStringKey.foregroundColor: UIColor.black, NSAttributedStringKey.paragraphStyle: paraghraphStyle ] let attString = NSMutableAttributedString(string:string, attributes: attributes) let baseLineOffset = offSet * type.rawValue let scriptTextAttributes: [NSAttributedStringKey : Any] = [ NSAttributedStringKey.font: scriptFont, NSAttributedStringKey.baselineOffset: baseLineOffset, NSAttributedStringKey.foregroundColor: UIColor.blue ] for (i,c) in string.enumerated() { for (theLength, aCharacter) in characters.enumerated() { if c == aCharacter { scriptedCharaterLocation = i attString.setAttributes(scriptTextAttributes, range: NSRange(location:scriptedCharaterLocation, length: length[theLength])) } } } return attString } }
Here's a Swift 5.1 solution (should work with older versions of Swift too) using recursion, that only focuses outputting a superscript from an Int (i.e. no formatting for display). extension Int { func superscriptString() -> String { let minusPrefixOrEmpty: String = self < 0 ? Superscript.minus : "" let (quotient, remainder) = abs(self).quotientAndRemainder(dividingBy: 10) let quotientString = quotient > 0 ? quotient.superscriptString() : "" return minusPrefixOrEmpty + quotientString + Superscript.value(remainder) } } enum Superscript { static let minus = "⁻" private static let values: [String] = [ "⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹" ] static func value(_ int: Int) -> String { assert(int >= 0 && int <= 9) return values[int] } } Here are some tests to prove correctness: func testPositiveIntegersSuperscript() { XCTAssertEqual(0.superscriptString(), "⁰") XCTAssertEqual(1.superscriptString(), "¹") XCTAssertEqual(2.superscriptString(), "²") XCTAssertEqual(3.superscriptString(), "³") XCTAssertEqual(4.superscriptString(), "⁴") XCTAssertEqual(5.superscriptString(), "⁵") XCTAssertEqual(6.superscriptString(), "⁶") XCTAssertEqual(7.superscriptString(), "⁷") XCTAssertEqual(8.superscriptString(), "⁸") XCTAssertEqual(9.superscriptString(), "⁹") XCTAssertEqual(10.superscriptString(), "¹⁰") XCTAssertEqual(11.superscriptString(), "¹¹") XCTAssertEqual(12.superscriptString(), "¹²") XCTAssertEqual(19.superscriptString(), "¹⁹") XCTAssertEqual(20.superscriptString(), "²⁰") XCTAssertEqual(21.superscriptString(), "²¹") XCTAssertEqual(99.superscriptString(), "⁹⁹") XCTAssertEqual(100.superscriptString(), "¹⁰⁰") XCTAssertEqual(101.superscriptString(), "¹⁰¹") XCTAssertEqual(102.superscriptString(), "¹⁰²") XCTAssertEqual(237.superscriptString(), "²³⁷") XCTAssertEqual(999.superscriptString(), "⁹⁹⁹") XCTAssertEqual(1000.superscriptString(), "¹⁰⁰⁰") XCTAssertEqual(1001.superscriptString(), "¹⁰⁰¹") XCTAssertEqual(1234.superscriptString(), "¹²³⁴") XCTAssertEqual(1337.superscriptString(), "¹³³⁷") } func testNegativeIntegersSuperscript() { XCTAssertEqual(Int(-1).superscriptString(), "⁻¹") XCTAssertEqual(Int(-2).superscriptString(), "⁻²") XCTAssertEqual(Int(-3).superscriptString(), "⁻³") XCTAssertEqual(Int(-4).superscriptString(), "⁻⁴") XCTAssertEqual(Int(-5).superscriptString(), "⁻⁵") XCTAssertEqual(Int(-6).superscriptString(), "⁻⁶") XCTAssertEqual(Int(-7).superscriptString(), "⁻⁷") XCTAssertEqual(Int(-8).superscriptString(), "⁻⁸") XCTAssertEqual(Int(-9).superscriptString(), "⁻⁹") XCTAssertEqual(Int(-10).superscriptString(), "⁻¹⁰") XCTAssertEqual(Int(-11).superscriptString(), "⁻¹¹") XCTAssertEqual(Int(-12).superscriptString(), "⁻¹²") XCTAssertEqual(Int(-19).superscriptString(), "⁻¹⁹") XCTAssertEqual(Int(-20).superscriptString(), "⁻²⁰") XCTAssertEqual(Int(-21).superscriptString(), "⁻²¹") XCTAssertEqual(Int(-99).superscriptString(), "⁻⁹⁹") XCTAssertEqual(Int(-100).superscriptString(), "⁻¹⁰⁰") XCTAssertEqual(Int(-101).superscriptString(), "⁻¹⁰¹") XCTAssertEqual(Int(-102).superscriptString(), "⁻¹⁰²") XCTAssertEqual(Int(-237).superscriptString(), "⁻²³⁷") XCTAssertEqual(Int(-999).superscriptString(), "⁻⁹⁹⁹") XCTAssertEqual(Int(-1000).superscriptString(), "⁻¹⁰⁰⁰") XCTAssertEqual(Int(-1001).superscriptString(), "⁻¹⁰⁰¹") XCTAssertEqual(Int(-1234).superscriptString(), "⁻¹²³⁴") XCTAssertEqual(Int(-1337).superscriptString(), "⁻¹³³⁷") } My solution is more than twice as fast as gorillaz' solution(which is string and array based), thanks to mine being math and recursion based. Here is proof: private typealias SuperscriptVector = (value: Int, expectedSuperstring: String) private let vector1to9: SuperscriptVector = (123456789, "¹²³⁴⁵⁶⁷⁸⁹") func performanceTest(times n: Int, function: (Int) -> () -> String) { func manyTimes(_ times: Int) { func doTest(vector: SuperscriptVector) { let result: String = function(vector.value)() XCTAssertEqual(result, vector.expectedSuperstring) } for _ in 0..<times { doTest(vector: vector1to9) } } manyTimes(n) } // 3.244 sec func testPerformanceMine() { measure { performanceTest(times: 1_000_000, function: Int.superscriptString) } } // 7.6 sec func testPerformanceStackOverflow() { measure { performanceTest(times: 1_000_000, function: Int.superscriptStringArrayBased) } }
For those using SwiftUI, an option is to use a unicode exception string in Text(): Text("c\u{2082}=a\u{2082}+b\u{2082}") /// c^2 = a^2 + b^2 One benefit of this method is easier inline subs/supers. If it must absolutely inherit from UILabel (e.g. for native NSAttributedString or native wrapping), you can leverage UIViewRepresentable and use the unicode exception string (which should work in most cases). Here is an option on SO: Stackoverflow. I have not tried the answer. And for those looking for unicode for common subscripts and superscripts (e.g. for arithmetic): Superscripts: 0 = 2070 1 = 00B9 2 = 00B2 3 = 00B3 4 = 2074 5 = 2075 6 = 2076 7 = 2077 8 = 2078 9 = 2079 + = 207A - = 207B ( = 207D ) = 207E n = 207F Subscripts: 0 = 2080 1 = 2081 2 = 2082 3 = 2083 4 = 2084 5 = 2085 6 = 2086 7 = 2087 8 = 2088 9 = 2089 + = 208A - = 208B ( = 208D ) = 208E e = 2091 n = 2099 Reference: unicode.org
A nice simple function that outputs a number as the superscript text. func exponent(i: Int) -> String { let powers : [String] = [ "\u{2070}", "\u{00B9}", "\u{00B2}", "\u{00B3}", "\u{2074}", "\u{2075}", "\u{2076}", "\u{2077}", "\u{2078}", "\u{2079}" ] let digits = Array(String(i)) var string = "" for d in digits { string.append("\(powers[Int(String(d))!])") } return string }
In SwiftUI it is possible to achieve superscript effect by using baselineOffset modifier. For example: Text("$") .foregroundColor(Color.white) .font(.custom(AppTheme.getRegularFont(), size: 13)) .baselineOffset(8.0) Text("20") .foregroundColor(AppTheme.primaryColor) .font(.custom(AppTheme.getRegularFont(), size: 25)) Here is how it looks:
I have created a String extension which takes a string and converts all of its superscript into unicode characters. This way you could for example share the resulting string without any hassle. extension Character { var unicode: String { // See table here: https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts let unicodeChars = [Character("0"):"\u{2070}", Character("1"):"\u{00B9}", Character("2"):"\u{00B2}", Character("3"):"\u{00B3}", Character("4"):"\u{2074}", Character("5"):"\u{2075}", Character("6"):"\u{2076}", Character("7"):"\u{2077}", Character("8"):"\u{2078}", Character("9"):"\u{2079}", Character("i"):"\u{2071}", Character("+"):"\u{207A}", Character("-"):"\u{207B}", Character("="):"\u{207C}", Character("("):"\u{207D}", Character(")"):"\u{207E}", Character("n"):"\u{207F}"] if let unicode = unicodeChars[self] { return unicode } return String(self) } } extension String { var unicodeSuperscript: String { let char = Character(self) return char.unicode } func superscripted() -> String { let regex = try! NSRegularExpression(pattern: "\\^\\{([^\\}]*)\\}") var unprocessedString = self var resultString = String() while let match = regex.firstMatch(in: unprocessedString, options: .reportCompletion, range: NSRange(location: 0, length: unprocessedString.count)) { // add substring before match let substringRange = unprocessedString.index(unprocessedString.startIndex, offsetBy: match.range.location) let subString = unprocessedString.prefix(upTo: substringRange) resultString.append(String(subString)) // add match with subscripted style let capturedSubstring = NSAttributedString(string: unprocessedString).attributedSubstring(from: match.range(at: 1)).mutableCopy() as! NSMutableAttributedString capturedSubstring.string.forEach { (char) in let superScript = char.unicode let string = NSAttributedString(string: superScript) resultString.append(string.string) } // strip off the processed part unprocessedString.deleteCharactersInRange(range: NSRange(location: 0, length: match.range.location + match.range.length)) } // add substring after last match resultString.append(unprocessedString) return resultString } mutating func deleteCharactersInRange(range: NSRange) { let mutableSelf = NSMutableString(string: self) mutableSelf.deleteCharacters(in: range) self = mutableSelf as String } } For example "x^{4+n}+12^{3}".superscripted() produces "x⁴⁺ⁿ+12³" This was inspired by HandyUIKit and the gist to my code is on Github
Here is what I came up with for a SwiftUI Text view with subscripts and superscripts embedded in the String initialize. Surround a subscript with \\b[text]\\e and a superscript with \\a[text]\\e where [text] are the characters in the sub- or superscript. // // FormattedText.swift // // Created by Joseph Levy on 8/25/21. import Foundation import SwiftUI enum Attribute { case normal; case sub; case sup } struct AttributedString { var attribute: Attribute var string: String } func StringToAttributedStrings(_ string: String) -> [AttributedString] { //var lastAtt: Attribute = .normal var splits = string.components(separatedBy: "\\") var filter = false var attSplits: [AttributedString] = [] for i in splits.indices { var a: Attribute = { //() -> Attribute in let firstchar = splits[i].first switch firstchar { case "a": do { a = .sup; filter = true } case "b": do { a = .sub; filter = true } case "e": do { a = .normal; filter = true } default: do { a = .normal if i > 0 { splits[i] = "\\" + splits[i] } filter = false; } } return a }() attSplits.append(AttributedString(attribute: a, string: filter ? String(splits[i].dropFirst()) : splits[i] )) } return attSplits } func FormattedText(_ string: String, up: CGFloat = 8, down: CGFloat = 3) -> Text { let aStrings = StringToAttributedStrings(string) var returnText = Text("") var addedText: Text for aString in aStrings { switch aString.attribute { case .normal: addedText = Text(aString.string) case .sub: addedText = Text(aString.string).font(.footnote).baselineOffset(-down) case .sup: addedText = Text(aString.string).font(.footnote).baselineOffset(up) } returnText = returnText + addedText } return returnText } Use FormattedText("Al\\bx\\eGa\\b1-x\\eAs\\a*\\e") gives
I created an AmountFormatter class which helped me convert decimal numbers into numbers with raised decimals. class AmountFormatter { static func sharedFormatter( decimalNumber: NSDecimalNumber, currency: String, raisedDecimals: Bool) -> NSAttributedString { let numberFormatter = NumberFormatter() numberFormatter.usesGroupingSeparator = true numberFormatter.groupingSeparator = "." numberFormatter.decimalSeparator = "," numberFormatter.numberStyle = .decimal let scale: Int16 = 2 let behavior = NSDecimalNumberHandler( roundingMode: .plain, scale: scale, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: true) guard let amountString = numberFormatter.string( from: decimalNumber.rounding(accordingToBehavior: behavior)) else { fatalError("Can't convert conversion from 'NSDecimalNumber' to string") } let currencyAmountString = currency + amountString let font = UIFont(name: "Roboto", size: 20) let fontSuper = UIFont(name: "Roboto", size: 10) let attributedCurrencyAmountString = NSMutableAttributedString( string: currencyAmountString, attributes: [.font: font!]) if raisedDecimals == false { return attributedCurrencyAmountString as NSAttributedString } var array = attributedCurrencyAmountString.string.split(separator: ",") let lenght = array[0].count attributedCurrencyAmountString.setAttributes( [.font: fontSuper!, .baselineOffset: 10], range: NSRange(location: lenght, length: 3)) attributedCurrencyAmountString.setAttributes( [.font: fontSuper!], range: NSRange(location: 0, length: 1)) return attributedCurrencyAmountString as NSAttributedString } }
extension String { func convertToSuperscriptDigits(from start: Int, to end: Int? = nil) - String { let end = end ?? self.count let startIndex = self.index(self.startIndex, offsetBy: start) let endIndex = self.index(self.startIndex, offsetBy: end) let replaceRange = startIndex..<endIndex let substring = self[replaceRange] let supers = [ "0": "\u{2070}", "1": "\u{00B9}", "2": "\u{00B2}", "3": "\u{00B3}", "4": "\u{2074}", "5": "\u{2075}", "6": "\u{2076}", "7": "\u{2077}", "8": "\u{2078}", "9": "\u{2079}"] let convertString = substring.map { (char) -> Character in Character(supers[String(char)] ?? String(char)) } return self.replacingCharacters(in: replaceRange, with: String(convertString)) }
This will superscript all the numbers in a string and remove the ^ character. Use: yourstring.addSuper() code: extension String { func addSuper() -> String { let charset = CharacterSet(charactersIn: "1234567890") let toSuper: [Character: String] = ["0": "\u{2070}", "1": "\u{00B9}", "2": "\u{00B2}", "3": "\u{00B3}", "4": "\u{2074}", "5": "\u{2075}", "6": "\u{2076}", "7": "\u{2077}", "8": "\u{2078}", "9": "\u{2079}", "-": "\u{207B}"] var resultString: String = "" var index: Int = 0 for charater in self { if String(charater).rangeOfCharacter(from: charset) != nil { resultString.append(toSuper[charater] ?? "") } else if charater != "^" { resultString.append(charater) } index += 1 } return resultString } }
I wrote a fun little algorithm for this as an extension on Int that doesn't require any messy attributed strings. Usage: let superscriptString = 8675309.superscriptString Implementation: extension Int { var superscriptString: String { var input: Int = self var result: String = "" while input > 0 { let lastDigit = input % 10 input /= 10 guard let superscript = lastDigit.superscript else { continue } result = superscript + result } return result } private var superscript: String? { switch self { case 0: return "\u{2070}" case 1: return "\u{00B9}" case 2: return "\u{00B2}" case 3: return "\u{00B3}" case 4: return "\u{2074}" case 5: return "\u{2075}" case 6: return "\u{2076}" case 7: return "\u{2077}" case 8: return "\u{2078}" case 9: return "\u{2079}" default: return nil } } }
First an extension to get a substring extension String { subscript(idx: Int) -> String { String(self[index(startIndex, offsetBy: idx)]) } } Next get the actual superScript func superScript(_ num: Int) -> String { var s = "" let numStr = String(num) for n in numStr.utf8 { let i = Int(n) - 48 // utf8 for '0' s += "⁰¹²³⁴⁵⁶⁷⁸⁹"[i] } return s } and to test for i in 0...12 { print(superScript(i), terminator: " ") } print(superScript(12345), terminator: " ") yielding output ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ¹⁰ ¹¹ ¹² ¹²³⁴⁵
in CoreText there is a key for such style: https://developer.apple.com/documentation/coretext/kctsuperscriptattributename so NSAttributedString has undocumented key __C.NSAttributedStringKey(_rawValue: NSSuperScript) so few lines of code can do the job: extension NSMutableAttributedString { func applySuperscript(range: NSRange) { let superScriptKey = NSAttributedString.Key("NSSuperScript") addAttribute(superScriptKey, value: Int64(1) , range: range) } } to make a subscript - use Int64(-1) I didn't research when this key appeared, maybe it was exist since even iOS 3 sdk. Also, keep in mind there is no guarantee Apple won't modify this key in future.