How do I use subscript and superscript in Swift? - ios
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.
Related
Get string from substring
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?
How to detect link in UILabel in swift 4?
I was using ActiveLabel as third party library to make link in a label for particular words.The code works fine for Swift 3 & 3.2. but does not work for swift 4. Below code i used let customType1 = ActiveType.custom(pattern: "\\sTerms & Conditions\\b") //Looks for "are" labelTc.enabledTypes.append(customType1) labelTc.customize { (label) in labelTc.text = "UserAgreement".localized label.numberOfLines = 0 label.lineSpacing = 4 label.textColor = UIColor(red: 131 / 255, green: 147 / 255, blue: 168 / 255, alpha: 1) //Custom types label.customColor[customType1] = Constant.AppColor.greenMeadow label.customSelectedColor[customType1] = Constant.AppColor.greenMeadow label.configureLinkAttribute = { (type, attributes, isSelected) in var atts = attributes switch type { case customType1: atts[NSAttributedStringKey.font._rawValue as String] = UIFont(name: self.labelTc.font.fontName, size: 15.0) atts[NSAttributedStringKey.underlineStyle.rawValue] = NSUnderlineStyle.styleSingle break case .mention: break case .hashtag: break case .url: break case .custom(let pattern): break default : break } return atts } Can anyone give me solution using native code instead of using third party library.
I was able to find out the solution for swift 4 as well. label.configureLinkAttribute = { (type, attributes, isSelected) in var atts = attributes switch type { case customType1: atts[NSAttributedStringKey.font.rawValue] = UIFont(name: self.labelTc.font.fontName, size: 15.0) atts[NSAttributedStringKey.underlineStyle.rawValue] = NSUnderlineStyle.styleSingle.rawValue break default: () } return atts }
Another solution. For me this better. Get your text from the UILabel. Unwrape this text Do what you want For example, in my case: struct DetectedLinkData { var link: URL var range: Range<String.Index> init(link: URL, range: Range<String.Index>) { self.link = link self.range = range } } class LinkDetecter { static func getLinks(in string: String) -> [DetectedLinkData] { guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { return [] } var result: [DetectedLinkData] = [] let matches = detector.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count) ) for match in matches { guard let range = Range(match.range, in: string), let url = URL(string: String(string[range]) ) else { continue } result.append(DetectedLinkData(link: url, range: range)) } return result } } let temp = LinkDetecter.getLinks(in: message["text"] as? String ?? "")
I'm implementing hashtag recognizer in UITextView but it never highlights two words with the same begining
I found this question: How to make UITextView detect hashtags? and I copied the accepted answer into my code. I also set up a delegate to my textview and then put this code: func textViewDidChange(textView: UITextView) { textView.resolveHashTags() } By doing that I want to check the user's input on the textview and each time user types a #hashtag - I will automatically highlight it. But there is a weird problem: it never highlights words that starts with the same letters. It looks like this: what might be the problem here?
func resolveHashTags(text : String) -> NSAttributedString{ var length : Int = 0 let text:String = text let words:[String] = text.separate(withChar: " ") let hashtagWords = words.flatMap({$0.separate(withChar: "#")}) let attrs = [NSFontAttributeName : UIFont.systemFont(ofSize: 17.0)] let attrString = NSMutableAttributedString(string: text, attributes:attrs) for word in hashtagWords { if word.hasPrefix("#") { let matchRange:NSRange = NSMakeRange(length, word.characters.count) let stringifiedWord:String = word attrString.addAttribute(NSLinkAttributeName, value: "hash:\(stringifiedWord)", range: matchRange) } length += word.characters.count } return attrString } To separate words I used a string Extension extension String { public func separate(withChar char : String) -> [String]{ var word : String = "" var words : [String] = [String]() for chararacter in self.characters { if String(chararacter) == char && word != "" { words.append(word) word = char }else { word += String(chararacter) } } words.append(word) return words } } I hope this is what you are looking for. Tell me if it worked out for you. Edit : func textViewDidChange(_ textView: UITextView) { textView.attributedText = resolveHashTags(text: textView.text) textView.linkTextAttributes = [NSForegroundColorAttributeName : UIColor.red] } Edit 2: Updated for swift 3.
Little late to the party private func getHashTags(from caption: String) -> [String] { var words: [String] = [] let texts = caption.components(separatedBy: " ") for text in texts.filter({ $0.hasPrefix("#") }) { if text.count > 1 { let subString = String(text.suffix(text.count - 1)) words.append(subString) } } return words }
Here is Swift 5 solution as String extension: extension String { func withHashTags(color: UIColor) -> NSMutableAttributedString { let words = self.components(separatedBy: " ") let attributedString = NSMutableAttributedString(string: self) for word in words { if word.hasPrefix("#") { let range = (self as NSString).range(of: word) attributedString.addAttribute(.foregroundColor, value: color, range: range) } } return attributedString } } Pass param color to set specific color for hashtags
Formatting Phone number in Swift
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) )
Removing characters from a string in Swift
I have a function: func IphoneName() -> String { let device = UIDevice.currentDevice().name return device } Which returns the name of the iPhone (simple). I need to remove the "'s Iphone" from the end. I have been reading about changing it to NSString and use ranges, but I am a bit lost!
What about this: extension String { func removeCharsFromEnd(count:Int) -> String{ let stringLength = countElements(self) let substringIndex = (stringLength < count) ? 0 : stringLength - count return self.substringToIndex(advance(self.startIndex, substringIndex)) } func length() -> Int { return countElements(self) } } Test: var deviceName:String = "Mike's Iphone" let newName = deviceName.removeCharsFromEnd("'s Iphone".length()) // Mike But if you want replace method use stringByReplacingOccurrencesOfString as #Kirsteins posted: let newName2 = deviceName.stringByReplacingOccurrencesOfString( "'s Iphone", withString: "", options: .allZeros, // or just nil range: nil)
You don't have to work with ranges in this case. You can use: var device = UIDevice.currentDevice().name device = device.stringByReplacingOccurrencesOfString("s Iphone", withString: "", options: .allZeros, range: nil)
In Swift3: var device = UIDevice.currentDevice().name device = device.replacingOccurrencesOfString("s Iphone", withString: "")
Swift 4 Code //Add String extension extension String { func removeCharsFromEnd(count:Int) -> String{ let stringLength = self.count let substringIndex = (stringLength < count) ? 0 : stringLength - count let index: String.Index = self.index(self.startIndex, offsetBy: substringIndex) return String(self[..<index]) } func length() -> Int { return self.count } } //Use string function like let deviceName:String = "Mike's Iphone" let newName = deviceName.removeCharsFromEnd(count: "'s Iphone".length()) print(newName)// Mike