SALE ENDS TODAY: Save 50% on all Swift books and bundles! >>

8 Useful Swift Extensions

Little bits of helper code to make your life easier.

Paul Hudson       @twostraws

Extensions make our coding lives easier by providing wrappers around commonly used functionality. Sometimes they help us avoid common mistakes, sometimes they involve particularly efficient solutions that would be tricky to recreate everywhere, but sometimes they are just there for convenience – and that’s perfectly fine.

In this article I’m going to walk you through eight extensions I use regularly, providing the code for them plus examples in action.

Counting words in a string

The definition of a word is more complex than you think, particularly because it’s common to see words joined by hyphens, en dashes (–) and em dashes (—). As a result, just separating by spaces is usually not good enough, but there is a neat little regular expression you can use to do a better job:

extension String {
    var wordCount: Int {
        let regex = try? NSRegularExpression(pattern: "\\w+")
        return regex?.numberOfMatches(in: self, range: NSRange(location: 0, length: self.utf16.count)) ?? 0
    }
}

The \w meta character means “any alphanumeric character”, so that matches sequences of letters and numbers in a string.

Use it like this:

let phrase = "The rain in Spain"
print(phrase.wordCount)

Replacing a fix number of substrings

Swift’s strings have a built-in method for replacing all instances of a substring with another, but if you want only a fixed number of replacements you need to do it yourself.

One smart solution here is to call range(of:) repeatedly, replacing instances of the substring until a maximum replacements parameter is reached.

Here’s that in code:

extension String {
    func replacingOccurrences(of search: String, with replacement: String, count maxReplacements: Int) -> String {
        var count = 0
        var returnValue = self

        while let range = returnValue.range(of: search) {
            returnValue = returnValue.replacingCharacters(in: range, with: replacement)
            count += 1

            // exit as soon as we've made all replacements
            if count == maxReplacements {
                return returnValue
            }
        }

        return returnValue
    }
}

Use it like this:

let phrase = "How much wood would a woodchuck chuck if a woodchuck would chuck wood?"
print(phrase.replacingOccurrences(of: "would", with: "should", count: 1))

Decoding JSON from your bundle

It’s common to want to load JSON data from your app bundle, perhaps to pull in some chapters for your guidebook, challenges for the user, a list of alternate apps to recommend, or whatever.

The problem is that doing this takes multiple, boring steps:

  1. Find the URL for the JSON file in your bundle. If that fails, throw an error.
  2. Try to load the URL into a Data instance. If that fails, throw an error.
  3. Attempt to decode that data into instances of your object. If that fails, throw an error.
  4. Finally, you have your data.

If that JSON file really is needed for your app, then it must always be in your bundle, must always be loadable from your bundle, and must always be decodable into objects of the type you expect. If any of those things aren’t true you have a serious programmer error on your hands, so your app shouldn’t really be running.

Rather than repeat each of those steps regularly, here’s an extension I use that wraps it up neatly:

extension Bundle {
    func decode<T: Decodable>(_ type: T.Type, from filename: String) -> T {
        guard let json = url(forResource: filename, withExtension: nil) else {
            fatalError("Failed to locate \(filename) in app bundle.")
        }

        guard let jsonData = try? Data(contentsOf: json) else {
            fatalError("Failed to load \(filename) from app bundle.")
        }

        let decoder = JSONDecoder()

        guard let result = try? decoder.decode(T.self, from: jsonData) else {
            fatalError("Failed to decode \(filename) from app bundle.")
        }

        return result
    }
}

You can use that to load any bundle JSON into any Codable type, like this:

let items = Bundle.main.decode([TourItem].self, from: "Tour.json")

Clamping a number

Clamping is the practice of forcing a value to fall within a specific range. For example, if I say “pick a number between 10 and 20”…

  • If you pick 15 then your number is 15.
  • If you pick 5, below the bottom of our range, then your number is clamped to 10.
  • If you pick 50, above the top of our range, then your number is clamped to 20.

We can write an extension that makes any kind of data clampable, like this:

extension Comparable {
    func clamp(low: Self, high: Self) -> Self {
        if (self > high) {
            return high
        } else if (self < low) {
            return low
        }

        return self
    }
}

