Skip to content

Instantly share code, notes, and snippets.

@ericlewis
Last active February 11, 2022 00:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericlewis/5898a13bab5c80f65bce92cb89109ca7 to your computer and use it in GitHub Desktop.
Save ericlewis/5898a13bab5c80f65bce92cb89109ca7 to your computer and use it in GitHub Desktop.
Customize the separator insets on lists in SwiftUI. Only tested on iOS 15. Does not work on SidebarListStyles or interface idioms that aren't pad / phone.
struct ContentView: View {
var body: some View {
List {
ForEach(0..<100) { index in
Text("Index: \(index)")
.listRowSeparatorInsets(
.init(
top: 0,
left: CGFloat(index) * 5,
bottom: 0,
right: 0
)
)
}
}
.listStyle(.insetGrouped)
}
}
extension View {
/// The top and bottom insets are ignored
public func listRowSeparatorInsets(_ insets: UIEdgeInsets) -> some View {
self.modifier(ListRowSeparatorInsetsModifier(insets: insets))
}
}
struct ListRowSeparatorInsetsModifier: ViewModifier {
let insets: UIEdgeInsets
func body(content: Content) -> some View {
content.background {
Representable(insets: insets)
.hidden()
}
}
struct Representable: UIViewRepresentable {
// Forces layoutMargins to take effect.
private static let paddingInset = UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 0)
let insets: UIEdgeInsets
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> some UIView { .init() }
func updateUIView(_ uiView: UIViewType, context: Context) {
guard let cell = context.coordinator.cellCache else {
guard let superview = uiView.superview,
// TODO: a better reflection solution, would be nice to grab the host directly
let host = Mirror(reflecting: superview).descendant("host", "some") as? UIView,
let cell = host.superview?.superview as? UITableViewCell else {
return
}
updateInsets(cell)
context.coordinator.cellCache = cell
return
}
updateInsets(cell)
context.coordinator.cellCache = cell
}
func updateInsets(_ cell: UITableViewCell) {
guard insets != cell.layoutMargins else { return }
if cell.separatorInset != Self.paddingInset {
cell.separatorInset = Self.paddingInset
}
cell.layoutMargins = insets
}
class Coordinator {
var cellCache: UITableViewCell?
}
}
}
@ericlewis
Copy link
Author

ericlewis commented Feb 10, 2022

How this works:

  1. We grab our background views superview which is the underlying _UIHostingView.
  2. We reflect over the _UIHostingView in order to gain access to the host property.
  3. The host property of _UIHostingView refers to a UIView or UIViewController, in our case we know it is a UIView.
  4. Our hosting view happens to be UITableViewCellContentView, so we superview one more time into UITableViewCell.

As for why you need to set separatorInset to a non-zero value is beyond me. It doesn't work for changing the leading inset either, so we adjust the layoutMargin, which appears to move the insets for us. This should be stable so long as List remains backed by UITableView, I'd also venture to guess this should work on iOS 14 too, which I think is when the bulk of the List refactor occurred.

My suspicion for why it does not work in SidebarStyle'd Lists is because they are backed by UICollectionView rather than UITableView.

@ericlewis
Copy link
Author

Oh, I bet the reason the insets need to be some non-zero amount is because they’re being applied to the table view. So when we set it to something non-zero we are taking control from the table view.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment