Swiftwater Logo

Swift Extensions

This entry is part 8 of 21 in the series Swiftwater

ABSTRACT

Swift has a nice feature (not unique to Swift, but Swift makes it an “everyday” feature): Extensions.

Extensions are sort of like class inheritance, but different. They allow you to add extra functionality to data types, structs, enums and classes.

As you’ll see, extensions don’t allow you to add stored properties to an entity, but the functionality is pretty damn powerful, and you can use it to do things like add all kinds of neat shortcuts to everyday classes and scalar types (which aren’t actually that “scalar” in Swift, where everything is some kind of object).

LOCALIZATION MADE EASY

In this example, I’ll show you how I do localization.

The “Prescribed” Fashion

The way that we’re supposed to do (manual) string localization, is to create a “Localizable.strings” file, and localize it. You have a variant of the file for each language you’ll be localizing in.

This is Apple’s treatise on the matter.

This is the way I generally do it. You can get a lot fancier, but this works for me. As I usually have to beg for help from non-technical native speakers of a language, I’ve found that a Localizable.strings file is the best way to get translations.

So, you have this Localizable.strings file, and it’s accessed via the func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String function.

You apply it in a fashion similar to this:

First, you have your Localizable.strings file, which contains a basic “dictionary” of strings, like so:

• • •

"This app's infested with bugs."             =   "This app's loaded with features!";
"What you are asking for is impossible."     =   "We'll have this ready for beta-testing in a month!";
"This project is a nightmare."               =   "This is the project that I've been dreaming of!";
"This schedule is completely unrealistic."   =   "This sounds like a fascinating challenge!";
• • •

By the way, you’ll note the syntax of the strings file looks more like C than Swift. Make sure that you add semicolons to the end of each line, or you’ll get build errors.

You have the token (dictionary key) on the left (blue), and the translation on the right (red). When you ask someone to do a translation for you, they leave the left side alone, and only change the right. This is a very common and sensible localization pattern.

This file shows a localization file for translating Engineering language into Desperate Project Management language.

So, in your code, you implement this like so:

let engineeringStatement: String = "This app's infested with bugs."
let projectManagementHears: String = NSLocalizedString(engineeringStatement, comment: "")
print("Engineering says: \"\(engineeringStatement)\".\nProject Management hears: \"\(projectManagementHears)\".")

Which outputs on the console:

Engineering says: "This app's infested with bugs.".
Project Management hears: "This app's loaded with features!".

What I Do

What I do, is create an extension of String, and add a “localizedVariant” calculated property to it, like so:

extension String {
    var localizedVariant: String { return NSLocalizedString(self, "") }
}

That way, whenever I want the localized variant of any given String, I simply reference its “localizedVariant” property, and the localization (if any) is done for me.

So, if I want to do the same as above, I simply do this:

print("Project Management hears: \"\("This app's infested with bugs.".localizedVariant)\".")
Project Management hears: "This app's loaded with features!".

SIX DEGREES OF SEPARATION

Another fairly classic example is adding a calculated property to the Double data type to convert Degrees into Radians. Radians are required in a lot of trig operations, so this is a fairly common conversion, especially if you are working with maps, which tend to use long/lat pairs expressed in Degrees. The Haversine Formula works in Radians, for example.

Let’s declare a Double extension, like so:

extension Double {
    var radians: Double { return (self * Double.pi)/180.0 }
}

In fact, since Swift is a UTF-8 language, we could even add the “π” symbol to the Double type:

extension Double {
    static var π: Double { return self.pi as Double }
    var π: Double { return type(of: self).π }
}

I like to declare it as a static (class) property, as well as an instance one, so that you can use either one, interchangeably. Note that I reference the class value in the instance value. Having an instance and class variant can be inherently dangerous (The implementations can fork), so if you do it, make sure there’s only one data source.

Also note that, in the instance implementation, I use “type(of: self)“, instead of “Double“. This is because someone from R’lyeh may subclass Double, and decide that Pi isn’t Pi in their dimension.

Now, let’s give that ol’ Haversine a spin (NOTE: I adapted a sample from Ray Wenderlich’s Algorithm Club for this.):

func haversineDistance(la1: Double, lo1: Double, la2: Double, lo2: Double, radius: Double = 6367444.7) -> Double {
    let haversin = { (angle: Double) -> Double in
        return (1 - cos(angle))/2
    }
    
    let ahaversin = { (angle: Double) -> Double in
        return 2*asin(sqrt(angle))
    }
    
    return (radius * ahaversin(haversin(la2.radians - la1.radians) + cos(la1.radians) * cos(la2.radians) * haversin(lo2.radians - lo1.radians))) / 1000.0
}

let amsterdam = (52.3702, 4.8952)
let newYork = (40.7128, -74.0059)

print("The distance from Amsterdam to New York is \(haversineDistance(la1: amsterdam.0, lo1: amsterdam.1, la2: newYork.0, lo2: newYork.1)) kilometers.")

Which gives us:

The distance from Amsterdam to New York is 5859.2709055619 kilometers.

Let’s use what we just learned to add a useful extension to the CoreLocation CLLocationCoordinate2D class:

extension CLLocationCoordinate2D {
    func distanceInKilometersFrom(_ inFrom: CLLocationCoordinate2D) -> Double {
        let haversin = { (angle: Double) -> Double in
            return (1 - cos(angle))/2
        }
        
        let ahaversin = { (angle: Double) -> Double in
            return 2 * asin(sqrt(angle))
        }
        
        return 6367.4447 * ahaversin(haversin(inFrom.latitude.radians - self.latitude.radians) + cos(self.latitude.radians) * cos(inFrom.latitude.radians) * haversin(inFrom.longitude.radians - self.longitude.radians))
    }
}

let amsterdamCoords = CLLocationCoordinate2D(latitude: 52.3702, longitude: 4.8952)
let newYorkCoords = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0059)

