165

I have this code to display a list of custom rows.

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            List(1...10) {_ in
                CustomRow()
            }
        }
    }
}

However, I want to remove the line on each row. I tried not using List and instead using ForEach inside ScrollView but it completely removes all the styling including its padding and margins. I just want to remove the lines and nothing else.

Please help, thank you.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
ygee
  • 2,580
  • 3
  • 17
  • 21
  • 2
    Possible duplicate of [Remove extra separators below List in SwiftUI](https://stackoverflow.com/questions/56498045/remove-extra-separators-below-list-in-swiftui) – Matteo Pacini Jun 12 '19 at 03:52
  • 4
    Hi, question in the link is to *remove extra lines* for empty rows, this one is to *remove all lines* for all rows (empty or non-empty), thank you. – ygee Jun 12 '19 at 05:55
  • Yes, slightly different questions, but unfortunately the same conclusion (for now): doesn't seem to be possible. See answers to this Q as well: https://stackoverflow.com/questions/56517904/how-do-i-modify-the-background-color-of-a-list/ – Marius Waldal Jun 12 '19 at 07:46
  • 1
    `LazyVStack` inside `ScrollView` is buggy and lacks all the features of `List` – visc Sep 27 '20 at 03:27

23 Answers23

257

iOS 15:

This year Apple introduced a new modifier .listRowSeparator that can be used to style the separators. you can pass .hidden to hide it:

List {
    ForEach(items, id:\.self) { 
        Text("Row \($0)")
            .listRowSeparator(.hidden)
    }
}

iOS 14:

you may consider using a LazyVStack inside a ScrollView instead (because iOS is NOT supporting UIAppearance for SwiftUI lists anymore).

LazyVStack Preview


iOS 13:

⚠️ This method is deprecated and it's not working from iOS 14

There is a UITableView behind SwiftUI's List for iOS 13. So to remove

Extra separators (below the list):

you need a tableFooterView and to remove

All separators (including the actual ones):

you need separatorStyle to be .none

Example of usage

init() {
    if #available(iOS 14.0, *) { 
        // iOS 14 doesn't have extra separators below the list by default.
    } else {
        // To remove only extra separators below the list:
        UITableView.appearance().tableFooterView = UIView()
    }

    // To remove all separators including the actual ones:
    UITableView.appearance().separatorStyle = .none
}

var body: some View {
    List {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
    }
}

Note that a static list doesn't show extra separators below the list by default

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 7
    thanks, this works, and you can add this to ```.onAppear ``` as well – Sky Dec 06 '19 at 05:43
  • 18
    `UITableView.appearance().separatorStyle = .none` weirdly isn't working for me, also not `.introspectTableView { tableView in tableView.separatorStyle = .none }` (with `SwiftUI-Introspect`...) – User Jul 31 '20 at 13:56
  • What iOS version? – Mojtaba Hosseini Jul 31 '20 at 15:50
  • 1
    iOS 14 builders using Forms who want one section to lack dividers between certain elements, but to retain dividers in other areas of the same view: simply use a LazyVStack around the views you want to lack divider lines – Beginner Aug 29 '20 at 22:28
  • 4
    Just to note, it's really important to make sure `.listRowSeparator(.hidden)` is _inside_ the `List`. – George Jun 12 '21 at 23:24
  • Would you mind updating the description in the "iOS 14" section here? > you may consider using a LazyVStack inside a ScrollView instead (because iOS is NOT supporting appearance for SwiftUI lists anymore). // The "iOS 15" section above seems to contradict with it. Apparently iOS will continue to support appearance for SwiftUI lists? – FateNuller Feb 25 '22 at 23:56
  • Saved my ass.... I had issue that I dont want the seperator and Dont want space on top for First row, But i was getting both issue in iOS 15 and so your answer helped me.. – Kudos Aug 21 '22 at 17:46
89

iOS 15+:

Simply add .listRowSeparator(.hidden) as a modifier to the view contained in the List. https://developer.apple.com/documentation/swiftui/texteditor/listrowseparator(_:edges:)

List {
    ForEach(garage.cars) { car in
        Text(car.model)
            .listRowSeparator(.hidden)
    }
}

iOS 13 only:

Adding UITableView.appearance().separatorColor = .clear anywhere in your code before the List appears should work. While this solution removes the separators, note that all List instances will be bound to this style as there’s no official way currently to only remove separators of specific instances. You may be able to run this code in onAppear and undo it in onDisappear to keep styles different.

