SwiftUI Play Time – Article 2 – Creating a list of business contacts

Hey there, welcome to SwiftUI Play Time. In this article, we will build on top of our previous post, using the business card we created and embed it into a vertically scrollable list.

The code we’re writing is publicly accessible here: https://github.com/Rusti18/SwiftUI-Play-Time. You will be able to see the latest commit of each article by checking the tags section.

As in the first article, we’ll approach each step in a fairly simplistic manner, explaining each thing we do, so that it’s easy to understand for somebody who’s just started with programming. Feel free and fast forward through these sections if you find it too simple.

The problem we will be approaching today is fairly simple – build a list of business contacts, using the business card we created previously.

We will build a quick list of tasks, based on the model we used in the previous article. Before we get to that, we need to consider the following aspects:

  • Our list should be dynamic – it should be able to display any number of items and those items should potentially differ from one another, meaning it should not repeat the same card over and over again. Though these requirements are straightforward for someone with experience, it’s something who’s currently just learning might not think of right away.
  • Based on the previous point – our current card uses hardcoded data. We should look for a way to make it configurable – so let’s start with that.

Our card currently looks like this:

struct BusinessCard: View {
private enum Constants {
static var imageToLabelsSpacing: CGFloat { 30.0 }
static var imageSize: CGSize { CGSize(width: 60.0, height: 60.0) }
static var horizontalPadding: CGFloat { 12.0 }
static var verticalPadding: CGFloat { 24.0 }
static var nameFontHeight: CGFloat { 18.0 }
static var occupationFontHeight: CGFloat { 11.0 }
static var companyFontHeight: CGFloat { 10.0 }
}
var body: some View {
HStack(spacing: Constants.imageToLabelsSpacing) {
Image("man")
.resizable()
.frame(width: Constants.imageSize.width, height: Constants.imageSize.height)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 0.0) {
Text("John Doe")
.font(.system(size: Constants.nameFontHeight, weight: .bold))
Spacer()
Text("Attorney at law")
.font(.system(size: Constants.occupationFontHeight, weight: .regular))
Spacer()
Text("Berkley Ltd.")
.font(.system(size: Constants.companyFontHeight, weight: .regular))
.italic()
}.frame(height: Constants.imageSize.height)
}.padding([.leading, .trailing], Constants.horizontalPadding)
.padding([.top, .bottom], Constants.verticalPadding)
}
}
view raw gistfile1.swift hosted with ❤ by GitHub

By looking at this piece of code – we can see what the hardcoded pieces are: “man”, “John Doe”, “Attorney at law” and “Berkley Ltd.”. In order to make the card configurable, we are going to extract these into properties and create an initializer, where we’ll directly specify these values. Our business card will become:

struct BusinessCard: View {
// MARK: – Properties
private let imageName: String
private let name: String
private let occupation: String
private let workplace: String
// MARK: – Init
init(imageName: String, name: String, occupation: String, workplace: String) {
self.imageName = imageName
self.name = name
self.occupation = occupation
self.workplace = workplace
}
var body: some View {
HStack(spacing: Constants.imageToLabelsSpacing) {
Image(imageName)
Text(name)
Text(occupation)
.font(.system(size: Constants.occupationFontHeight, weight: .regular))
Text(workplace)
}
}
struct BusinessCard_Previews: PreviewProvider {
static var previews: some View {
BusinessCard(
imageName: "man",
name: "John Doe",
occupation: "Attorney at law",
workplace: "Berkeley Ltd."
)
}
}
view raw card.swift hosted with ❤ by GitHub

While name, occupation and workplace are self-explanatory, imageName needs a small explanation – it represents the name of the image as it can be seen under the project Assets section.

Now that we have this in place, the next step is to set up the infrastructure for the vertical business contacts list. In SwiftUI, we can achieve this in more than a single way. We shall discuss two of these ways:

