Creating a list in SwiftUI

marinbenc

marinbenc🐧

Posted on February 11, 2020

Creating a list in SwiftUI

By this part of the course, you've already built a couple of different SwiftUI screens using a bunch of SwiftUI views. You've used Texts, Images, Rectangles, VStacks and HStacks, but there's one type of screen we haven't yet built: A list.

Lists (AKA table views) are at the core of almost all iOS apps. The most popular apps like Instagram, Twitter or Reddit all show a big list of stuff on their main screen. In this part of this SwiftUI course, you'll learn how to show a list of items in SwiftUI.

You'll learn this by building a contacts screen. This screen will show all of the users your current user can chat with. By the end of this part, you'll have a screen that looks like this:

Showing a List in SwiftUI

Before you can build that screen, though, you first need a way to navigate to it. Let's get started!

You can always find the finished project code on GitHub.

Navigating to the contacts screen

You'll start by creating a new SwiftUI view file for your contacts screen and naming it ContactsView.swift. Change the struct to the following:

import SwiftUI

struct ContactsView: View {

  var body: some View {
    Text("Contacts")
  }

}
Enter fullscreen mode Exit fullscreen mode

So far it doesn't show much — you'll take care of that later. For now, let's add a way to navigate to this view. Open LoginView.swift and change the navigation link's destination to show the newly created contacts view, instead of EmptyView:

NavigationLink(destination: ContactsView(), isActive: $showContacts) {
  EmptyView()
}
Enter fullscreen mode Exit fullscreen mode

If you run the app now and click the login button, you'll navigate to the view. Now we can fill it up with contacts! Well... in a bit.

I am creating this course in collaboration with CometChat, a modern chat platform to help you add chat to you Swift app. During the next few weeks, we’ll be releasing installments of our free SwiftUI course here on dev.to! In the course, you’ll dive deep into SwiftUI by building a real-world production-scale chat app, learning SwiftUI in a practical way, on a scale larger than a simple example app. Follow me to get notified of future parts of this course! You can also follow @CometChat on Twitter see the course of CometChat’s blog.

What's in a List?

First, we'll lay some groundwork on SwiftUI lists. In SwiftUI, Lists are created by using a view called, shockingly, List. Much like Group or VStack, a List is a sort of wrapper around a bunch of child views. It hugs all of its children in a tight embrace and positions them on the screen, one below the other, with separators, paddings, a scroll view and everything else you'd expect from a list.

SwiftUI's List is backed by a UITableView on iOS, which means that the same cell reuse behavior you're used to is there on SwiftUI, too. Apple is an environmentally conscious company, after all, so cells are recycled.

Instead of loading all of your rows at once, SwiftUI will only take up memory for the rows that are currently visible. As you scroll, the rows that disappear are not destroyed, but instead, go into a pool of deactivated rows. When the list needs a new row, instead of allocating one, it will take one from the pool.

When the list takes a row from the pool, it will apply any necessary changes to its views (text values, images, etc.) to match the item at the current index. This gives the appearance of having many rows, while in reality only a couple are ever loaded at once.

All this is to say that Lists are very fast and memory-efficient, regardless of how many rows there are in the list.

In UIKit, you'd probably first create a table view and then think about placing items in that table. The component-based nature of SwiftUI means you'll have to reverse your thinking: Start from smaller views and work your way up.

That's why you'll first create the view for each row in your list and only then begin assembling the contacts screen.

Note: On websites like StackOverflow, you'll often see people suggesting you replace List with a ScrollView and a ForEach view to achieve a desired look. In some cases, this works well. However, ScrollView will load all the items at once. If you have a lot of items, a plain scroll view will be much less efficient than a List!

Making a SiwftUI List row view

Before we create our items, we need a model struct to house information about each contact. Create a new plain Swift file called Contact.swift. Change the contents to the following:

import Foundation

struct Contact: Identifiable, Equatable {
  let name: String
  let avatar: URL?
  let id: String
  var isOnline: Bool
}
Enter fullscreen mode Exit fullscreen mode

You'll notice Contact conforms to Identifiable. To easily show an array of items in a list, those items need to each have a unique identifier. Remember, rows are reused, so the list needs a way of uniquely identifying each item. You conform to Identifiable by giving the struct an id property that is unique.

To create the row view, create a new SwiftUI View file and name it ContactRow.swift.

This is what you'll end up with:

Creating a list cell / row view in SwiftUI

This view has a few things going on. It shows the avatar with a little online badge, the contact's name, as well as their last message.

As you just learned, it's better to start from smaller views and build up to larger views. That's why you'll start by creating a separate view for the avatar and the online badge.

First, you'll need to add a few placeholder avatars to your app. Download the avatar placeholder image and add it to Assets.xcassets. Name the image avatar_placeholder0.

With that in place, add the view struct to the file:

private struct AvatarView: View {
  // 1
  let url: URL?
  let isOnline: Bool

  var body: some View {
    // 2
    ZStack {
      // 3
      Image("avatar_placeholder0")
        .resizable()
        .frame(width: 37, height: 37)
      // 4
      Circle()
        .frame(width: 10, height: 10)
        .foregroundColor(isOnline ? .green : .gray)
        .padding([.leading, .top], 25)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With this code, you create a new SwiftUI view for the avatar. You'll use AvatarView inside the view for the list's row. Here's what's going on in the struct:

  1. The view has two properties, one for the avatar URL and one that tells it whether that contact is online. For now, you'll use placeholder avatars. Later in the course, you'll learn how to fetch them from the internet.
  2. Inside of body, you'll position the image and the online indicator in a ZStack. You've already learned about VStack and HStack. ZStack is similar to those, but in 3D! Instead of arranging items below or to the right of each other, it arranges items on top of each other. This is useful for when you want two views to overlap. In this example, you want the online indicator to be on top of the image.
  3. In the ZStack, you first create an image view with the placeholder and give it a fixed size of 37 by 37 points.
  4. Next, you create a circular view of 10 by 10 points and color it green or gray depending on if the user is online or not. You also position the circle to be in the bottom-right corner of the image.

You'll need to modify the preview to see the view you just created. Scroll down to the PreviewProvider struct and change its content to the following:

struct AvatarView_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      AvatarView(url: nil, isOnline: true)
        .previewLayout(.fixed(width: 100, height: 100))
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This shows the avatar view in all its glory, with a fixed size, inside a group.

Creating a circular avatar view in SwiftUI

You'll use the group later to show your contact row as well. Speaking of which, let's get on creating that view.

Add another View struct to the same file, underneath AvatarView:

struct ContactRow: View {

  struct ContactItem: Identifiable {
    let contact: Contact
    let lastMessage: String
    let unread: Bool

    var id: String { contact.userID }
  }

  let item: ContactItem

}
Enter fullscreen mode Exit fullscreen mode

This is the view that will be shown in the list. Instead of adding a bunch of properties to the struct, you create a nested struct to house all of the data necessary for displaying a contact.

Again, you use Identifiable to help the list reuse items. In this case, passing along the contact's ID will suffice.

For this to be a complete view, you'll also need to add body to the struct:

var body: some View {
  VStack {
    Spacer()
  }
  .background(item.unread ? 
    Color(red: 236 / 255, green: 240 / 255, blue: 254 / 255) : 
    nil)
  .frame(maxWidth: .infinity)
  .frame(height: 67)
}
Enter fullscreen mode Exit fullscreen mode

To center the contents of the view, you'll place the image and the texts inside a vertical stack. For now, the stack contains only a spacer. You also give the stack a light blue background color if the last message is unread, set its width to stretch out as far as it can and make sure its height is a fixed 67 points.

Next, modify the preview so it shows the contact view underneath the avatar view:

Group {
  AvatarView(url: nil, isOnline: true)
    .previewLayout(.fixed(width: 100, height: 100))

  ContactRow(item: ContactRow.ContactItem(
    contact: Contact(name: "Some Name", avatar: nil, id: "0", isOnline: true),
    lastMessage: "Last message is a pretty big message",
    unread: true))
    .previewLayout(.fixed(width: 300, height: 67))

  ContactRow(item: ContactRow.ContactItem(
    contact: Contact(name: "Other Name", avatar: nil, id: "1", isOnline: false),
    lastMessage: "Last message is a pretty big message",
    unread: false))
    .previewLayout(.fixed(width: 300, height: 67))
}
Enter fullscreen mode Exit fullscreen mode

To test all the variations of the contact view, you'll show one preview where the user is online and the message is unread, and one where the user if offline and the message was already read.

All of this code to display a completely empty view! Let's fix that.

Add the avatar view inside a horizontal stack to ContactRow's body:

var body: some View {
  VStack {
    Spacer()

    HStack {
      AvatarView(url: nil, isOnline: item.contact.isOnline)
        .padding(.leading, 20)

      Spacer()
    }

    Spacer()
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

You place the avatar in a horizontal stack so that you can add the two text views to the right of the avatar. You also add another spacer to the bottom of the VStack so that everything is vertically centered in the row.

Next, add the two texts to the view between the avatar view and the spacer in the HStack:

VStack {
  ...
  HStack {
    AvatarView(url: nil, isOnline: item.contact.isOnline)
      .padding(.leading, 20)

    // New code:
    VStack(alignment: .leading) {
      Text(item.contact.name)
        .foregroundColor(.body)
        .fontWeight(item.unread ? .medium : .regular)
        .lineLimit(1)

      Text(item.lastMessage)
        .foregroundColor(.body)
        .font(.system(size: 12))
        .fontWeight(item.unread ? .medium : .regular)
        .lineLimit(1)
        .padding(.top, 2)
    }
    .padding(.leading, 10)
    .padding(.trailing, 20)
    // ---

    Spacer()
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Place the two texts inside a vertical stack since they're arranged one on top of the other. You'll also limit the number of lines of text to 1, to make sure the text doesn't wrap. Finally, you'll add some padding around the vertical stack so that there's a space between the texts and the image.

Creating a list cell / row view in SwiftUI

That's your contact item! It displays the name, the avatar, the last message as well as if the contact is online or not, all at a glance.

Using SwiftUI Lists

Now, it's time to head over to ContactsView.swift to add a bunch of contacts inside a list.

You'll start by creating a state property that will hold your contacts. For now, fill it up with a couple of fake contacts:

@State private var items: [ContactRow.ContactItem] = [
  .init(
    contact: Contact(name: "Some Name", avatar: nil, id: "0", isOnline: true),
    lastMessage: "This is my last message that I sent you",
    unread: true),
  .init(
    contact: Contact(name: "Other Name", avatar: nil, id: "1", isOnline: false),
    lastMessage: "This is my last message that I sent you",
    unread: false),
  .init(
    contact: Contact(name: "Third Name", avatar: nil, id: "2", isOnline: true),
    lastMessage: "This is my last message that I sent you",
    unread: false)
]
Enter fullscreen mode Exit fullscreen mode

You can use this array to populate a list. Replace body with the following:

var body: some View {
  List(items) { item in
    ContactRow(item: item)
  }
}
Enter fullscreen mode Exit fullscreen mode

To use a List with an array, you can pass the array to the list, as well as a function that will create the view for each row, given the array element for that row. Since you already created your row view, you can just create that view with the contact.

Note: Remember that to show a dynamic array of items in a List, those items need to conform to the Identifiable protocol as described above.

Showing a list of custom views in SwiftUI

In only three lines of code, you have a list of dynamic items from an array. Take that, UIKit!

Removing separators from a SwiftUI List

However, there is a slight problem with this screen. Instead of a nice-looking list, we have a space and a separator between each item. This happens because SwiftUI uses a plain UITableView to show the list, which has built-in separators and insets.

To change this, you first need to tweak the way you created the list. I showed you a shortcut for creating a list from an array, but there's another way to do it using ForEach. Change body to the following:

var body: some View {
  List {
    ForEach(0..<items.count, id: \.self) { i in
      ContactRow(item: self.items[i])
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, List receives a builder that builds an array of views. You'll build this array using ForEach. ForEach is a special type of view that replaces itself with a list of views, one below the other. In the above code,

ForEach(0..<items.count, id: \.self) { i in
  ContactRow(item: self.items[i])
}
Enter fullscreen mode Exit fullscreen mode

...is exactly the same as writing:

ContactRow(item: self.items[0])
ContactRow(item: self.items[1])
ContactRow(item: self.items[2])
Enter fullscreen mode Exit fullscreen mode

Each of these three views will get treated as a separate row by the list. Again, because a List needs to identify items, you'll pass the current index (self) as the ID of that item.

Note: In UIKit it was somewhat difficult to combine different types of cells. It was especially hard to combine static and dynamic cells. In SwiftUI, since List receives an array of rows, it's very easy to combine static and dynamic content. You can first return a hard-coded row, then a ForEach of a couple of different dynamic rows, then another static one, and so on. A List can hold any combination of static and dynamic rows of different types.

Now that you changed the way you build your list, you can apply a little hack to remove the separators.

Since List is backed by a UITableView, any global changes you make to table views will also apply to SwiftUI lists. UIKit provides an API to make global changes to all instances of a view called UIAppearance. Each UIView subclass has an appearance method that returns something called an appearance proxy. Changing properties of the appearance proxy propagates those changes to all instances of the class, like changing a static property.

You can change the table view's appearance proxy to remove the separators, thus removing all separators from you List.

Add the following initializer to the view struct:

init() {
  UITableView.appearance().tableFooterView = UIView()
  UITableView.appearance().separatorStyle = .none
}
Enter fullscreen mode Exit fullscreen mode

Note: Appearance proxies are not limited to table views. Lots of other SwiftUI views are backed by UIKit views. You can also use this pattern to change the look of navigation bars, toggles, texts and other views.

Keep in mind though, this is a bit of a hack. For now, SwiftUI is backed by UIKit, but this is an implementation detail that you don't want to rely on. Nothing is stopping Apple from ditching UITableView and making their own thing in SwiftUI. I'm showing you this trick because, currently, there's no better way to remove separators.

When the struct loads, it will remove all separators from every table view in the app.

How to remove separators from a SwiftUI List

The list now looks much cleaner, but I think it looks better with separators.

Adding custom separators to a SwiftUI List

You might be pulling your hair out by now because you just removed them! Well, SwiftUI is flexible enough that, instead of using UITableView's separators, it's better to make our own using plain SwiftUI views.

Go back the view for your item: ContactRow.swift. Inside body, use a rectangle view at the bottom of the VStack to add separators back in:

  var body: some View {
    VStack {
      Spacer()

      HStack {
        ...
      }

      Spacer()

      // New code:
      Rectangle()
        .frame(height: 1)
        .foregroundColor(Color(UIColor.separator))
    }
    ...
  }
Enter fullscreen mode Exit fullscreen mode

The rectangle will stretch the width of the view by default, all you need to change is to set the height to 1 point and set the color to whichever separator color you'd like.

Head back to ContactView.swift to see your list:

Adding a custom List separator in SwiftUI

It's already looking great, but there's one small issue I can see. There's still extra space around each item.

Removing space around SwiftUI List rows

SwiftUI adds space around each row in a List by default. This space is called the inset of a list row. The inset is an instance of EdgeInsets, a struct that holds values for how much the view is shifted from the leading, trailing, top and bottom edges.

You can remove the inset by calling listRowInsets on the view inside the list, and giving it a value of EdgeInsets(), where all the values are zero.

Add the following to body:

var body: some View {
  List {
    ForEach(0..<items.count, id: \.self) { i in
      ContactRow(item: self.items[i])
        .listRowInsets(EdgeInsets())
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Okay, now it's looking perfect.

Removing the default spacing around SwiftUI List rows

Except... I'd still like to add one final touch. In the original design, there was a shadow underneath the last item, giving the whole screen a sense of depth that is currently missing.

Adding a shadow to a SwiftUI view

To add a shadow to the row you'll call, you guessed it: shadow. This method takes the color of the shadow, its radius, as well as an x and y value that determines how much the shadow is offset from the view.

SwiftUI adds shadows to opaque parts of the view. That is parts that have some sort of color. Right now, the background of your rows is transparent. It appears white because the color underneath them is white. To show the shadow underneath a row, you need to first color the background in white, and then add the shadow.

Add the following to body:

var body: some View {
  List {
    ForEach(0..<items.count) { i in
      ContactRow(item: self.items[i])
        .listRowInsets(EdgeInsets())
        .background(Color.white)
        .shadow(
          color: i == self.items.count - 1 ? 
            Color(UIColor.black.withAlphaComponent(0.08)) : 
            Color.clear,
          radius: 10, x: 0, y: 2)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You set the background color to white and apply a shadow to the view. You only want to add a shadow to the last row, though, so you make the shadow completely transparent if the row is not the last.

Adding a shadow to a SwiftUI view

Okay, now I'm happy with the look of this screen. Let's run this in the simulator so see it in action.

Creating a SwiftUI List view

Oh boy, it looks like you're not off the hook yet! There's one more issue to take care of. The original design shows a title in the navigation bar, which isn't there in our version. Thankfully, that's easy to add.

Adding a navigation bar title in SwiftUI

To add a navigation bar title, you need to call navigationBarTitle on the root view of your body. In your case, that's the list view:

var body: some View {
  List {
    ...
  }.navigationBarTitle("Contacts", displayMode: .inline)
}
Enter fullscreen mode Exit fullscreen mode

By default, SwiftUI adds a large, bold navigation bar title with lots of space around it. You already saw this when you were building your login screen. In this case, you want a more subtle title that is part of the navigation bar. That's why you pass .inline as the display mode, integrating the title with the bar.

Adding a regular navigation bar to a SwiftUI view

Now it looks perfect. No, for real this time. You can take a break and pat yourself on the back, I won't be bothering you with design changes anymore.

Conclusion

You're slowly moving more and more towards a fully-fledged SwiftUI chat app! You built out a contacts screen that shows a list of users.

If you want to know more about SwiftUI lists, here are a few links:

  • In UIKit, table views can hold sections of items. The same is true for SwiftUI Lists. To organize rows into sections, place a Section view inside a list, and rows inside the section.
  • There's a couple of different list styles that you can use off the shelf. There's a default list, a plain list, a grouped one (used in the Settings app in iOS), a carousel and one that looks like the lists in an iOS sidebar. You can find all of them here. To change the style of your list, call listStyle on the list.

While the contacts in your list might be hard-coded for now, everything is in place to load these users from the internet — in later parts of this SwiftUI course. So keep reading!

💖 💪 🙅 🚩
marinbenc
marinbenc🐧

Posted on February 11, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related