string
It is one of the most basic data types in all programming languages. Whether used for console applications, network services, GUI-based applications, or game development, it is an important part of programming. The Swift language provides String
classes that can handle most common text operations—most, but not all.
I’ve been coding in Swift since 2014, and I can hardly remember a project that didn’t use String
extensions. Here I’ll share the extensions I use the most. Some of it I wrote myself, and some I borrowed from different open source sources. All examples will work in Swift 5, but most of them will be compatible with older versions as well.
If you are still an Android developer, you may be interested in my article 10 Practical Kotlin String Extensions .
1. MD5 hash value calculation
The first useful extension provides computing the MD5 hash of a string. It’s useful when you’re using a web service or checking if a file is correct. The extension does not use any external dependencies, but uses the native CommonCrypto framework, so it needs to link with a bridge header file.
#import <CommonCrypto/CommonCrypto.h>
The extension itself is simple. This assumes that you want to return the MD5 as a string (for example, passed as a parameter to the server). If you need to get data type data, you can skip one step and return the hash variable directly.
import Foundation
extension String {
var md5: String? {
let length = Int(CC_MD5_DIGEST_LENGTH)
guard let data = self.data(using: String.Encoding.utf8) else { return nil }
let hash = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in
var hash: [UInt8] = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
return hash
}
return (0..<length).map { String(format: "%02x", hash[$0]) }.joined()
}
}
If you are adding it to an external frame, add the keyword var
before the variable public
.
how to use
let password: String = "your password"
guard let passwordMD5 = password.md5 else {
showError("Can't calculate MD5 of your password")
return
}
Since in this example you’re nil
extremely unlikely to get , this usage also makes sense:
let passwordMD5 = password.md5!
2. Localized strings
If you are developing an application that supports multiple languages, you may need to embed some text strings in the code that will appear on the screen in the user’s language. Editing storyboards or XIB files doesn’t require writing any code – but dynamic strings do.
The native way is to use NSLocalizedString
functions. For example:
NSLocalizedString("string_id", comment: "")
It always works fine and usually doesn’t cause any problems, but it looks a little ugly. This extension is a syntactic sugar:
import Foundation
extension String {
var localized: String {
NSLocalizedString(self, comment: "")
}
}
how to use
"string_id".localized
3. Integer subscript
Typical operations on a string in any programming language involve obtaining a portion of it—or, as programmers say, taking a slice. It can be the first few characters, the last few characters, or some part in the middle of the string.
One of the things that surprises me the most in Swift is how difficult it is to get substrings (and array fragments – but that’s another topic). You can’t write "Hello, world"[0...4]
something like this directly, like in many languages . But it would be comfortable, right?
So what do we need?
let str = "Hello, world"
print(str[...4]) // "Hello"
print(str[..<5]) // "Hello"
print(str[7...]) // "world"
print(str[3...4] + str[2]) // "lol"
This code looks simple and easy to understand. But if you try to compile it, you’ll see this error:
'subscript(_:)' is unavailable: cannot subscript String with an integer range, use a String.Index range instead.
Traditionally, there are solutions for this, and our solution is to write an String
extension of the class.
import Foundation
extension String {
subscript (i: Int) -> Character {
return self[index(startIndex, offsetBy: i)]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
if end < start { return "" }
return self[start..<end]
}
subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
if end < start { return "" }
return self[start...end]
}
subscript (bounds: CountablePartialRangeFrom<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(endIndex, offsetBy: -1)
if end < start { return "" }
return self[start...end]
}
subscript (bounds: PartialRangeThrough<Int>) -> Substring {
let end = index(startIndex, offsetBy: bounds.upperBound)
if end < startIndex { return "" }
return self[startIndex...end]
}
subscript (bounds: PartialRangeUpTo<Int>) -> Substring {
let end = index(startIndex, offsetBy: bounds.upperBound)
if end < startIndex { return "" }
return self[startIndex..<end]
}
}
It works almost exactly like the example above, except that it requires some type conversion. Of course, substring returns Substring
an instance of the type, not String
the type. And when you index a character, it returns an Character
instance of type. Both types can be transcoded String
.
let str = "Hello, world"
let strHello = String(str[...4])
let strWorld = String(str[7...])
4. Check the content
Many times I need to check String
if a string contains numbers. Or just numbers. For example, usernames usually only contain numbers and letters, but passwords should contain many more character types. The name should contain only letters.
There are several helper functions (wrapped as extensions) that are useful for validating input data before submitting it to the server.
import Foundation
extension String {
var containsOnlyDigits: Bool {
let notDigits = NSCharacterSet.decimalDigits.inverted
return rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
}
var containsOnlyLetters: Bool {
let notLetters = NSCharacterSet.letters.inverted
return rangeOfCharacter(from: notLetters, options: String.CompareOptions.literal, range: nil) == nil
}
var isAlphanumeric: Bool {
let notAlphanumeric = NSCharacterSet.decimalDigits.union(NSCharacterSet.letters).inverted
return rangeOfCharacter(from: notAlphanumeric, options: String.CompareOptions.literal, range: nil) == nil
}
}
This file contains three common extensions, but you can write similar extensions for other character sets as needed. The general rule is to get a character set (or a union of multiple character sets) that contains all allowed characters. Then reverse this character set and check if the string contains any of the symbols in it.
how to use
let a1 = "12345".containsOnlyDigits // true
let a2 = "a12345".containsOnlyDigits // false
let b1 = "abcde".containsOnlyLetters // true
let b2 = "abcde1".containsOnlyLetters // false
let c1 = "abcde12345".isAlphanumeric // true
let c2 = "abcde.12345".isAlphanumeric // false
5. Check if the string is a valid email address
When it comes to input data validation, we can’t avoid checking email addresses. Most apps ask users for their email address for various purposes such as verification, subscription, sending receipts, and more.
I found many solutions online. The idea behind them all is the same: we create a regular expression and evaluate a string containing the email address (or not containing it). The difference is the regular expression itself.
An email address always consists of two parts: left@right
, where on the left is your identifier (such as your name), and on the right is your complete domain name. Which domain names are allowed is a matter of choice. For example, it’s unclear whether user@localhost
such emails are allowed . For most use cases it’s not allowed, but if you write a console application for local use it can be a valid address.
This solution works for most cases.
import Foundation
extension String {
var isValidEmail: Bool {
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
return emailTest.evaluate(with: self)
}
}
Alternative version (suggested by Tromgy ):
extension String {
func matches(_ expression: String) -> Bool {
if let range = range(of: expression, options: .regularExpression, range: nil, locale: nil) {
return range.lowerBound == startIndex && range.upperBound == endIndex
} else {
return false
}
}
var isValidEmail: Bool {
matches("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}")
}
}
The advantage of this solution is that it simplifies the work of regular expressions. Both options are valid.
how to use
let approved = "[email protected]".isValidEmail // true
let rejected = "12345".isValidEmail // false
Phone numbers can also be verified in the same way, but require an external library – such as this one .
6. Save and retrieve local settings
Many times, we need to store some data – for example, a username or user ID – without creating a file. A typical way to save and load data is through UserDefaults
classes, formerly called NSUserDefaults
.
What you need to know about it:
- When the application is restarted,
UserDefaults
the data previously stored in is still there. - Uninstalling the app will clear
UserDefaults
the data in . - You can read data
UserDefaults
from it and from any thread. - After saving the data to
UserDefaults
, you should callsynchronize()
the method (only available in iOS 11 and earlier)
UserDefaults
Typical usage:
let value: String? = UserDefaults.standard.string(forKey: "key")
If there is a key
value associated with , you get it; if not, you get nil
.
Saving is more complicated:
UserDefaults.standard.set("value", forKey: "key")
UserDefaults.standard.synchronize() // iOS 11 and earlier
And delete:
UserDefaults.standard.removeObject(forKey: "key")
UserDefaults.standard.synchronize() // iOS 11 and earlier
Let’s use a Swift extension to simplify this process. These extensions have two goals.
- It should shorten the code and make it clearer.
- It should be returned if the value was not saved
null
.
import Foundation
extension Int {
init?(key: String) {
guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
self.init(UserDefaults.standard.integer(forKey: key))
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
extension Bool {
init?(key: String) {
guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
self.init(UserDefaults.standard.bool(forKey: key))
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
extension Float {
init?(key: String) {
guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
self.init(UserDefaults.standard.float(forKey: key))
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
extension Double {
init?(key: String) {
guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
self.init(UserDefaults.standard.double(forKey: key))
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
extension Data {
init?(key: String) {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
self.init(data)
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
extension String {
init?(key: String) {
guard let str = UserDefaults.standard.string(forKey: key) else { return nil }
self.init(str)
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
extension Array where Element == Any {
init?(key: String) {
guard let array = UserDefaults.standard.array(forKey: key) else { return nil }
self.init()
self.append(contentsOf: array)
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
extension Dictionary where Key == String, Value == Any {
mutating func merge(dict: [Key: Value]) {
for (k, v) in dict {
updateValue(v, forKey: k)
}
}
init?(key: String) {
guard let dict = UserDefaults.standard.dictionary(forKey: key) else { return nil }
self.init()
self.merge(dict: dict)
}
func store(key: String) {
UserDefaults.standard.set(self, forKey: key)
UserDefaults.standard.synchronize()
}
}
UserDefaults
There are several supported types, but any serializable type can be similarly extended.
how to use
let age = 25
age.store(key: "age")
print(Int(key: "age")) // Optional(25)
print(Float(key: "age")) // Optional(25.0)
print(String(key: "age")) // Optional("25")
print(String(key: "age1")) // nil
let dict: [String: Any] = [
"name": "John",
"surname": "Doe",
"occupation": "Swift developer",
"experienceYears": 5,
"age": 32
]
dict.store(key: "employee")
print(Dictionary(key: "employee"))
// Optional(["name": John, "occupation": Swift developer, "age": 32, "experienceYears": 5, "surname": Doe])
As you can see, it automatically converts the type if necessary.
I must point out that there is one drawback to these extensions. They save changes every time they perform store
an operation, which causes unnecessary writes to disk or flash memory. On the other hand, you never forget to synchronize updates. It’s up to you whether to use these extensions.
Note:
UserDefaults.standard.synchronize()
This operation is no longer required starting with iOS 12 (pointed out by David Such ).
7. Parse JSON data from String string
The JavaScript Object Notation (JSON) format has become so popular that not many Swift applications avoid it. Even if you don’t do it directly, some frameworks will do it underneath.
JSON source code is nothing but a string. So we can write an String
extension of class to parse it and turn it into a [String: Any]
dictionary of type .
Swift has a standard class for JSON parsing — JSONONSerialization
. But it uses Data
, instead String
. That’s why we’re going to create two extensions. Data
and String
.
Data+json.swift
:
import Foundation
extension Data {
init?(json: Any) {
guard let data = try? JSONSerialization.data(withJSONObject: json, options: .fragmentsAllowed) else { return nil }
self.init(data)
}
func jsonToDictionary() -> [String: Any]? {
(try? JSONSerialization.jsonObject(with: self, options: .allowFragments)) as? [String: Any]
}
func jsonToArray() -> [Any]? {
(try? JSONSerialization.jsonObject(with: self, options: .allowFragments)) as? [Any]
}
}
String+json.swift
:
import Foundation
extension String {
init?(json: Any) {
guard let data = Data(json: json) else { return nil }
self.init(decoding: data, as: UTF8.self)
}
func jsonToDictionary() -> [String: Any]? {
self.data(using: .utf8)?.jsonToDictionary()
}
func jsonToArray() -> [Any]? {
self.data(using: .utf8)?.jsonToArray()
}
}
how to use
let dict: [String: Any] = [
"name": "John",
"surname": "Doe",
"age": 31
]
print(dict)
// ["surname": "Doe", "name": "John", "age": 31]
let json = String(json: dict)
print(json)
// Optional("{\"surname\":\"Doe\",\"name\":\"John\",\"age\":31}")
let restoredDict = json?.jsonToDictionary()
print(restoredDict)
// Optional(["name": John, "surname": Doe, "age": 31])
8. Conversion
Swift transformations themselves are fairly simple, but sometimes it’s more appropriate to present them as extensions. For example, if you convert code from Kotlin (Android to iOS), you might see toIntOrNull()
functions like this. Let’s write them in Swift.
import Foundation
extension String {
func toInt() -> Int {
Int(self)!
}
func toIntOrNull() -> Int? {
Int(self)
}
}
The logic here is very simple. The extension is very short, but it can be useful if you need to convert a lot of Android code to iOS code. Likewise, you can add extensions such as toDouble()
.toString()
how to use
print("10".toInt())
// 10
print("15".toIntOrNull())
// Optional(15)
print("5.5".toInt())
// CRASH!
print("5a".toInt())
// CRASH!
9. Extract color from string
Swift doesn’t have a nice way to convert a hex string to a color, so obviously we can do that with an extension.
First, colors are represented differently in iOS and Mac. UIKit
provides UIColor
a class, while Cocoa
has an almost identical NSColor
class. To make the code generic, we will introduce an alias UniColor
.
#if os(iOS) || os(tvOS)
import UIKit
typealias UniColor = UIColor
#else
import Cocoa
typealias UniColor = NSColor
#endif
private extension Int {
func duplicate4bits() -> Int {
return (self << 4) + self
}
}
extension UniColor {
convenience init?(hexString: String) {
self.init(hexString: hexString, alpha: 1.0)
}
fileprivate convenience init?(hex3: Int, alpha: Float) {
self.init(red: CGFloat( ((hex3 & 0xF00) >> 8).duplicate4bits() ) / 255.0,
green: CGFloat( ((hex3 & 0x0F0) >> 4).duplicate4bits() ) / 255.0,
blue: CGFloat( ((hex3 & 0x00F) >> 0).duplicate4bits() ) / 255.0,
alpha: CGFloat(alpha))
}
fileprivate convenience init?(hex6: Int, alpha: Float) {
self.init(red: CGFloat( (hex6 & 0xFF0000) >> 16 ) / 255.0,
green: CGFloat( (hex6 & 0x00FF00) >> 8 ) / 255.0,
blue: CGFloat( (hex6 & 0x0000FF) >> 0 ) / 255.0, alpha: CGFloat(alpha))
}
convenience init?(hexString: String, alpha: Float) {
var hex = hexString
if hex.hasPrefix("#") {
hex = String(hex[hex.index(after: hex.startIndex)...])
}
guard let hexVal = Int(hex, radix: 16) else {
self.init()
return nil
}
switch hex.count {
case 3: self.init(hex3: hexVal, alpha: alpha)
case 6: self.init(hex6: hexVal, alpha: alpha)
default: self.init()
return nil
}
}
convenience init?(hex: Int) {
self.init(hex: hex, alpha: 1.0)
}
convenience init?(hex: Int, alpha: Float) {
if (0x000000 ... 0xFFFFFF) ~= hex {
self.init(hex6: hex, alpha: alpha)
} else {
self.init()
return nil
}
}
}
extension String {
func toColor() -> UniColor? {
UniColor(hexString: self)
}
}
extension Int {
func toColor(alpha: Float = 1.0) -> UniColor? {
UniColor(hex: self, alpha: alpha)
}
}
how to use
let strColor = "#ff0000" // Red color
let color = strColor.toColor()
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var alpha: CGFloat = 0.0
color?.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
print(red, green, blue, alpha)
// 1.0 0.0 0.0 1.0
10. Group characters
The last extension is useful for formatting bank card numbers and other data that needs to be grouped. It can also be used to group numbers of large numbers, but there is a special class. Other possible uses are converting fixed-width spreadsheets to comma-delimited spreadsheets, console games, and many other uses.
The idea of this extension is to insert some characters or string after every n characters of the original string. For example, if we get a card number 1234567890123456
, we usually want to display it as 1234 5678 9012 3456
.
import Foundation
extension String {
mutating func insert(separator: String, every n: Int) {
self = inserting(separator: separator, every: n)
}
func inserting(separator: String, every n: Int) -> String {
var result: String = ""
let characters = Array(self)
stride(from: 0, to: count, by: n).forEach {
result += String(characters[$0..<min($0+n, count)])
if $0+n < count {
result += separator
}
}
return result
}
}
how to use
var cardNumber = "1234567890123456"
cardNumber.insert(separator: " ", every: 4)
print(cardNumber)
// 1234 5678 9012 3456
let pin = "7690"
let pinWithDashes = pin.inserting(separator: "-", every: 1)
print(pinWithDashes)
// 7-6-9-0
Summarize
I hope these extensions save you time and help you make your code cleaner.
see you later. Happy coding!