print("The distance from Amsterdam to New York is \(newYorkCoords.distanceInKilometersFrom(amsterdamCoords)) kilometers.")

Which gives us the same result as above:

The distance from Amsterdam to New York is 5859.2709055619 kilometers.

Yes, I know. In real-life, we need to use a modified version, to account for Earth’s shape, but this is a lot simpler, for demonstration purposes.

ENUMS, BUT NOT TUPLES

You can also extend enums, but not completely. You can’t add extra cases. What you can do, however, is add methods, and they can act a lot like cases:

extension DecodingError {
    // You can't do this:
    // case WTF
    
    // However, You can declare a static var that will smell a bit like another case...or...something...
    static var WTF: String { return "\u{1F4A9}" }
    // Or an instance var
    var WTF: String { return type(of: self).WTF }
    // And you can declare static or instance functions:
    static func noReallyWTFDude() -> String { return self.WTF }
    func noReallyWTFDude() -> String { return self.WTF }
}

let dailyWTF = DecodingError.WTF
let crapData = DecodingError.dataCorrupted(DecodingError.Context(codingPath:[], debugDescription: dailyWTF, underlyingError: nil))
let thisDataStinks = crapData.noReallyWTFDude()

You can’t extend pure tuples:

typealias Coordinates = (x: Double, y: Double)
// These are both errors:
//extension Coordinates {
//
//}
//extension (x: Double, y: Double) {
//
//}

CONCLUSION

We found out about extending various types, including some Core Foundation types (CLLocationCoordinate2D), and saw how we could add some useful functionality to our coding.

However, there’s more down this rabbit-hole, which we’ll get to in the next post.


SAMPLE PLAYGROUND

import CoreLocation

// Pretend this Dictionary is the result of parsing a "Localizable.strings" file.
// In this case, we are translating from Engineering to Marketing.
let g_localized_strings: [String: String] = [
    "This app's infested with bugs.": "This app's loaded with features!",
    "What you are asking for is impossible.": "We'll have this ready for beta-testing in a month!",
    "This project is a nightmare.": "This is the project that I've been dreaming of!",
    "This schedule is completely unrealistic.": "This sounds like a fascinating challenge!"
]

// Now that's out of the way, let's extend String.

// You cannot add stored properties to a class, struct or enum.
// For example, this is an error:
// extension String {
//     var alternateValue: String = ""
// }

// However, you can add calculated properties:
extension String {
    // NOTE: In "Real Life," I'd have this more like so:
    // var localizedVariant: String { return NSLocalizedString(self, "") }
    var localizedVariant: String { return g_localized_strings[self] ?? self }
}

// Let's see that in action!
let engineeringStatement = "This app's infested with bugs."
let projectManagementHears = engineeringStatement.localizedVariant
let projectManagementAlsoHears = "What you are asking for is impossible.".localizedVariant