Now it will work great on integers, doubles, and other numbers, like this:

let number1 = 5
print(number1.clamp(low: 0, high: 10))
print(number1.clamp(low: 0, high: 3))
print(number1.clamp(low: 6, high: 10))

let number2 = 5.0
print(number2.clamp(low: 0, high: 10))
print(number2.clamp(low: 0, high: 3))
print(number2.clamp(low: 6, high: 10))

It even works on other comparable things, like strings:

let letter1 = "r"
print(letter1.clamp(low: "a", high: "f"))

Truncating with ellipsis

UIKit’s labels do a great job of truncating strings to a specific length, but for other purposes – such as writing out to a file, rendering to an image, or showing messages – we need to roll something ourselves.

extension String {
    func truncate(to length: Int, addEllipsis: Bool = false) -> String  {
        if length > count { return self }

        let endPosition = self.index(self.startIndex, offsetBy: length)
        let trimmed = self[..<endPosition]

        if addEllipsis {
            return "\(trimmed)..."
        } else {
            return String(trimmed)
        }
    }
}

Use it like this:

let testString = "He thrusts his fists against the posts and still insists he sees the ghosts."
print(testString.truncate(to: 20, addEllipsis: true))

Loading bundle images

If you have UIImage instances in an asset catalog for your bundle, it can be annoying having to unwrap them when you’ve using UIImage(named:). You know they are present because otherwise your app bundle is in a really bad state, but Swift does not, hence the unwrap.

One option is to use create a bundleName initializer that centralizes force unwraps in one place:

extension UIImage {
    convenience init(bundleName: String) {
        self.init(named: bundleName)!
    }
}

Another is to create a StaticString initializer that stops you from trying to call that initializer using a string you didn’t type by hand – a fail-safe to avoid string interpolation going wrong:

extension UIImage {
    convenience init(bundleName: StaticString) {
        self.init(named: "\(bundleName)")!
    }
}

Use it like this:

let image = UIImage(bundleName: "Horse")

Counting elapsed days

If you have two dates and want to know how many days separate the two, it’s not something you can do just by counting the number of seconds that elapsed.

Instead, this is a job for Foundation’s Calendar class: look at the start of day for each date, read the date components from each one, then return the gap, like this:

extension Date {
    func days(between otherDate: Date) -> Int {
        let calendar = Calendar.current

        let startOfSelf = calendar.startOfDay(for: self)
        let startOfOther = calendar.startOfDay(for: otherDate)
        let components = calendar.dateComponents([.day], from: startOfSelf, to: startOfOther)

        return abs(components.day ?? 0)
    }
}

Adding a prefix to a string

If you have a collection of URLs like “www.hackingwithswift.com” and you want to make sure they all start with “https://“, you might write something like this:

let fullURLs = urls.map { "https://\($0)" }

But what if some have the “https://“ prefix already? In that case you’ll end up with some that are correct, and others that are “https://https://www.apple.com”. Awkward!

Here’s a small extension to avoid that problem entirely, because it checks whether the prefix exists before adding it:

extension String {
    func withPrefix(_ prefix: String) -> String {
        if self.hasPrefix(prefix) { return self }
        return "\(prefix)\(self)"
    }
}  

Use it like this:

let url = "www.hackingwithswift.com"
let fullURL = url.withPrefix("https://")

Now what?

Extensions are such a simple way to make our coding lives easier, so we can spend more time focusing on the hard that problems that really matter. What are your favorite extensions? Let me know on Twitter!

SPONSORED Instabug helps you identify and resolve severe crashes quickly. You can retrace in-app events and know exactly which line of code caused the crash along with environment details, network logs, repro steps, and the session profiler. Ask more questions or keep users up-to-date with in-app replies straight from your dashboard. Instabug takes data privacy seriously, so no one sees your data but you! See more detailed features comparison and try Instabug's crash reporting SDK for free.

Save 50% on all books and bundles

The biggest ever Hacking with Swift sale is now on, letting you save 50% on all books and bundles. Learn something new with Swift and enjoy great savings while the sale lasts!

Click here to save 50% in our Black Friday sale!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and a speaker at Swift events around the world. If you're curious you can learn more here.

Was this page useful? Let us know!

Average rating: 4.4/5