stringIt 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 Stringclasses 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 Stringextensions. 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 varbefore 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 nilextremely 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 NSLocalizedStringfunctions. 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 Stringextension 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 Substringan instance of the type, not Stringthe type. And when you index a character, it returns an Characterinstance 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 Stringif 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@localhostsuch 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 UserDefaultsclasses, formerly called NSUserDefaults.

What you need to know about it:

  • When the application is restarted, UserDefaultsthe data previously stored in is still there.
  • Uninstalling the app will clear UserDefaultsthe data in .
  • You can read data UserDefaultsfrom it and from any thread.
  • After saving the data to UserDefaults, you should call synchronize()the method (only available in iOS 11 and earlier)

UserDefaultsTypical usage:

let value: String? = UserDefaults.standard.string(forKey: "key")

If there is a keyvalue 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.

  1. It should shorten the code and make it clearer.
  2. 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()
    }
}

UserDefaultsThere 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 storean 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 Stringextension 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. Dataand 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. UIKitprovides UIColora class, while Cocoahas an almost identical NSColorclass. 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!

Leave a Reply

Your email address will not be published. Required fields are marked *