I have a list with a ForEach loop for items. I would like to place a button on the side of the list that would pull the list to the top if the list is not currently at the top.
The key is that the button should only be visible if the list is currently not at the top position, therefore, I need some way to get the position of the list or track the top element in ForEach loop to know when it is in view to trigger the show button event.
CodePudding user response:
I believe the following gives the behaviour you are looking for. Note that I took the liberty of disabling the "Scroll to Top" button, rather than hiding it, because the hiding causes layout changes that are troublesome.
The gist of it is to use GeometryReaders to compare the top of the list (in the global coordinate system) to the bottom of the first list entry (in the same coordinate system) and enable the button when the list entry is entirely above the top of the list.
The line of the form let _ = { ... }()
allows us to inject some code into the declarative code structure SwiftUI uses.
The listProxy.scrollTo(1)
call will cause the list to scroll to the position that makes the list item marked with .id(1)
.
struct ScrollButtonTest: View {
@State private var listTop: CGFloat = 0.0
@State private var firstRowBottom: CGFloat = 0.0
var body: some View {
VStack {
ScrollViewReader { listProxy in
Text("listTop: \(listTop)")
Text("firstRowBottom: \(firstRowBottom)")
Button("Scroll to Top") {
listProxy.scrollTo(1)
}.disabled(listTop < firstRowBottom )
GeometryReader { outerGP in
List {
// We set up the first item manually so that
// we can wrap it in a GeometryReader
GeometryReader { innerGP in
let _ = {
listTop = outerGP.frame(in: .global).origin.y
firstRowBottom = innerGP.frame(in: .global).origin.y innerGP.frame(in: .global).height
}()
Text("Item 1")
}.id(1)
ForEach(2..<99) { index in
Text("Item \(index)").id(index)
}
}
}
}
}
}
}
CodePudding user response:
You can use enumerated()
along with ForEach
to track an index of the array and use it to hide and show your button. Something like this.
struct ContentView: View {
let items = (1...50).map(String.init)
@State private var showScrollButton = false
var body: some View {
ScrollViewReader { reader in
ZStack(alignment: .bottomTrailing) {
List {
let enumerated = items.enumerated().map({ $0 })
ForEach(enumerated, id: \.element) { (index, item) in
Text(item)
.id(index)
.onAppear {
guard index == 0, showScrollButton else {
return
}
showScrollButton = false
}
.onDisappear {
guard index == 0, !showScrollButton else {
return
}
showScrollButton = true
}
}
}
if showScrollButton {
Button(action: {
withAnimation {
reader.scrollTo(0, anchor: .top)
}
}, label: {
Image(systemName: "chevron.up.circle")
.font(.largeTitle)
.foregroundColor(.black)
.padding()
.padding(.trailing)
})
}
}
}
}
}