Also note that this code assumes Apple is using a UITableView to back List which is not true in the iOS 14 SDK. Hopefully they add an official API in the future. Credit to https://twitter.com/singy/status/1169269782400647168.

Natanel
  • 1,706
  • 1
  • 18
  • 19
  • While this code may answer the question, it would be better to explain how it solves the problem without introducing others and why to use it. Code-only answers are not useful in the long run. – Al Foиce ѫ Sep 12 '19 at 20:14
  • Finally, thanks! Just simple and it works (at least for now), Cheers! – ygee Sep 27 '19 at 07:33
  • 26
    Thanks! Just a note that I would modify a separator style instead since it is easier to restore to the initial state `.onAppear { UITableView.appearance().separatorStyle = .none } .onDisappear { UITableView.appearance().separatorStyle = .singleLine }` – Artemiy Sobolev Oct 12 '19 at 20:30
  • Using this api crashes the app for some reason, need to find another workaround. – Karen Karapetyan Nov 25 '19 at 20:08
  • Apple is likely to decouple `List` from using `UITableView` under the hood. When it does, this implementation will break. You can use `.listRowInsets` on your List's subviews instead. – Pigpocket Jan 06 '20 at 23:47
  • One problem with using this in `.onAppear` and `.onDisappear` is that the latter doesn't get called when you present a modal sheet, so all lists and forms inside the modal still get effected by `.onAppear`. – iMaddin Apr 10 '20 at 06:33
  • Maybe use a single column grid in iOS 14? – Ever Uribe Jun 28 '20 at 00:31
  • @EverUribe it’s been possible to even use a simple `VStack` to avoid separators but you don’t get the benefits of using `List`. Not sure how much of a difference this makes in 14 though especially with the introduction of `LazyVStack` which delays initialization of future rows until needed to show the user. Also animation enhancements might make updates to the rows more natural. Using `LazyVGrid` is of course possible as well in 14 with additional benefits. – Natanel Jun 28 '20 at 18:17
11

Check out SwiftUI-Introspect. It exposes the underlying UIKit/AppKit views.

iOS 13 builds only:

In this case you could manipulate the UITableView directly (without having to change all table views via the appearance proxy):

    import Introspect
    :
    :
    List {
        ...
    }.introspectTableView { tableView in
         tableView.separatorStyle = .none
    }
sam-w
  • 7,478
  • 1
  • 47
  • 77
markiv
  • 1,578
  • 16
  • 12
  • 2
    Yup, this no longer works on iOS 14 builds. Depending on your use-case, you could try `SidebarListStyle` to hide separators: `.listStyle(SidebarListStyle())` – markiv Sep 21 '20 at 08:46
11

IOS 14

enter image description here

There is currently no solution to hide the separators on the IOS 14 beta.

If you don't need an editable List, you should use a LazyVStack inside a ScrollView.

But if you want to stay on the List. I found a solution on the Apple forum by samwarner. https://developer.apple.com/forums/thread/651028

This is a temporary solution. In some cases you may need to adjust the insets. Here is its implementation:

struct HideRowSeparatorModifier: ViewModifier {
    static let defaultListRowHeight: CGFloat = 44
    var insets: EdgeInsets
    var background: Color
    
    init(insets: EdgeInsets, background: Color) {
        self.insets = insets
        var alpha: CGFloat = 0
        UIColor(background).getWhite(nil, alpha: &alpha)
        assert(alpha == 1, "Setting background to a non-opaque color will result in separators remaining visible.")
        self.background = background
    }
    
    func body(content: Content) -> some View {
        content
            .padding(insets)
            .frame(
                minWidth: 0, maxWidth: .infinity,
                minHeight: Self.defaultListRowHeight,
                alignment: .leading
            )
            .listRowInsets(EdgeInsets())
            .background(background)
    }
}

extension EdgeInsets {
    static let defaultListRowInsets = Self(top: 0, leading: 16, bottom: 0, trailing: 16)
}

extension View {
    func hideRowSeparator(insets: EdgeInsets = .defaultListRowInsets, background: Color = .white) -> some View {
        modifier(HideRowSeparatorModifier(insets: insets, background: background))
    }
}

Finally, here is the implementation on a list. You have to add .hideRowSeparator() on the list cell.

struct CustomRow: View {
    let text: String
    
    var body: some View {
        HStack {
            Text(self.text)
            Image(systemName: "star")
        }
    }
}

