When you think about it, we take a heck of a lot of stuff for granted when we write Swift code. For example, if we write 4 < 5
, we expect that to return true – the developers of Swift (and LLVM, larger compiler project that sits behind Swift) have already done all the hard work of checking whether that calculation is actually true, so we don’t have to worry about it.
But what Swift does really well is extend functionality into lots of places using protocols and protocol extensions. For example, we know that 4 < 5
is true because we’re able to compare two integers and decide whether the first one comes before or after the second. Swift extends that functionality to arrays of integers: we can compare all the integers in an array to decide whether each one should come before or after the others. Swift then uses that result to sort the array.
So, in Swift we expect this kind of code to Just Work:
struct ContentView: View {
let values = [1, 5, 3, 6, 2, 9].sorted()
var body: some View {
List(values, id: \.self) {
Text(String($0))
}
}
}
We don’t need to tell sorted()
how it should work, because it understands how arrays of integers work.
Now consider a struct like this one:
struct User: Identifiable {
let id = UUID()
var firstName: String
var lastName: String
}
We could make an array of those users, and use them inside a List
like this:
struct ContentView: View {
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
]
var body: some View {
List(users) { user in
Text("\(user.lastName), \(user.firstName)")
}
}
}
That will work just fine, because we made the User
struct conform to Identifiable
.
But how about if we wanted to show those users in a sorted order? If we modify the code to this it won’t work:
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
].sorted()
Swift doesn’t understand what sorted()
means here, because it doesn’t know whether to sort by first name, last name, both, or something else.
One way to do this is by providing a closure to sorted()
to do the sorting ourselves. We'll be handed two objects from the array, $0
and $1
if you're using shorthand names, and we should return true if the first object should be sorted before the second, like this:
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
].sorted {
$0.lastName < $1.lastName
}
That absolutely works, but it’s not an ideal solution for two reasons.
First, this is model data, by which I mean that it’s affecting the way we work with the User
struct. That struct and its properties are our data model, and in a well-developed application we don’t really want to tell the model how it should behave inside our SwiftUI code. SwiftUI represents our view, i.e. our layout, and if we put model code in there then things get confused.
Second, what happens if we want to sort User
arrays in multiple places? You might copy and paste the closure once or twice, before realizing you’re just creating a problem for yourself: if you end up changing your sorting logic so that you also use firstName
if the last name is the same, then you need to search through all your code to make sure all the closures get updated.
Swift has a better solution. Arrays of integers get a simple sorted()
method with no parameters because Swift understands how to compare two integers. In coding terms, Int
conforms to the Comparable
protocol, which means it defines a function that takes two integers and returns true if the first should be sorted before the second.
We can make our own types conform to Comparable
, and when we do so we also get a sorted()
method with no parameters. This takes two steps:
Comparable
conformance to the definition of User
.<
that takes two users and returns true if the first should be sorted before the second.Here’s how that looks in code:
struct User: Identifiable, Comparable {
let id = UUID()
var firstName: String
var lastName: String
static func <(lhs: User, rhs: User) -> Bool {
lhs.lastName < rhs.lastName
}
}
There’s not a lot of code in there, but there is still a lot to unpack.
First, yes the method is just called <
, which is the “less than” operator. It’s the job of the method to decide whether one user is “less than” (in a sorting sense) another, so we’re adding functionality to an existing operator. This is called operator overloading, and it can be both a blessing and a curse.
Second, lhs
and rhs
are coding conventions short for “left-hand side” and “right-hand side”, and they are used because the <
operator has one operand on its left and one on its right.
Third, this method must return a Boolean, which means we must decide whether one object should be sorted before another. There is no room for “they are the same” here – that’s handled by another protocol called Equatable
.
Fourth, the method must be marked as static
, which means it’s called on the User
struct directly rather than a single instance of the struct.
Finally, our logic here is pretty simple: we’re just passing on the comparison to one of our properties, asking Swift to use <
for the two last name strings. You can add as much logic as you want, comparing as many properties as needed, but ultimately you need to return true or false.
Tip: One thing you can’t see in that code is that conforming to Comparable
also gives us access to the >
operator – greater than. This is the opposite of <
, so Swift creates it for us by using <
and flipping the Boolean between true and false.
Now that our User
struct conforms to Comparable
, we automatically get access to the parameter-less version of sorted()
, which means this kind of code works now:
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
].sorted()
This resolves the problems we had before: we now isolate our model functionality in the struct itself, and we no longer need to copy and paste code around – we can use sorted()
everywhere, safe in the knowledge that if we ever change the algorithm then all our code will adapt.
GO FURTHER, FASTER Unleash your full potential as a Swift developer with the all-new Swift Career Accelerator: the most comprehensive, career-transforming learning resource ever created for iOS development. Whether you’re just starting out, looking to land your first job, or aiming to become a lead developer, this program offers everything you need to level up – from mastering Swift’s latest features to conquering interview questions and building robust portfolios.
Link copied to your pasteboard.