How to scroll List programmatically in SwiftUI?
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
}
}
backup
How can I programmatically scroll in SwiftUI?
It's not the view model's responsibility to scroll, but what you can do is add a selectedIndex
property to your view model, and observe and react to the change.
var body: some View {
ScrollView {
ScrollViewReader { pageScroller in
ForEach(0..<100) { i in
Text("Example \(i)")
.frame(width: 200, height: 200)
.background(colors[i % colors.count])
.id(i)
}
.onChange(of: viewModel.selectedIndex) { newIndex in
pageScroller.scrollTo(newIndex, anchor: .top)
}
}
}
.frame(height: 350)
}
Since you mentioned that your content is loaded asynchronously, you can probably update the selectedIndex
property once everything is loaded.
Scroll to specific section and item in SwiftUI list
scrollTo(_:, anchor:)
goes to a view with a specific ID. You aren't assigning any IDs to your views, so presumably you're accidentally taking advantage of some default. Make a specific ID for each day in each month (e.g. January-01 ... December-31) and assign that to each calendar row:
CalendarRow(...).id("June-09")
How to make a SwiftUI List scroll automatically?
Update: In iOS 14 there is now a native way to do this.
I am doing it as such
ScrollViewReader { scrollView in
ScrollView(.vertical) {
LazyVStack {
ForEach(notes, id: \.self) { note in
MessageView(note: note)
}
}
.onAppear {
scrollView.scrollTo(notes[notes.endIndex - 1])
}
}
}
For iOS 13 and below you can try:
I found that flipping the views seemed to work quite nicely for me. This starts the ScrollView at the bottom and when adding new data to it automatically scrolls the view down.
- Rotate the outermost view 180
.rotationEffect(.radians(.pi))
- Flip it across the vertical plane
.scaleEffect(x: -1, y: 1, anchor: .center)
You will have to do this to your inner views as well, as now they will all be rotated and flipped. To flip them back do the same thing above.
If you need this many places it might be worth having a custom view for this.
You can try something like the following:
List(chatController.messages, id: \.self) { message in
MessageView(message.text, message.isMe)
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
Here's a View extension to flip it
extension View {
public func flip() -> some View {
return self
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
}
SwiftUI ScrollView programmatically scroll to top based on change of dropdown
You can use ScrollViewReader
(docs & HWS article).
For your use case, we need to save the ScrollViewProxy
in a @State
variable so it can be accessed outside of the closure. Use it like so:
struct ContentView: View {
@State private var reader: ScrollViewProxy?
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1 ... 7, id: \.self) { num in
Text("\(num)")
.font(.title3)
.padding([.top, .bottom], 5)
.onTapGesture {
self.getGames()
print("Tap:", num)
// Scroll to tag 'GameCell-top'
reader?.scrollTo("GameCell-top", anchor: .top)
}
}
}
}
ScrollView {
ScrollViewReader { reader in
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
.id("GameCell-top") // <- ID so reader works
.tag("GameCell-top") // <- Tag view for reader
.onAppear {
self.reader = reader // <- Set current reader proxy
}
// You can uncomment this view and do a similar thing to the VStack above
// GameCell(games: $games, picks: self.$playerPicks)
}
}
.onAppear(perform: getGames)
}
}
}
Result (I scroll bottom scroll view, then tap any number from 1 to 7 at the top):
Scroll SwiftUI List to new selection
In the new relase of SwiftUI for iOs 14 and MacOs Big Sur they added the ability to programmatically scroll to a specific cell using the new ScrollViewReader:
struct ContentView: View {
let colors: [Color] = [.red, .green, .blue]
var body: some View {
ScrollView {
ScrollViewReader { value in
Button("Jump to #8") {
value.scrollTo(8)
}
ForEach(0..<10) { i in
Text("Example \(i)")
.frame(width: 300, height: 300)
.background(colors[i % colors.count])
.id(i)
}
}
}
}
}
Then you can use the method .scrollTo()
like this
value.scrollTo(8, anchor: .top)
Credit: www.hackingwithswift.com
Related Topics
How to Add Uitableview Within a Uitableviewcell
Starting Iphone App Development in Linux
How to Apply Gradient to Background View of iOS Swift App
How to Get Current Location from User in Ios
Uploading File With Parameters Using Alamofire
Returning Method Object from Inside Block
What Is "Constrain to Margin" in Storyboard in Xcode 6
Uilabel Sizetofit Doesn't Work With Autolayout Ios6
How to Detect First Time App Launch on an Iphone
How to Change the Background Color of a Uibutton While It's Highlighted
How to Resize the Uiimage to Reduce Upload Image Size
How to Trigger a Block After a Delay, Like -Performselector:Withobject:Afterdelay:
Uialertview Addsubview in Ios7
Having a Uitextfield in a Uitableviewcell
Getting Error "No Such Module" Using Xcode, But the Framework Is There