struct ContentView : View {
    @State private var fruits = ["Apple", "Orange", "Pear", "Lemon"]
    
    var body: some View {
        VStack(alignment: .leading) {
            List {
                ForEach(self.fruits, id: \.self) { str in
                    CustomRow(text: str)
                        .hideRowSeparator()
                }
            }
        }
        .padding(.top)
    }
}
Atomike-Toxic
  • 212
  • 1
  • 4
  • 12
  • 1
    using this workaround, the first row shows the separator from top – JAHelia Sep 01 '20 at 08:30
  • 1
    ...excellent Modifier, thank you very much. In case you do a custom swipe-cell, you might be setting `.listRowBackground(background)` in addition to your existing `.background(background)` in the HideRowSeparatorModifiers's body... – iKK May 06 '21 at 13:32
  • 1
    @JAHelia yes, i have the same problem – LiangWang Aug 25 '21 at 11:37
  • I gave up on using a List. Quite a hassle for removing paddings and separators. LazyVStack worked (since you mentioned if you dont need an editable List). – chitgoks Sep 11 '22 at 04:40
10

iOS 13 builds only:

While these answers are technically correct they will affect a List or Form globally(across the entire app) from my experience.

A hacky way I found to resolve this problem, at least in my app, is to add the following code to the "main" content view of the app:

.onAppear(perform: {
    UITableView.appearance().separatorStyle = .none
})

Then on any other view that you want to the separator lines add this to the end of the List or Form view

.onAppear(perform: {
    UITableView.appearance().separatorStyle = .singleLine
})

This seems to add the single line separator to any view sheet that is above the main content view. Again this is all anecdotal to my recent SwiftUI experience.

In my experience I only had to add the .onAppear(... = .singleLine) method to one of my "detail" views and the separator line appeared on all subsequent views that were presented.

Edit: Another note as this answer continues to gain attention. This solution I posted doesn't solve all cases, it certainly did not solve it for me, again in some cases. I ended up using Introspect for SwiftUI to solve this problem across the entire app.

I hope this clears up some confusion and frustration as people come across this post.

sam-w
  • 7,478
  • 1
  • 47
  • 77
glob
  • 180
  • 2
  • 10
8

Doing something like:

UITableView.appearance().separatorColor = .clear

works, but in a lot of cases is not something that I would recommend. These are global changes - i.e. they will affect all instances of UITableView. This is a problem if you have multiple UITableViews that want different styles. Or if you are developing a framework, clients using your framework will inherit those changes too!

A safer solution is to only target UITableViews that live inside a specified container. Luckily the appearance api gives us a way to be specific:

UITableView.appearance(whenContainedInInstancesOf: [UIHostingController<YourSwiftUiViewHere>.self]).separatorColor = .clear
tomcully
  • 204
  • 2
  • 4
  • 2
    First of all, your code won't even compile as the correct name is `appearance(whenContainedInInstancesOf:)`. Overall though, it sounds like a better approach, indeed. Sadly, it didn't work for me. I replaced global appearance overwrite (worked as expected) with the targeted one and it had no effect whatsoever... – NeverwinterMoon Apr 05 '20 at 07:22
  • 1
    @NeverwinterMoon thanks for letting me know about the typo! I'll update it now. I wrote it directly in stack overflow so my bad. I have had it working for separator color, cell background color and other properties. not sure why it's not working correctly for you. I can only checking out the view hierarchy and checking what is overlaying the incorrect colour for you. check this out for view hierarchy inspector https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html – tomcully Apr 06 '20 at 11:00
8

For iOS 14 you have this :

.listStyle(SidebarListStyle()) # IOS 14
YanSte
  • 10,661
  • 3
  • 57
  • 53
  • 8
    Thanks for pointing this. It removed the separator and disclosure arrow, but now it adds a gray background which I can't remove it. Is there a way to change the background? Why does apple has to make it so complicated?... – Emil Jan 23 '21 at 22:32
  • 1
    @Emil I don't know :) – YanSte Aug 18 '21 at 07:24
  • Not working on iOS 16 – Farid Feb 24 '23 at 15:17
4

Use a ScrollView?

Some state that represents your list

@State var menuItems: [String] = ["One", "Two", "Three"]

The a SwiftUI ForEach loop inside a ScrollView

ScrollView {
    ForEach(self.menuItems, id: \.self) { item in
        Text(item)
    }
}
Jon Vogel
  • 5,244
  • 1
  • 39
  • 54
  • That's a valid suggestion but @Mojtaba Hosseini's answer is more complete when it also suggests a `LazyVStack` inside the `ScrollView`. You could make it clearer here what are the trade-offs when replacing List by ScrollView - one of them is fixed with `LazyVStack` (iOS 14+), so at least not all cells are initialized at once, but you still don't get the full reusability behavior a List/UITableView would give you, nor any other styling, when the original question was only about hiding the separator and explicitly mentions `ScrollView` has been attempted. – Gobe Sep 27 '20 at 00:04
4

All the answers tell you to use ScrollView (which is what I recommend too)

But in case you want to use List and want to remove the separator lines..

Install the SwiftPM: https://github.com/siteline/SwiftUI-Introspect

SAMPLE:

List {
    Text("Item 1")
    Text("Item 2")
}
.introspectTableView { tableView in
    tableView.separatorStyle = .none
}
Sarvesh Sridhar
  • 387
  • 3
  • 3
4

For iOS13,iOS14 and remove the separator at the top of the first cell

Add viewModifier

extension View {
    func hideRowSeparator(insets: EdgeInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0),
                          background: Color = .white) -> some View {
        modifier(HideRowSeparatorModifier(insets: insets, background: background))
    }
}

struct HideRowSeparatorModifier: ViewModifier {

  static let defaultListRowHeight: CGFloat = 44

  var insets: EdgeInsets
  var background: Color

  init(insets: EdgeInsets, background: Color) {
    self.insets = insets

    var alpha: CGFloat = 0
    if #available(iOS 14.0, *) {
        UIColor(background).getWhite(nil, alpha: &alpha)
        assert(alpha == 1, "Setting background to a non-opaque color will result in separators remaining visible.")
    }
    self.background = background
  }

  func body(content: Content) -> some View {
    content
        .padding(insets)
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: Self.defaultListRowHeight)
        .listRowInsets(EdgeInsets())
        .overlay(
            VStack {
                HStack {}
                .frame(maxWidth: .infinity)
                .frame(height: 1)
                .background(background)
                Spacer()
                HStack {}
                .frame(maxWidth: .infinity)
                .frame(height: 1)
                .background(background)
            }
            .padding(.top, -1)
        )
  }
}

Usage

struct ContentView: View {
    var body: some View {
        List {
            ForEach(0 ..< 30) { item in
                HStack(alignment: .center, spacing: 30) {
                    Text("Hello, world!:\(item)").padding()
                }
                .hideRowSeparator(background: .white)
            }
        }
        .listStyle(PlainListStyle())
    }
}
wangrui460
  • 79
  • 5
3

I started a project to solve this in iOS14 since the iOS 13 workarounds no longer work. It allows setting the separator style, separator color, and separator inset.

Hide separators on the List

List { <content> }
    .listSeparatorStyle(.none)

Show a single divider line with configurable color and insets

List { <content> }
    .listSeparatorStyle(.singleLine, color: .red, inset: EdgeInsets(top: 0, leading: 50, bottom: 0, trailing: 20)

https://github.com/SchmidtyApps/SwiftUIListSeparator

SchmidtyApps
  • 145
  • 1
  • 4
3

Remove paddings and separator

iOS 14.2, Xcode 12.2

ScrollView {
    LazyVStack {
        ForEach(viewModel.portfolios) { portfolio in
            PortfolioRow(item: portfolio)
        }
    }
}

This gives you complete control over the list. Current implementation of List doesn't provide full control and contains some issues.

atereshkov
  • 4,311
  • 1
  • 38
  • 49
3

This seems to be the only thing that works for me.

List() {

}
.listStyle(SidebarListStyle())
ScottyBlades
  • 12,189
  • 5
  • 77
  • 85
3

If you want to hide only the bottom seperator, do something like this

Text("Hello, world!")
   .listRowSeparator(.hidden, edges: [.bottom])
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
talshahar
  • 528
  • 3
  • 12
2

I have the same issue as you about handling separates line in List of SwiftUI.

To make it more simple, let's think about these case:

If your app target is:

iOS 13 and 14: just use ScrollView:

ScrollView {
 ForEach(datas, id: \.self) { item in
    YourCustomRowView(data: item)
 }
}

Just keep in mind that you can handle everything in ScrollView that List can handle (just do more a little bit)

iOS 15: Use List, because in iOS 15 have a lot of function was added more to handle separate line:

List {
  ForEach(self.datas) { item in
    YourCustomRowView(data: item)
        .listRowSeparator(.hidden)
  }
}
1

For iOS 14:

As .listRowSeparator(.hidden) is available only for iOS 15, you can hide separator in lower versions by setting edgeinsets as 0 explicitly.

Content View:

List {
    ForEach(viewModel.items, id: \.id) { item in
        YourListItem(item)
        .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
    }
}

Accompany above change, by making background of row item to white (or color of root page)

Row Item:

var body: some View {
    VStack {
        ..... your content
    }
    .background(Colors.white)
}

VStack is just example. It can be any component.

0

This is my extension ListRowExtensions for hide list row separator and custom this one.

import SwiftUI

// MARK: List row extensions

extension View {
    
    func hideListRowSeparator() -> some View {
        return customListRowSeparator(insets: .init(), insetsColor: .clear)
    }
    
    func customListRowSeparator(
        insets: EdgeInsets,
        insetsColor: Color) -> some View {
        modifier(HideRowSeparatorModifier(insets: insets,
                                          background: insetsColor
        )) .onAppear {
            UITableView.appearance().separatorStyle = .none
            UITableView.appearance().separatorColor = .clear
        }
    }
}

// MARK: ViewModifier

private struct HideRowSeparatorModifier: ViewModifier {
        
    var insets: EdgeInsets
    var background: Color
    
    func body(content: Content) -> some View {
        content
            .padding(insets)
            .frame(
                minWidth: 0,
                maxWidth: .infinity,
                maxHeight: .infinity,
                alignment: .leading
            )
            .listRowInsets(EdgeInsets())
            .background(background)
    }
}

Use :

// Without list row separator
List {
    ForEach(self.viewModel.data, id: \.id) { item in
        Text("Text")
    }
    .hideRowSeparatorItemList()
}

// With list row separator with color and size
List {
    ForEach(self.viewModel.data, id: \.id) { item in
        Text("Text")
    }
    .customListRowSeparator(insets: EdgeInsets(top: 0,
                                               leading: 0,
                                               bottom: 5,
                                               trailing: 0),
                            insetsColor: Color.red)
}
YanSte
  • 10,661
  • 3
  • 57
  • 53
0

I'm not sure if you need all the functionality of "UITableView" in SwiftUI, but if you just want to just display a list of views in iOS 13 or later couldn't you just do:

ScrollView {
    VStack(alignment: .leading) {
        ForEach(1...10) { _ in
            CustomRow()
        }
    }
}

And then add .padding() for any margins you want?

Bill Dunay
  • 181
  • 1
  • 4
0

It is possible to just use negative insets and solid color to mask over the separator.

Dan Fu
  • 1
-1

You can remove the dividers by setting the listStyle to InsetListStyle(), like this:

.listStyle(InsetListStyle())

Add this to the end of your code

-1

A simple solution that will work on both iOS 13 and 14

extension List {
func removeSeparator() -> some View {
    if #available(iOS 14.0, *) {
        return self.listStyle(SidebarListStyle()).erasedToAnyView()
    } else {
        return self.onAppear {
            UITableView.appearance().separatorStyle = .none
        }.erasedToAnyView()
    }
}
jfredsilva
  • 1,434
  • 1
  • 9
  • 15
-1

I have the same problem. But I know a handmade solution for it. So, if you set List row parameters like:

.listRowInsets(EdgeInsets(top: -5, leading: 0, bottom: 0, trailing: 0))

and padding of row view body like

.padding(EdgeInsets(top: Statics.adjustValue(v: 10), leading: Statics.adjustValue(v: 10), bottom: Statics.adjustValue(v: 10), trailing: Statics.adjustValue(v: 10)))

then separators will be hidden.

FOR ALL iOS VERSIONS

Len_X
  • 825
  • 11
  • 29
-1

This is my solution that contains all considerations:

    
    let style: UITableViewCell.SeparatorStyle
    
    public func body(content: Content) -> some View {
        content
            .introspectTableView { tableView in
                     tableView.separatorStyle = .none
            }
    }
}
 
public extension View {
    
    func listSeparator(style: UITableViewCell.SeparatorStyle) -> some View {
        ModifiedContent(content: self, modifier: ListSeparatorStyle(style: style))
    }
}

Implemented:

List {
   // code...
}
.listSeparator(style: .none)
moyoteg
  • 351
  • 3
  • 11