Customising a List in SwiftUI
December 01, 2020Building a List
in SwiftUI is a real pleasure compared to its counterpart from UIKit. We went from having a UITableView
, a UITableViewDataSource/UITableViewDelegate
and a custom cell to just making a simple loop on our data to generate our List
🥳.
But if you ever tried to hide a separator or the NavigationLink
arrow from your list then you know that the initial dream 😇 can quickly become a nightmare 😡.
Today let's see how to customise those two items into a List
both for iOS13 and iOS14.
iOS13
Table separator
Let's start with iOS13 which you may still need to be compatible with. Hiding the separator is actually pretty easy: you can rely on UITableView
appearance!
UITableView.appearance().separatorStyle = .none
Downside of this technique is it will be disabled for the all application. If you need more granular control, one solution might be to rely on SwiftUI-Introspect:
List {
...
}
.introspectTableView { tableView in
tableView.separatorStyle = .none
}
NavigationLink arrow indicator
Disabling the arrow is a little bit "trickier". Basically you'll have to use a ZStack
to layer an empty NavigationLink
(yes). On top of which you'll actually display your content:
List {
ForEach(data) {
ZStack {
NavigationLink(destination: MyDestination()) {
EmptyView()
}
.opacity(0)
MyRowContent($0)
}
}
}
It's more verbose and less "aesthetic" than regular NavigationLink but it efficiently hide the arrow.
Voila! You customised your app for iOS13. But unfortunately for you... those don't work on iOS14 😬
iOS14
Apple changed some implementation details and so we are back to finding solutions for iOS14. We could use same workaround just as we did on iOS13. However this is far from ideal and we might face new issues when iOS15 is out.
Fortunately for us Apple also introduced LazyVStack this year. So instead of using a List we can instead migrate to a LazyVStack
:
ScrollView {
LazyVStack {
ForEach(data) {
NavigationLink(destination: MyDestination()) {
MyRowContent($0)
}
}
}
}
With this code you will have neither separator nor navigation accessory arrow issues. But what if you still need to support iOS13? You can build a custom view to use use List
or LazyVStack
.
struct CompatibleList<Row: Identifiable, RowContent: View>: View {
private let rows: [Row]
private let rowContent: (Row) -> RowContent
init(rows: [Row], @ViewBuilder rowContent: @escaping (Row) -> RowContent) {
self.rows = rows
self.rowContent = rowContent
}
var body: some View {
if #available(iOS 14.0, *) {
ScrollView {
LazyVStack(spacing: 0) {
rowsBody
}
}
} else {
List {
rowsBody
}
.listStyle(PlainListStyle())
.introspectTableView { tableView in
tableView.separatorStyle = .none
}
}
}
var rowsBody: some View {
ForEach(rows) { row in
rowContent(row)
}
}
}
While nice note that we are now much more limited in our API. We can't use Section
for instance. For that we would need to go one level deeper and make our own custom Result Builder (formerly known as Function Builder).
You may also have noticed that this custom view only handle half of our issues: we don't handle NavigationLink
for iOS13 and iOS14! Easiest solution I found is to build an extension to use in place of NavigationLink
itself.
extension View {
@ViewBuilder
func listItemLink<Destination: View>(_ destination: Destination) -> some View {
if #available(iOS 14, *) {
NavigationLink(destination: destination) {
self
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ZStack {
NavigationLink(destination: destination) {
EmptyView()
}
.opacity(0)
self
}
}
}
}
You may not need
frame(maxWidth:,maxHeight)
but I noticed some scroll performance issues when usingNavigationLink
insideLazyVStack
without it.
Customing a list in SwiftUI on iOS13 is far beyond from simple. On the other hand starting the use of LazyVStack
on iOS14 will make it painless. But it will come at the cost of some tradeoffs to support both platforms.