Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to scroll List programmatically in SwiftUI?

Tags:

ios

swift

swiftui

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?

  • scroll List to end
  • scroll List to top
  • and others

(I don't like wrapping full UITableView infrastructure into UIViewRepresentable/UIViewControllerRepresentable as was proposed earlier on SO).

like image 605
Asperi Avatar asked Mar 25 '20 19:03

Asperi


People also ask

How do I programmatically scroll in SwiftUI?

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.


Video Answer


2 Answers

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

demo2

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                         )                 }             }         }     } } 

demo

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     } } 
like image 168
Asperi Avatar answered Sep 21 '22 02:09

Asperi


Just scroll to the id:

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


Preview

preview

like image 33
Mojtaba Hosseini Avatar answered Sep 22 '22 02:09

Mojtaba Hosseini