It looks like in current tools/system, just released Xcode 11.4 / iOS 13.4, there will be no SwiftUI-native support for "scroll-to" feature in List
. So even if they, Apple, will provide it in next major released, I will need backward support for iOS 13.x.
So how would I do it in most simple & light way?
(I don't like wrapping full UITableView
infrastructure into UIViewRepresentable/UIViewControllerRepresentable
as was proposed earlier on SO).
If you want to programmatically make SwiftUI's ScrollView move to a specific location, you should embed a ScrollViewReader inside it. This provides a scrollTo() method that can move to any view inside the parent scrollview, just by providing its anchor.
SWIFTUI 2.0
Here is possible alternate solution in Xcode 12 / iOS 14 (SwiftUI 2.0) that can be used in same scenario when controls for scrolling is outside of scrolling area (because SwiftUI2 ScrollViewReader
can be used only inside ScrollView
)
Note: Row content design is out of consideration scope
Tested with Xcode 12b / iOS 14
class ScrollToModel: ObservableObject { enum Action { case end case top } @Published var direction: Action? = nil } struct ContentView: View { @StateObject var vm = ScrollToModel() let items = (0..<200).map { $0 } var body: some View { VStack { HStack { Button(action: { vm.direction = .top }) { // < here Image(systemName: "arrow.up.to.line") .padding(.horizontal) } Button(action: { vm.direction = .end }) { // << here Image(systemName: "arrow.down.to.line") .padding(.horizontal) } } Divider() ScrollViewReader { sp in ScrollView { LazyVStack { ForEach(items, id: \.self) { item in VStack(alignment: .leading) { Text("Item \(item)").id(item) Divider() }.frame(maxWidth: .infinity).padding(.horizontal) } }.onReceive(vm.$direction) { action in guard !items.isEmpty else { return } withAnimation { switch action { case .top: sp.scrollTo(items.first!, anchor: .top) case .end: sp.scrollTo(items.last!, anchor: .bottom) default: return } } } } } } } }
SWIFTUI 1.0+
Here is simplified variant of approach that works, looks appropriate, and takes a couple of screens code.
Tested with Xcode 11.2+ / iOS 13.2+ (also with Xcode 12b / iOS 14)
Demo of usage:
struct ContentView: View { private let scrollingProxy = ListScrollingProxy() // proxy helper var body: some View { VStack { HStack { Button(action: { self.scrollingProxy.scrollTo(.top) }) { // < here Image(systemName: "arrow.up.to.line") .padding(.horizontal) } Button(action: { self.scrollingProxy.scrollTo(.end) }) { // << here Image(systemName: "arrow.down.to.line") .padding(.horizontal) } } Divider() List { ForEach(0 ..< 200) { i in Text("Item \(i)") .background( ListScrollingHelper(proxy: self.scrollingProxy) // injection ) } } } } }
Solution:
Light view representable being injected into List
gives access to UIKit's view hierarchy. As List
reuses rows there are no more values then fit rows into screen.
struct ListScrollingHelper: UIViewRepresentable { let proxy: ListScrollingProxy // reference type func makeUIView(context: Context) -> UIView { return UIView() // managed by SwiftUI, no overloads } func updateUIView(_ uiView: UIView, context: Context) { proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy } }
Simple proxy that finds enclosing UIScrollView
(needed to do once) and then redirects needed "scroll-to" actions to that stored scrollview
class ListScrollingProxy { enum Action { case end case top case point(point: CGPoint) // << bonus !! } private var scrollView: UIScrollView? func catchScrollView(for view: UIView) { if nil == scrollView { scrollView = view.enclosingScrollView() } } func scrollTo(_ action: Action) { if let scroller = scrollView { var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1)) switch action { case .end: rect.origin.y = scroller.contentSize.height + scroller.contentInset.bottom + scroller.contentInset.top - 1 case .point(let point): rect.origin.y = point.y default: { // default goes to top }() } scroller.scrollRectToVisible(rect, animated: true) } } } extension UIView { func enclosingScrollView() -> UIScrollView? { var next: UIView? = self repeat { next = next?.superview if let scrollview = next as? UIScrollView { return scrollview } } while next != nil return nil } }
scrollView.scrollTo(ROW-ID)
Since SwiftUI structured designed Data-Driven, You should know all of your items IDs. So you can scroll to any id with ScrollViewReader
from iOS 14 and with Xcode 12
struct ContentView: View { let items = (1...100) var body: some View { ScrollViewReader { scrollProxy in ScrollView { ForEach(items, id: \.self) { Text("\($0)"); Divider() } } HStack { Button("First!") { withAnimation { scrollProxy.scrollTo(items.first!) } } Button("Any!") { withAnimation { scrollProxy.scrollTo(50) } } Button("Last!") { withAnimation { scrollProxy.scrollTo(items.last!) } } } } } }
Note that ScrollViewReader
should support all scrollable content, but now it only supports ScrollView
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With