Reading time: 2 min

This recipe shows how to implement a custom separator/divider in any SwiftUI list feeded by a ForEach - like VStack, LazyVStack, HStack, LazyHStack, etc.

The end result looks like this:


The gist of the recipe is to implement a custom ForEach that inserts separators between items. It does so by doubling the number of items, and then rendering either the content or the separator based on the index. It also allows you to specify if you want to render the last separator or not. Each separator view builder gets its element above as a parameter:

struct ForEachWithSeparator<Data: RandomAccessCollection, Content: View, Separator: View>: View 
where Data.Element: Hashable {
  let data: Data // data to render
  let content: (Data.Element) -> Content // data item render
  let separator: (Data.Element) -> Separator // separator renderer
  let showLast: Bool // if true, shows the separator at the end of the list

  var body: some View {
    let size = data.count * 2 - (showLast ? 0 : 1)
    let firstIndex = data.indices.startIndex
    return ForEach(0..<size) { i in
      let element = data[data.index(firstIndex, offsetBy: i / 2)]
      if i % 2 == 0 {
      } else {

extension ForEach where Data.Element: Hashable, Content: View {
  func separator<Separator: View>(showLast: Bool = true,
                                  @ViewBuilder separator: @escaping (Data.Element) -> Separator) -> some View {
    ForEachWithSeparator(data: data,
                         content: content,
                         separator: separator,
                         showLast: showLast)

And here's the sample of it in action:

struct Separator_Previews: PreviewProvider {
  static let colors: [Color] = [.red, .blue, .green, .gray, .orange, .pink, .purple, .yellow, .black]
  static let items = ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit,", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua.", "Ut", "enim", "ad", "minim", "veniam,", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."]

  static var previews: some View {
    ScrollView {
      LazyVStack {
        ForEach(items, id: \.self) { item in
        }.separator(showLast: true) { item in
          let size = item.count - 1
          let colorIndex = min(size, colors.count - 1)
            .frame(height: CGFloat(size) * 1.2)

Next Post Previous Post