UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

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.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

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!

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI 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 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 Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.