  1. Using the List component
  2. Using a ScrollView with a ForEach and a VStack or LazyVStack

List Component Way

SwiftUI has a native component called List that fits our use case perfectly. You can read about it in detail here. Let’s see how it applies for our example.

We create a simple list, but we are going to hardcode two items for now. We will come back to making it dynamic later.

First, let’s create a new file, called BusinessCardList.swift, where we’re going to define the list: File -> New -> Under the iOS platform look for User Interface and select -> SwiftUI View. Insert the BusinessCardList name.

As mentioned, we will only define a simple List with hardcoded cards:

import SwiftUI
struct BusinessCardList: View {
var body: some View {
List {
BusinessCard(
imageName: "man",
name: "Jack Doe",
occupation: "Plumber",
workplace: "Plumbers LLC"
)
BusinessCard(
imageName: "man",
name: "John Doe",
occupation: "Dentist",
workplace: "Dentists LLC"
)
}
}
}
struct BusinessCardList_Previews: PreviewProvider {
static var previews: some View {
BusinessCardList()
}
}

which, when running the Preview, looks like this:

Pretty simple, isn’t it? Let’s look at the ScrollView option.

ScrollView Component Way

List applies some customizations to the items, that are out of our control, or that we need to remove ourselves – such as, for example, the separator between the items. With ScrollView, there are no changes applied to the items, thus giving us more customization freedom.

In order to achieve the same result – or at least visually similar, due to changes provided by List by default – you need to embed the content into a VStack or LazyVStack, depending on the needs.

The decision between VStack and LazyVStack is performance. In this article, Apple recommends using the second only when performance issues arise. While this might not always be straightforward, a good rule of thumb is that we should use the first for smaller lists, which are not likely to change in the long run, and go with LazyVStack when we could use a data source that we don’t have a size guarantee of, and we’ll be expecting a lot of elements in that list.

Using the same, 2-element hardcoded option, we’re going to create a new file, BusinessCardList_Scroll.swift to showcase this different implementation:

import SwiftUI
struct BusinessCardList_Scroll: View {
var body: some View {
ScrollView {
VStack {
BusinessCard(
imageName: "man",
name: "Jack Doe",
occupation: "Plumber",
workplace: "Plumbers LLC"
)
BusinessCard(
imageName: "man",
name: "John Doe",
occupation: "Dentist",
workplace: "Dentists LLC"
)
}
}
}
}
struct BusinessCardList_Scroll_Previews: PreviewProvider {
static var previews: some View {
BusinessCardList_Scroll()
}
}

You can immediately notice that there is a difference here:

ScrollView does not apply any changes to the items. They’re just displayed one under the other, without separators, background color changes, corner rounding or other adjustments.

Both these examples are using two hardcoded items. How do we proceed in case we receive a larger list of items, of unknown length – for example from a server – and we want to display all of them?

Using a data source

Whenever we want to display multiple items into a dynamic context, we need to use a data source of some sort. In our case, we can use an array of items tailored for the business card, so let’s create one – File -> New -> Swift File and let’s name it BusinessCardViewModel.

Let’s make it a struct and declare the four properties we use in the business card:

struct BusinessCardViewModel {
let imageName: String
let name: String
let occupation: String
let workplace: String
}

We can now also update the BusinessCard – we will use a view model to initialize the parameters, instead of explicitly passing each parameter:

import SwiftUI
struct BusinessCard: View {
// MARK: – Properties
private let viewModel: BusinessCardViewModel
// MARK: – Init
init(viewModel: BusinessCardViewModel) {
self.viewModel = viewModel
}
// MARK: – Constants
private enum Constants {
static var imageToLabelsSpacing: CGFloat { 30.0 }
static var imageSize: CGSize { CGSize(width: 60.0, height: 60.0) }
static var horizontalPadding: CGFloat { 12.0 }
static var verticalPadding: CGFloat { 24.0 }
static var nameFontHeight: CGFloat { 18.0 }
static var occupationFontHeight: CGFloat { 11.0 }
static var companyFontHeight: CGFloat { 10.0 }
}
var body: some View {
HStack(spacing: Constants.imageToLabelsSpacing) {
Image(viewModel.imageName)
.resizable()
.frame(width: Constants.imageSize.width, height: Constants.imageSize.height)
.clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/)
VStack(alignment: .leading, spacing: 0.0) {
Text(viewModel.name)
.font(.system(size: Constants.nameFontHeight, weight: .bold))
Spacer()
Text(viewModel.occupation)
.font(.system(size: Constants.occupationFontHeight, weight: .regular))
Spacer()
Text(viewModel.workplace)
.font(.system(size: Constants.companyFontHeight, weight: .regular))
.italic()
}.frame(height: Constants.imageSize.height)
}.padding([.leading, .trailing], Constants.horizontalPadding)
.padding([.top, .bottom], Constants.verticalPadding)
}
}
struct BusinessCard_Previews: PreviewProvider {
static var viewModel = BusinessCardViewModel(
imageName: "man",
name: "John Doe",
occupation: "Attorney at law",
workplace: "Berkeley Ltd."
)
static var previews: some View {
BusinessCard(viewModel: viewModel)
}
}

We need to also update our existing List and ScrollView implementations, so that the BusinessCard is initialized with a view model instead of explicitly passing the values. I will leave this as a quick exercise for you before moving forward.

Let’s now focus on using a data source for populating our two list implementations. We can use a simple array of BusinessCardViewModel and iterate over it inside our implementations. So a simple list implementation would become:

struct BusinessCardList: View {
let viewModels = [
BusinessCardViewModel(
imageName: "man",
name: "Jack Doe",
occupation: "Plumber",
workplace: "Plumbers LLC"
),
BusinessCardViewModel(
imageName: "man",
name: "John Doe",
occupation: "Dentist",
workplace: "Dentists LLC"
),
]
var body: some View {
List(viewModels) { viewModel in
BusinessCard(viewModel: viewModel)
}
}
}

However, we notice that the compiler starts to complain:

Initializer 'init(_:rowContent:)' requires that 'BusinessCardViewModel' conform to 'Identifiable'

What is this and why does it matter?

Introducing Identifiable

For each layout rendering run, SwiftUI explores the view and, whenever a difference is found, performs an update. I will not go in too deep into this mechanism, as it’s outside the scope of this article – you can see it explained in great depth here.

Simply put, Identifiable is part of this difference detection mechanism, used to say which view is which inside the view hierarchy. It’s a protocol that adds the requirement that items conforming to it expose a property called id that is Hashable.

In order to fulfill this constraint, we will make BusinessCardViewModel adhere to Identifiable by adding a property called id of type UUID (meaning Universally Unique Identifier). We’ll initialize it at declaration time, so that it’s not synthesized within the initializer:

struct BusinessCardViewModel: Identifiable {
let id = UUID()
let imageName: String
let name: String
let occupation: String
let workplace: String
}

Once we do this, the compiler will stop complaining and we’ll be able to declare our List without issues.

ScrollView and List with ForEach

In order to create a ScrollView – since it does not provide the same initializers as List does – you need a component to iterate through the array of view models. ForEach is the construct to use in this situation.

It does a similar job to what we’ve see in List: either iterate through a collection of Identifiable items, or iterate through a list of items, while providing a parameter to be used as a unique identifier (see the next section for more details on this).

Let’s see it in action:

struct BusinessCardList_Scroll: View {
let viewModels = [
BusinessCardViewModel(
imageName: "man",
name: "Jack Doe",
occupation: "Plumber",
workplace: "Plumbers LLC"
),
BusinessCardViewModel(
imageName: "man",
name: "John Doe",
occupation: "Dentist",
workplace: "Dentists LLC"
)
]
var body: some View {
ScrollView {
VStack {
ForEach(viewModels) { viewModel in
BusinessCard(viewModel: viewModel)
}
}
}
}
}

The principle is the same: you iterate through a collection of items and create a card for each item! It’s even more similar when you see the same construct used for List, which is also possible:

List {
ForEach(viewModels) { viewModel in
BusinessCard(viewModel: viewModel)
}
}

Working around Identifiable

Another way we could iterate through view models, without having our BusinessCardViewModel conform to Identifiable, would be to rely on one of its properties and use it as a unique identifier. This works as long as the item is Hashable. Basic types (Int, String etc.) all conform to Hashable by default, so we can achieve it easily within our example.

Both the List initializer and the ForEach iteration construct we’ve mentioned support looping through a collection of items that don’t adhere to Identifiable, as long as we provide a property of the items we’re looping through that can be used as a unique identifier.

In our case, the potential for repetition within the view model’s parameters is extremely high (you can see that even in our example, we’re using the same imageName for both view models).

Based on that, using imageName as an identifier, we will get:

List(viewModels, id: \.imageName) { viewModel in
BusinessCard(viewModel: viewModel)
}

While it works, it does not work correctly for us, as in our example we’re not using a unique imageName, so the List will be incorrectly built:

Since it relies on a unique parameter to build the list items, and imageName is not unique within our data source, it will reuse a previously built card, instead of creating a new one.

Make sure to always use parameters with unique values! Otherwise, you will experience similar situations and it will not always be straightforward why the list is not rendered correctly!

This applies to using Identifiable items too – the id we expose should also be unique!

If we replace imageName with name – the list will be rendered correctly. However, in a real world situation, there can easily be more people with the same name, so while it would work correctly for this example, I would never rely on it in a production application.

Wrap-up

  • Our goal was to build a simple list of business cards, using the card we built in the first article. ✅
  • We described two ways of achieving this:
    • using the List component ✅
    • using the ScrollView component ✅
  • While we also showed an option where we hardcoded our BusinessCard items in the list ✅, we also presented a way to dynamically create a list from a data source ✅
  • Briefly explained Identifiable ✅, how to get around it ✅, and what pitfalls to avoid whenever relying on unique identifiers to define our layout ✅

As in the first article, we’ve built these components without running the project once, by the power of SwiftUI previews.

You can find all the code, free to download, publicly accessible, at https://github.com/Rusti18/SwiftUI-Play-Time.

Hope you learnt something new today. Let me know if you have any questions. Cheers!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: