So you want to build a list with Apple's new declarative UI framework. Maybe you're used to building for the web like I am, and you think, "Let's mock up a data structure and iterate over it to make a list." Pretty straightforward, or so you thought. In JavaScript, you might do something like this:
// Mock data structure
const racers = [
{
id: 1,
name: 'Valentino Rossi',
team: 'Yamaha'
},
{
id: 2,
name: 'Marc Márquez',
team: 'Repsol Honda'
},
];
// In React
racers.map(racer => (
<p key={racer.id}>{racer.name} rides with {racer.team}</p>
));
// In ES6
const container = document.querySelector('.container');
racers.map(racer => {
const item = document.createElement('p');
item.setAttribute('id', racer.id);
item.textContent = `${racer.name} rides with ${racer.team}`;
container.appendChild(item);
});
I thought I could do the same thing with SwiftUI. Define an array of dictionaries and iterate over them using something like SwiftUI's ForEach
or List
views. In UIKit, List
would be roughly equal to UITableView
, and from my experience with UITableView
, the table wants everything to be set up in a very particular fashion. So approaching cautiously, will List
require us to do some extra stuff, or can we just chuck some data in and the world will be well and good? Turns out, there's a bit more setup. This won't work:
import SwiftUI
struct RacerList : View {
// Mock data structure
let racers: [[String:String]] = [
[
"name": "Valentino Rossi",
"team": "Yamaha"
],
[
"name": "Marc Márquez",
"team": "Repsol Honda"
]
]
var body: some View {
List(racers) { racer in
if let name: String = racer.name, team: String = racer.team {
Text("\(name) rides with \(team)")
}
}
}
}
The compiler throws this error: Unable to infer complex closure return type; add explicit type to disambiguate
, essentially boiling down to: "Hey, I don't understand what type you're returning." But didn't we say the name
and team
optionals are strings when we unwrapped them?
As it turns out, the problem isn't with the SwiftUI view code, it's with the data structure. Swift is a strongly typed, protocol-oriented language (bold for my own sake). The data you pass into a List
needs to conform to the Identifiable
protocol so that it knows how to reference each item.
I like to think about protocols like this: protocols are to class inheritance as GraphQL is to REST. In regular class inheritance in most object-oriented languages, if you want to extend a class, you get all the methods and properties and other stuff from that class in your new sub-class. With protocols, you split up that baggage into different "slices" that you can tell your new classes to conform to. Much like GraphQL, where you tell the backend exactly what you want and what you don't want, protocols allow you to define exactly what you want your sub classes (or structs) to get.
We can implement the Identifiable
protocol like this:
import SwiftUI
// Mock data structure
struct Racer: Identifiable {
var id: Int
var name: String
var team: String
}
struct RacerList : View {
var body: some View {
let a = Racer(id: 1, name: "Valentino Rossi", team: "Yamaha")
let b = Racer(id: 2, name: "Marc Márquez", team: "Repsol Honda")
let racers = [a, b]
return List(racers) { racer in
Text("\(racer.name) rides with \(racer.team)")
}
}
}
Yay, it works! 🙌 Now if we're to start refactoring this a bit, we can take the data structure, or model as it's known in the iOS world, and put it in a separate directory with all our models. Then, anywhere in our app we define a capital R Racer
, the compiler knows we are referencing our model, and hence has detailed information about how it conforms to Identifiable
and the type of each property.
Done! This has been another round trip in what may become a loosely associated series of articles about how to do things in Swift coming from JavaScript land. Thanks for reading!
More resources:
Top comments (0)