SwiftUI
Remove Line Separators
List Customization
iOS Development
Swift Programming

How can I remove the line separators from a List in SwiftUI without using ForEach?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

SwiftUI list separators are controlled by system defaults and list style, so they can appear even in simple layouts. If your screen has static rows or grouped sections, you can hide separators without wrapping content in ForEach. The right approach depends on iOS version and whether you use List or a custom scroll layout.

Why Separators Behave Differently Across Versions

In modern iOS, SwiftUI exposes row-level separator APIs. In older versions, separator behavior is inherited from underlying UIKit table views. That is why a snippet that works on one target may fail on another.

Practical rule:

  • iOS 15 and newer: use SwiftUI modifiers such as listRowSeparator.
  • Older targets: use scoped UIKit appearance fallback.

Keeping version logic explicit prevents surprises during deployment-target changes.

Hiding Separators in a Static List

You do not need ForEach if rows are fixed. Apply row modifiers directly to each item.

swift
1import SwiftUI
2
3struct SettingsView: View {
4    var body: some View {
5        List {
6            Text("Account")
7                .listRowSeparator(.hidden)
8            Text("Notifications")
9                .listRowSeparator(.hidden)
10            Text("Privacy")
11                .listRowSeparator(.hidden)
12        }
13        .listStyle(.plain)
14    }
15}

This is the clearest approach for short, predictable lists.

If you use sections, separators can still be hidden per row in each section:

swift
1List {
2    Section("General") {
3        Text("Appearance").listRowSeparator(.hidden)
4        Text("Language").listRowSeparator(.hidden)
5    }
6
7    Section("Support") {
8        Text("Help Center").listRowSeparator(.hidden)
9        Text("Contact").listRowSeparator(.hidden)
10    }
11}
12.listStyle(.insetGrouped)

Apply Modifier at the Right Level

A common issue is putting the separator modifier on List and expecting all rows to change automatically. In many layouts, the reliable behavior comes from applying the modifier to each row view.

If you have repeated custom row components, hide separators inside that component so usage stays consistent.

swift
1struct MenuRow: View {
2    let title: String
3
4    var body: some View {
5        HStack {
6            Text(title)
7            Spacer()
8            Image(systemName: "chevron.right")
9                .foregroundColor(.secondary)
10        }
11        .padding(.vertical, 8)
12        .listRowSeparator(.hidden)
13    }
14}

Then use MenuRow repeatedly in List without needing ForEach.

Fallback for Older iOS Targets

If you support versions where row-level API is unavailable, use a scoped UIKit fallback. Keep the fallback local to the screen and restore defaults when leaving.

swift
1import SwiftUI
2
3struct LegacyListView: View {
4    var body: some View {
5        List {
6            Text("Alpha")
7            Text("Beta")
8            Text("Gamma")
9        }
10        .onAppear {
11            UITableView.appearance().separatorStyle = .none
12        }
13        .onDisappear {
14            UITableView.appearance().separatorStyle = .singleLine
15        }
16    }
17}

This avoids leaking global appearance changes into unrelated screens.

When List Is Not the Best Choice

If design requires card spacing, custom backgrounds, and no table behavior, use ScrollView plus LazyVStack. This gives full control and naturally avoids default separators.

swift
1import SwiftUI
2
3struct CardMenuView: View {
4    let items = ["Home", "Billing", "Security", "About"]
5
6    var body: some View {
7        ScrollView {
8            LazyVStack(spacing: 12) {
9                ForEach(items, id: \.self) { item in
10                    Text(item)
11                        .frame(maxWidth: .infinity, alignment: .leading)
12                        .padding()
13                        .background(Color(.secondarySystemBackground))
14                        .clipShape(RoundedRectangle(cornerRadius: 12))
15                }
16            }
17            .padding()
18        }
19    }
20}

Tradeoff is that you lose some built-in list features and must handle behavior such as editing or index sidebars yourself.

Accessibility and Visual Clarity

Removing separators can reduce readability in dense menus. Add another visual grouping signal:

  • Increase vertical spacing.
  • Use card backgrounds.
  • Add subtle dividers inside custom rows where needed.

Also test dynamic type sizes. Rows may visually merge at large text sizes if spacing is too tight.

Common Pitfalls

  • Applying separator modifier only on List and expecting row-level effect. Fix by applying modifier on row views.
  • Using global UITableView.appearance without restoring it. Fix by resetting in onDisappear.
  • Removing separators without adding alternative grouping cues. Fix with spacing, background contrast, or card containers.
  • Assuming identical behavior across all iOS versions. Fix by using explicit availability and fallback logic.
  • Switching to custom scroll layout without considering missing list features. Fix by reviewing interaction requirements before migration.

Summary

  • You can hide separators in SwiftUI List without using ForEach.
  • Row-level listRowSeparator(.hidden) is the preferred modern API.
  • For older targets, use a scoped UIKit fallback and restore defaults.
  • Custom ScrollView layouts provide full control when List styling is too restrictive.
  • Preserve readability and accessibility after removing visual dividers.

Course illustration
Course illustration

All Rights Reserved.