Last active
February 11, 2022 00:13
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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? | |
} | |
} | |
} |
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
How this works:
_UIHostingView
._UIHostingView
in order to gain access to thehost
property.host
property of_UIHostingView
refers to aUIView
orUIViewController
, in our case we know it is aUIView
.UITableViewCellContentView
, so we superview one more time intoUITableViewCell
.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 thelayoutMargin
, which appears to move the insets for us. This should be stable so long asList
remains backed byUITableView
, I'd also venture to guess this should work on iOS 14 too, which I think is when the bulk of theList
refactor occurred.My suspicion for why it does not work in SidebarStyle'd Lists is because they are backed by
UICollectionView
rather thanUITableView
.