print("Engineering says: \"\(engineeringStatement)\".\nProject Management hears: \"\(projectManagementHears)\".")

print("Project Management hears: \"\("This app's infested with bugs.".localizedVariant)\".")

// You can add a static (or class) property:
extension String {
    static var Parrot: String { return "Norwegian Blue" }
}

let deadParrotWas = String.Parrot

// Here's an example of extending Double to return its value (assumed to be Degrees) as Radians.
extension Double {
    // radians = degrees * π / 180.0
    var radians: Double { return (self * Double.pi)/180.0 }
}

// You can also get cheeky (Remember that Swift is UTF-8 compatible):
extension Double {
    static var π: Double { return self.pi as Double }
    var π: Double { return type(of: self).π }
}

let maipai:Double = .π

// You can also add functions in extensions
extension Double {
    func multiplyMeBy(_ inValue: Double) -> Double { return self * inValue }
}

let result = 2.multiplyMeBy(7)

// This returns the value's "Golden Ratio," either higher or lower.
extension Double {
    static var goldenRatio: Double { return (1 + 5.0.squareRoot()) / 2 }
    var goldenRatioValues: (below: Double, above: Double) { return (self / type(of: self).goldenRatio, self * type(of: self).goldenRatio) }
}

let itsGolden = Double.goldenRatio
let mySize: Double = 1.234567
let sizeOfSmallerElement = mySize.goldenRatioValues.below
let sizeOfLargerElement = mySize.goldenRatioValues.above

let degrees90 = 90.0
let degreesToRadians90 = degrees90.radians
let degreesToRadians180 = 180.0.radians

// Adapted from here: https://github.com/raywenderlich/swift-algorithm-club/tree/master/HaversineDistance
func haversineDistance(la1: Double, lo1: Double, la2: Double, lo2: Double, radius: Double = 6367444.7) -> Double {
    let haversin = { (angle: Double) -> Double in
        return (1 - cos(angle))/2
    }
    
    let ahaversin = { (angle: Double) -> Double in
        return 2*asin(sqrt(angle))
    }
    
    return (radius * ahaversin(haversin(la2.radians - la1.radians) + cos(la1.radians) * cos(la2.radians) * haversin(lo2.radians - lo1.radians))) / 1000.0
}

let amsterdam = (52.3702, 4.8952)
let newYork = (40.7128, -74.0059)

print("The distance from Amsterdam to New York is \(haversineDistance(la1: amsterdam.0, lo1: amsterdam.1, la2: newYork.0, lo2: newYork.1)) kilometers.")

extension CLLocationCoordinate2D {
    func distanceInKilometersFrom(_ inFrom: CLLocationCoordinate2D) -> Double {
        let haversin = { (angle: Double) -> Double in
            return (1 - cos(angle))/2
        }
        
        let ahaversin = { (angle: Double) -> Double in
            return 2 * asin(sqrt(angle))
        }
        
        return 6367.4447 * ahaversin(haversin(inFrom.latitude.radians - self.latitude.radians) + cos(self.latitude.radians) * cos(inFrom.latitude.radians) * haversin(inFrom.longitude.radians - self.longitude.radians))
    }
}

let amsterdamCoords = CLLocationCoordinate2D(latitude: 52.3702, longitude: 4.8952)
let newYorkCoords = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0059)

print("The distance from Amsterdam to New York is \(newYorkCoords.distanceInKilometersFrom(amsterdamCoords)) kilometers.")

// You can extend enums, as well:
extension DecodingError {
    // You can't do this:
    // case WTF
    
    // However, You can declare a static var that will smell a bit like another case...or...something...
    static var WTF: String { return "\u{1F4A9}" }
    // Or an instance var
    var WTF: String { return type(of: self).WTF }
    // And you can declare static or instance functions:
    static func noReallyWTFDude() -> String { return self.WTF }
    func noReallyWTFDude() -> String { return self.WTF }
}

let dailyWTF = DecodingError.WTF
let crapData = DecodingError.dataCorrupted(DecodingError.Context(codingPath:[], debugDescription: dailyWTF, underlyingError: nil))
let thisDataStinks = crapData.noReallyWTFDude()

// You cannot extend tuples:
typealias Coordinates = (x: Double, y: Double)
// These are both errors:
//extension Coordinates {
//
//}
//extension (x: Double, y: Double) {
//
//}