Related
I have following string "#[Hema](hema_ramburuth), #[Ilesh P](ilesh.panchal), #[Lewis Murphy](lewis) how are you?". I want to display this screen like this "Hema, Ilesh P, Lewis Murphy how are you?" also I want to identify the screen for the click event.
I have used the ActiveLabel repo for the click.
Hey I have had encountered a similar requirement. So this is how I have handled.
I have created an extension for String
extension String {
/// Returns range of text in the string
func getRange(OfText text: String) -> NSRange {
let nsRepresentation = self as NSString
return nsRepresentation.range(of: text)
}
}
In your View Controller,
var tapPrivacyGesture = UITapGestureRecognizer()
#IBOutlet weak var yourLabel: UILabel!
var displayText = String()
func matchesForRegexInText(regex: String, text: String, firstBracket: String, lastBracket: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matches(
in: text,
options: [],
range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range) }.map { $0.replacingOccurrences(of: firstBracket, with: "") }.map { $0.replacingOccurrences(of: lastBracket, with: "") }
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
var givenString = "#[Hema](hema_ramburuth), #[Ilesh P](ilesh.panchal), #[Lewis Murphy](lewis) how are you?"
let nameStrings = matchesForRegexInText(regex: "\\[(.*?)\\]", text: givenString, firstBracket: "[", lastBracket: "]")
let removeForUIStrings = matchesForRegexInText(regex: "\\((.*?)\\)", text: givenString, firstBracket: "(", lastBracket: ")")
removeForUIStrings.forEach {
givenString = givenString.replacingOccurrences(of: "(\($0))", with: "")
}
nameStrings.forEach {
givenString = givenString.replacingOccurrences(of: "[\($0)]", with: $0)
}
givenString = givenString.replacingOccurrences(of: "#", with: "")
print(givenString)
displayText = givenString
tapPrivacyGesture.addTarget(self, action: #selector(self.handlePolicyTap(tap:)))
yourLabel.addGestureRecognizer(tapPrivacyGesture)
yourLabel.isUserInteractionEnabled = true
func handlePolicyTap(tap: UITapGestureRecognizer) {
let storage = NSTextStorage(attributedString: yourLabel.attributedText ?? NSAttributedString())
let layoutManager = NSLayoutManager()
storage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: CGSize(width: yourLabel.frame.size.width, height: yourLabel.frame.size.height+100))
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = (yourLabel.lineBreakMode)
textContainer.maximumNumberOfLines = yourLabel.numberOfLines
layoutManager.addTextContainer(textContainer)
let location: CGPoint = tap.location(in: yourLabel)
let characterIndex: Int = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard
characterIndex < storage.length,
let question = currentQuestion else {
return
}
nameStrings.forEach {
let range = displayText.getRange(OfText: $0)
if range.contains(characterIndex) {
/// Perform actions on click of this string
}
}
}
As from your question, just hard-code parsing done below.
let fullString = "#[Hema](hema_ramburuth), #[Ilesh P](ilesh.panchal), #[Lewis Murphy](lewis) how are you?"
let allarray = fullString.split(separator: ",")
let messageArray = allarray.last
let message = messageArray?.split(separator: ")")
let correctMessage = message?.last
var allNames : String = ""
for namesString in allarray {
if allNames.count > 0 {
allNames += ", "
}
let name = String(namesString)
allNames += name.slice(from: "#[", to: "]") ?? ""
}
if allNames.count > 0 {
allNames += correctMessage ?? ""
}
print("Name and Message --- > \(allNames)")
Slicing string using String extension
extension String {
func slice(from: String, to: String) -> String? {
return (range(of: from)?.upperBound).flatMap { substringFrom in
(range(of: to, range: substringFrom..<endIndex)?.lowerBound).map { substringTo in
substring(with: substringFrom..<substringTo)
}
}
}
}
I've printed output as below:
Name and Message --- > Hema, Ilesh P, Lewis Murphy how are you?
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())
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)
)
I am getting a string from html parse that is;
string = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
my code is something like
var startIndex = text.rangeOfString("'")
var endIndex = text.rangeOfString("',")
var range2 = startIndex2...endIndex
substr= string.substringWithRange(range)
i am not sure if my second splitting string should be "'" or "',"
i want my outcome as
substr = "Info/99/something"
extension String {
func slice(from: String, to: String) -> String? {
return (range(of: from)?.upperBound).flatMap { substringFrom in
(range(of: to, range: substringFrom..<endIndex)?.lowerBound).map { substringTo in
String(self[substringFrom..<substringTo])
}
}
}
}
"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
.sliceFrom("'", to: "',")
I'd use a regular expression to extract substrings from complex input like this.
Swift 3.1:
let test = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
if let match = test.range(of: "(?<=')[^']+", options: .regularExpression) {
print(test.substring(with: match))
}
// Prints: Info/99/something
Swift 2.0:
let test = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
if let match = test.rangeOfString("(?<=')[^']+", options: .RegularExpressionSearch) {
print(test.substringWithRange(match))
}
// Prints: Info/99/something
I rewrote one of the top Swift answers to understand what it was doing with map. I prefer a version using guard, IMO.
extension String {
func slice(from: String, to: String) -> String? {
guard let rangeFrom = range(of: from)?.upperBound else { return nil }
guard let rangeTo = self[rangeFrom...].range(of: to)?.lowerBound else { return nil }
return String(self[rangeFrom..<rangeTo])
}
}
behavior:
let test1 = "a[b]c".slice(from: "[", to: "]") // "b"
let test2 = "abc".slice(from: "[", to: "]") // nil
let test3 = "a]b[c".slice(from: "[", to: "]") // nil
let test4 = "[a[b]c]".slice(from: "[", to: "]") // "a[b"
To find all substrings that are between a starting string and an ending string:
extension String {
func sliceMultipleTimes(from: String, to: String) -> [String] {
components(separatedBy: from).dropFirst().compactMap { sub in
(sub.range(of: to)?.lowerBound).flatMap { endRange in
String(sub[sub.startIndex ..< endRange])
}
}
}
}
let str = "start A end ... start B end"
str.sliceMultipleTimes(from: "start", to: "end") // ["A", "B"]
This works if it is always the second split:
let subString = split(string, isSeparator: "'")[1]
You can use var arr = str.componentsSeparatedByString(",") as your second split which will return you array
Swift 4.2:
extension String {
//right is the first encountered string after left
func between(_ left: String, _ right: String) -> String? {
guard let leftRange = range(of: left), let rightRange = range(of: right, options: .backwards)
,leftRange.upperBound <= rightRange.lowerBound else { return nil }
let sub = self[leftRange.upperBound...]
let closestToLeftRange = sub.range(of: right)!
return String(sub[..<closestToLeftRange.lowerBound])
}
}
Consider using a regular expression to match everything between single quotes.
let string = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
let pattern = "'(.+?)'"
let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)
let results = regex!.matchesInString(string, options: nil, range: NSMakeRange(0, count(string))) as! [NSTextCheckingResult]
let nsstring = string as NSString
let matches = results.map { result in return nsstring.substringWithRange(result.range)}
// First match
println(matches[0])
Swift 5
extension String {
///Returns an empty string when there is no path.
func substring(from left: String, to right: String) -> String {
if let match = range(of: "(?<=\(left))[^\(right)]+", options: .regularExpression) {
return String(self[match])
}
return ""
}
}
In Swift 4 or later you can create an extension method on StringProtocol to support substrings as well. You can just return a Substring instead of a new String:
edit/update: Swift 5 or later
extension StringProtocol {
func substring<S: StringProtocol>(from start: S, options: String.CompareOptions = []) -> SubSequence? {
guard let lower = range(of: start, options: options)?.upperBound
else { return nil }
return self[lower...]
}
func substring<S: StringProtocol>(through end: S, options: String.CompareOptions = []) -> SubSequence? {
guard let upper = range(of: end, options: options)?.upperBound
else { return nil }
return self[..<upper]
}
func substring<S: StringProtocol>(upTo end: S, options: String.CompareOptions = []) -> SubSequence? {
guard let upper = range(of: end, options: options)?.lowerBound
else { return nil }
return self[..<upper]
}
func substring<S: StringProtocol, T: StringProtocol>(from start: S, upTo end: T, options: String.CompareOptions = []) -> SubSequence? {
guard let lower = range(of: start, options: options)?.upperBound,
let upper = self[lower...].range(of: end, options: options)?.lowerBound
else { return nil }
return self[lower..<upper]
}
func substring<S: StringProtocol, T: StringProtocol>(from start: S, through end: T, options: String.CompareOptions = []) -> SubSequence? {
guard let lower = range(of: start, options: options)?.upperBound,
let upper = self[lower...].range(of: end, options: options)?.upperBound
else { return nil }
return self[lower..<upper]
}
}
Usage:
let string = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
let substr = string.substring(from: "'") // "Info/99/something', 'City Hall',1, 99);"
let through = string.substring(through: "Info") // "javascript:getInfo"
let upTo = string.substring(upTo: "Info") // "javascript:get"
let fromUpTo = string.substring(from: "'", upTo: "',") // "Info/99/something"
let fromThrough = string.substring(from: "'", through: "',") // "Info/99/something',"
let fromUpToCaseInsensitive = string.substring(from: "'info/", upTo: "/something", options: .caseInsensitive) // "99"
If you want to support also from the start or end of the string
extension String {
func slice(from: String, to: String) -> String? {
return (from.isEmpty ? startIndex..<startIndex : range(of: from)).flatMap { fromRange in
(to.isEmpty ? endIndex..<endIndex : range(of: to, range: fromRange.upperBound..<endIndex)).map({ toRange in
String(self[fromRange.upperBound..<toRange.lowerBound])
})
}
}
}
"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
.slice(from: "'", to: "',") // "Info/99/something"
"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
.slice(from: "", to: ":") // "javascript"
"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
.slice(from: ":", to: "") // "getInfo(1,'Info/99/something', 'City Hall',1, 99);"
"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
.slice(from: "", to: "") // "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
if you want another syntax, maybe more readable
extension String {
func slice(from: String, to: String) -> String? {
guard let fromRange = from.isEmpty ? startIndex..<startIndex : range(of: from) else { return nil }
guard let toRange = to.isEmpty ? endIndex..<endIndex : range(of: to, range: fromRange.upperBound..<endIndex) else { return nil }
return String(self[fromRange.upperBound..<toRange.lowerBound])
}
}
Swift 4 version of #litso. To find all values in text
func find(inText text: String, pattern: String) -> [String]? {
do {
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let result = regex.matches(in: text, options: .init(rawValue: 0), range: NSRange(location: 0, length: text.count))
let matches = result.map { result in
return (text as NSString).substring(with: result.range)
}
return matches
} catch {
print(error)
}
return nil
}
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.