Little bits of helper code to make your life easier.
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.
SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until September 29th.
Sponsor Hacking with Swift and reach the world's largest Swift community!
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)
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))
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:
Data
instance. If that fails, throw an error.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 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”…
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"))
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))
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")
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)
}
}
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://")
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 Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until September 29th.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.