I am working on a small macOS app using SwiftUI. I struggled to get List
working as I wanted to. TIL that List
works better on iOS :)
Out-of-the-box | Goal |
I thought about using ScrollView
and a LazyVStack
to emulate a list, but I missed out on specific functionality, especially the option to move items in a list.
List {
ForEach(items, id: \.id) { entry in
HStack {
Text(entry.text)
.lineLimit(2)
Spacer()
Button {
deleteItem(entry)
} label: {
Image(systemName: "trash")
}
}
}
.onMove { indices, destination in
// TODO: update items array accordingly
}
}
If you do not want to use List
, I recommend reading the article from Avi Tsadok for inspiration. He wrote about reordering items in a LazyVStack
in iOS apps by replicating UIKit Drag and Drop in SwiftUI.
%[avitsadok.medium.com/reorder-items-in-swift..
As you can see in the screenshot above, I had to solve two issues:
- Background color of the list
- Divider
Background color
Super helpful was this answer to the following Stack Overflow question.
I chose to use this extension to override List
's color:
extension NSTableView {
open override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
backgroundColor = NSColor.clear
enclosingScrollView!.drawsBackground = false
}
}
This approach was ok in my case as my app has only one list. An alternative approach is to remove a list's background (without affecting the whole app!) by using Introspect. Introspect allows you to get the underlying UIKit or AppKit element of a SwiftUI view for styling.
Divider
Stack Overflow came to my rescue again.
The divider should really be provided by the List automatically as it does on iOS but unfortunately is not as of macOS 12.3.
The padding around each row in a List seems to be there to account for the selection highlight. So if we want to provide our own Divider but want it to look native while accounting for the highlight padding we would need to offset the Divider like so:
struct ContentView: View {
@Binding var selection: Set<Item>
var body: some View {
List(selection: $selection) {
SomeView()
.swiftyListDivider()
}
}
}
import SwiftUI
extension View {
func swiftyListDivider() -> some View {
background(Divider().offset(y: 4.0), alignment: .bottom)
}
}
This approach was 'good enough' even though this does not precisely replicate the Divider behavior like other apps.