Reading time: 2 min

This recipe shows how to remove / hide list separator in SwiftUI. This is tricky because each SwiftUI version has its own solution. The end result looks like this:

Simulator%20Screen%20Shot%20-%20iPhone%20SE%20%282nd%20generation%29%20-%202021-11-20%20at%2021.19.12

This recipe will first show the solutions for each SwiftUI version and then present a custom component that works on any version.

SwiftUI 1 (iOS 13, MacOS 10.15)

In SwiftUI 1, you need to tap into the underlying UITableView and change its appearance to hide the separator:

List(1...100, id: \.self) { item in
  Text("\(item)")
}.onAppear {
  UITableView.appearance().separatorStyle = .none
}.onDisappear {
  UITableView.appearance().separatorStyle = .singleLine
}
SwiftUI 2 (iOS 14, MacOS 11)

SwiftUI 2 is the troublemaker of the bunch, because SwiftUI 1 solution just doesn't work on it (which is weird by itself). Unfortunately, there's no easy way out here: you need to simulate a List with a ForEach within a LazyVStack inside a ScrollView. Yeah, it's that bad.

ScrollView {
  LazyVStack {
    ForEach(1...100, id: \.self) { item in
      Text("\(item)")
    }
  }
}

The biggest issue with this approach is that it just doesn't look like a normal list. You can modify the views a bit to achieve the same effect, but it's just sad having to spend time on frivolous issues like that.

SwiftUI 3+ (iOS 15+, MacOS 12+)

SwiftUI 3 finally properly solves this problem with a new modifier, listRowSeparator, that you can attach to row content views:

List(1...100, id: \.self) { item in
  Text("\(item)")
    .listRowSeparator(.hidden)
}

Solution for any version

Here's a small custom view that merges all the recipes above into a single, reusable component:

struct NoSeparatorList<Data, ID, Content>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
  let data: Data
  let id: KeyPath<Data.Element, ID>
  let content: (Data.Element) -> Content

  public init(_ data: Data,
              id: KeyPath<Data.Element, ID>,
              @ViewBuilder content: @escaping (Data.Element) -> Content) {
    self.data = data
    self.id = id
    self.content = content
  }

  var body: some View {
    if #available(iOS 15.0, *) {
      List(data, id: id) { item in
        content(item)
          .listRowSeparator(.hidden)
      }
    } else if #available(iOS 14.0, *) {
      ScrollView {
        LazyVStack {
          ForEach(data, id: id, content: content)
        }
      }
    } else {
      List(data, id: id, rowContent: content)
        .onAppear {
          UITableView.appearance().separatorStyle = .none
        }.onDisappear {
          UITableView.appearance().separatorStyle = .singleLine
        }
    }
  }
}

Then, you can simply use it like this:

NoSeparatorList(1...100, id: \.self) { index in
  HStack {
    Text("List item \(index)")
    Spacer()
    Image(systemName: "chevron.right")
  }
}

Next Post Previous Post