Tuples in Swift, Advanced Usage and Best Practices

Anonymous Structs

Anonymous Structs

Tuples as well as structs allow you to combine different types into one type:

let user1 = (name: "Carl", age: 40)
// vs.
struct User {
    let name: String
    let age: Int
}
let user2 = User(name: "Steve", age: 39)

As you can see, these two types are similar, but whereas the tuple exists simply as an instance, the struct requires both a struct declaration and a struct initializer. This similarity can be leveraged whenever you have the need to define a temporary struct inside a function or method. As the Swift docs say:

Tuples are useful for temporary groups of related values. (...) If your data structure is likely to persist beyond a temporary scope, model it as a class or structure (...)

As an example of this, consider the following situation where the return values from several functions first need to be uniquely collected and then inserted:

func zipForUser(userid: String) -> String { return "12124" }
func streetForUser(userid: String) -> String { return "Charles Street" }
let users = [user1]

// Find all unique streets in our userbase
var streets: [String: (zip: String, street: String, count: Int)] = [:]
for user in users {
    let zip = zipForUser(userid: user.name)
    let street = streetForUser(userid: user.name)
    let key = "\(zip)-\(street)"
    if let (_, _, count) = streets[key] {
        streets[key] = (zip, street, count + 1)
    } else {
        streets[key] = (zip, street, 1)
    }
}

// drawStreetsOnMap(streets.values)
for street in streets.values { print(street) }

Here, the tuple is being used as a simple structure for a short-duration use case. Defining a struct would also be possible but not strictly necessary.

Another example would be a class that handles algorithmic data, and you're moving a temporary result from one method to the next one. Defining an extra struct for something only used once (in between two or three methods) may not be required.

// Made up algorithm
func calculateInterim(values: [Int]) -> 
    (r: Int, alpha: CGFloat, chi: (CGFloat, CGFloat)) {
    return (values[0], 2, (4, 8))
}
func expandInterim(interim: (r: Int, 
                         alpha: CGFloat, 
                           chi: (CGFloat, CGFloat))) -> CGFloat {
    return CGFloat(interim.r) + interim.alpha + interim.chi.0 + interim.chi.1
}

print(expandInterim(interim: calculateInterim(values: [1])))

There is, of course, a fine line here. Defining a struct for one instance is overly complex; defining a tuple 4 times instead of one struct is overly complex too. Finding the sweet spot depends.