How to Make Custom List Rows

SwiftUICustomRows.png

When I previously wrote about how to create Lists in SwiftUI I used a very simple list row. In this tutorial we’ll create a better styled, more complex list row composed of multiple views.

Setting up

The first thing we’ll need before we get started on building out our views is a data model. For the custom list row in this tutorial we’ll need our data model to:

  • Subscribe to Identifiable and have an id so it can be used in a List

  • Have a name

  • Have a summary

  • Have an image name (which will just be an image in the asset catalog for now)

Here’s the data model we’ll be using for items in this tutorial:

struct Item: Identifiable {
    let id = UUID()
    let name: String
    let summary: String
    let imageName: String
}

Next up we’ll need some test data. I’ll be honest, I was struggling for inspiration of what kind of data to use while writing this tutorial. So I did what anyone else would do - I took a break and ordered stuff online 😂. BUT! While I was at it I decided to order some coffee since I’m running low (plus the whole COVID-19 quarantine thing, so need to stock up!) and that’s where I found inspiration. Shout out to the guys at Kings Coast Coffee! (Totally not a sponsored post, they just have great coffee and a great looking website!)

For my test data I went ahead and added an extension on Item that looks like this:

extension Item {
    static var testData: [Item] {
        return [
            Item(name: "THE DARKNESS BLEND", summary: "Perfect start to any day or just the right roast to keep you going. Crisp, subtle strawberry and apple notes pair with cocoa for a smooth, sweet finish.", imageName: "DarknessBlend"),
            Item(name: "COUNTACH", summary: "With notes of caramel, a hint of red fruit and rich chocolate are all wrapped up in a smooth and velvety finish. This signature blend is designed for those looking to create cafe quality espresso in the comfort of your own home.", imageName: "Countach"),
            Item(name: "GET WOKE BLEND", summary: "Looking for something truly unique? This fruity and creamy blend features notes of caramel, blueberry, dark cherries and brown sugar.", imageName: "GetWokeBlend"),
            Item(name: "EZ MORNING BLEND", summary: "Silky smooth and notes of raspberry, caramel and citrus make this the perfect morning cup of coffee.", imageName: "EZMorning"),
            Item(name: "DR. LUPO'S LIFELINE ROAST", summary: "The Lifeline Roast is a unique collaboration, hand crafted and chosen by the Doctor himself. This combination of bright and berry bomb Ethiopian Yirgacheffe blends and balanced with the earthy and robust notes of Sumatra and Guatemala. Bittersweet, wine like flavors are highlighted in this vibrant cup of coffee. This roast will be your lifeline on those days you just need a little something extra.", imageName: "LupoBlend")
        ]
    }
}

I also added images for each coffee in my test data (with names matching the imageName property). Feel free to use whatever test data you’d like, though!

Breaking up the row into components

View composition is huge when architecting SwiftUI apps. You want to break your views down to smaller pieces that you can then combine together into bigger, more complex views. For this example the cell isn’t too complex, but splitting it up a bit will be better than trying to put everything together in one view.

OneRow.png

Looking at a single row on its own I see two main components; the “card-like” background with rounded corners and a shadow, and the content of the row that displays the name, summary, and image. We’ll build out each of those as a separate view, where the content view is pulled into the background view.

CardContentView

OneRowStacks.png

The content view can be put together with a couple of stack views. First, there’s an HStack that lays out the text on the left and the image on the right. Then the name and summary are put into a VStack so they lay out vertically.

struct CardContentView: View {
    @Binding var item: Item

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 8) {
                Text(item.name)
                    .font(.title)
                    .fixedSize(horizontal: false, vertical: true)
                Text(item.summary)
                    .font(.caption)
            }
            Image(item.imageName)
                .resizable()
                .frame(width: 100, height: 100)
        }
        .padding()
    }
}

First thing you’ll see in our CardContentView is a Binding to an Item. This is so we can extract the data from the item model to display it in the view. Then in the body we have our stack views to lay everything out. Make sure to align your VStack of Text views to .leading so spacing doesn’t look totally different on each list row!

There are a couple of things in there that may not look familiar to you:

  • .fixedSize(horizontal: false, vertical: true)

    • This allows the name Text view to expand horizontally (versus being determined by its parent) while fixing its vertical size to its ideal size. This prevents the name from truncating due to its parent wanting to lay out narrower by default.

  • .resizable()

    • This modifier is added to the image so it can be resized to a smaller frame (in our case, 100x100)

CardListRow

EmptyRow.png

Next up is to create the list row itself, which will be composed of the card-like background in a ZStack with the CardContentView we created earlier. Don’t forget to include a Binding to an Item here as well so it can be passed along to the CardContentView!

struct CardListRow: View {
    @Binding var item: Item

    var body: some View {
        ZStack {
            Color.white
                .cornerRadius(12)
            CardContentView(item: $item)
        }
        .fixedSize(horizontal: false, vertical: true)
        .shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 2)
    }
}

Putting it all together

OK so we have a CardListRow that embeds a CardContentView to display information from our model, Item. Now it’s time to put it all together in a List!

struct ContentView: View {
    @State var items: [Item]

    var body: some View {
        List(items.indices) { itemIndex in
            CardListRow(item: self.$items[itemIndex])
        }
        .padding(EdgeInsets(top: 44, leading: 0, bottom: 24, trailing: 0))
        .edgesIgnoringSafeArea(.all)
    }
}

I made a couple of layout tweaks here to lay things out a little better. First, I wanted to allow the list to scroll all the way up under the status bar so I added the .edgesIgnoringSafeArea(.all) modifier. Then I adjusted the padding so when the view loads it doesn’t start with the first item all the way up at the top under the status bar. I also added a little bottom padding so it scrolls up above the home bar on newer iPhones.

Now you can test it all out using the test data we added earlier:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(items: Item.testData)
    }
}

But what about that separator?

You’re likely seeing a separator show up between your list rows. If you want to get rid of that you can hide it using UITableView.appearance(). I have found the best way to do this is to set the separatorStyle to .none in .onAppear, then reset it in .onDisappear. So now your ContentView should look like this:

struct ContentView: View {
    @State var items: [Item]

    var body: some View {
        List(items.indices) { itemIndex in
            CardListRow(item: self.$items[itemIndex])
        }
        .padding(EdgeInsets(top: 44, leading: 0, bottom: 24, trailing: 0))
        .edgesIgnoringSafeArea(.all)
        .onAppear {
            UITableView.appearance().separatorStyle = .none
        }
        .onDisappear {
            UITableView.appearance().separatorStyle = .singleLine
        }
    }
}

Congratulations! Now you know how to create more complex custom List rows by composing views.

Check out some more recent tutorials

Previous
Previous

Easy to Use Cell Reuse Extensions

Next
Next

Splitting Strings in Swift