52

My problem is that i have (in SwiftUI) a ScrollView with an foreach inside. Know when the foreach loads all of my entries i want that the last entry is focused.

I did some google research, but i didn't find any answer.

  ScrollView {
    VStack {
      ForEach (0..<self.entries.count) { index in
        Group {
          Text(self.entries[index].getName())
        }
      }
    }
  }
Leahpar
  • 583
  • 1
  • 4
  • 12
  • 2
    Possible duplicate of [How to make a SwiftUI List scroll automatically?](https://stackoverflow.com/questions/57258846/how-to-make-a-swiftui-list-scroll-automatically) – LuLuGaGa Oct 14 '19 at 16:00
  • 1
    I think this guy has a solution for you https://github.com/mremond/SwiftUI-ScrollView-Demo . I'll try this tomorrow. – gujci Oct 14 '19 at 20:31

6 Answers6

40

Scrolling to the bottom on change

I don't have enough reputation to post a comment yet, so here you go @Dam and @Evert

To scroll to the bottom whenever the number of entries in your ForEach changes you can also use the same method with a ScrollViewReader, as mentioned in the answer above, by adding the view modifier onChange like so:

struct ContentView: View {
    let colors: [Color] = [.red, .blue, .green]
    var entries: [Entry] = Array(repeating: Entry(), count: 10)

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                            
                ForEach(0..<entries.count) { i in
                    Text(self.entries[i].getName())
                        .frame(width: 300, height: 200)
                        .background(colors[i % colors.count])
                        .padding(.all, 20)
                }
                .onChange(of: entries.count) { _ in
                    value.scrollTo(entries.count - 1)
                }
            }
        }
    }
}
noranraskin
  • 1,127
  • 8
  • 17
  • Maybe you don't have (had) enough reputation but you deserve(d) to got the medal. Very nice solution avoiding to have an array of elements confirming to Hashable (my array is [AnyView]. – Luc-Olivier May 19 '21 at 19:14
35

According to Apple's documentation on ScrollViewReader, it should wrap the scroll view, instead of being nested in.

Example from Apple docs:

@Namespace var topID
@Namespace var bottomID

var body: some View {
    ScrollViewReader { proxy in
        ScrollView {
            Button("Scroll to Bottom") {
                withAnimation {
                    proxy.scrollTo(bottomID)
                }
            }
            .id(topID)

            VStack(spacing: 0) {
                ForEach(0..<100) { i in
                    color(fraction: Double(i) / 100)
                        .frame(height: 32)
                }
            }

            Button("Top") {
                withAnimation {
                    proxy.scrollTo(topID)
                }
            }
            .id(bottomID)
        }
    }
}

func color(fraction: Double) -> Color {
    Color(red: fraction, green: 1 - fraction, blue: 0.5)
}

This other article also shows how to use ScrollViewReader to wrap a List of elements, which can be also handy. Code example from the article:

ScrollViewReader { scrollView in
    List {
        ForEach(photos.indices) { index in
            Image(photos[index])
                .resizable()
                .scaledToFill()
                .cornerRadius(25)
                .id(index)
        }
    }
 
    .
    .
    .
}

For me, using value.scrollTo(entries.count - 1) does not work. Furthermore, using value.scrollTo(entries.last?.id) didn't work either until I used the .id(...) view modifier (maybe because my entries do not conform to Identifiable).

Using ForEach, here is the code I'm working on:

ScrollViewReader { value in
    ScrollView {
        ForEach(state.messages, id: \.messageId) { message in
            MessageView(message: message)
                .id(message.messageId)
        }
    }
    .onAppear {
        value.scrollTo(state.messages.last?.messageId)
    }
    .onChange(of: state.messages.count) { _ in
        value.scrollTo(state.messages.last?.messageId)
    }
}

One important consideration when using onAppear is to use that modifier on the ScrollView/List and not in the ForEach. Doing so in the ForEach will cause it to trigger as elements on the list appear when manually scrolling through it.

Eneko Alonso
  • 18,884
  • 9
  • 62
  • 84
  • 1
    This should be the correct answer for Xcode 13.4.1 and iOS 15.5 - (1) The Reader wrapping the ScrollView. (2) The key element overlooked in other answers is the use of .id(message.id)... (3) in my case, the .onAppear {} is not needed – eharo2 Jun 10 '22 at 02:35
  • 1
    Can confirm this, it will only scroll if the view is tagged with .id() modifier – YosefAhab Jul 09 '22 at 16:40
  • When I first came across posts about ScrollViewReader, I was confused why the ScrollView was the outer parent instead of the reader. Thank you for confirming this. – chitgoks Nov 16 '22 at 04:00
26

Solution for iOS14 with ScrollViewReader

in iOS14 SwiftUI gains a new ability. To be able to scroll in a ScrollView up to a particular item with a given id. There is a new type called ScrollViewReader which works just like Geometry Reader.

The code below will scroll to the last item in your View.

So this is your struct for 'Entry' I guess:

struct Entry {
    let id = UUID()
    
    func getName() -> String {
         return "Entry with id \(id.uuidString)"
    }
}

And the main ContentView:

struct ContentView: View {
    var entries: [Entry] = Array(repeating: Entry(), count: 10)

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                ForEach(entries, id: \.id) { entry in
                    Text(entry.getName())
                        .frame(width: 300, height: 200)
                        .padding(.all, 20)
                }
                .onAppear {
                    value.scrollTo(entries.last?.id, anchor: .center)
                }
            }
        }
    }
}

Try to run this in the new version of SwiftUI announced at WWDC20. I think it is a great enhancement.

Lucas van Dongen
  • 9,328
  • 7
  • 39
  • 60
multitudes
  • 2,898
  • 2
  • 22
  • 29
  • 4
    Hey, do you have any idea on how to make it scroll every time a new entry is added? I'm making a chat app and I'd like it to scroll to the bottom every time a new message arrives. – Evert Jul 23 '20 at 09:28
  • 1
    I have the same problem, when I open the view, the ForEach is empty, and when I add content, it doesn't scroll. Is that possible with an horizontal ScrollView? – Dam Oct 06 '20 at 10:15
  • @Dam you need to explore View.onPreferenceChange – Arutyun Enfendzhyan Jan 17 '21 at 11:45
6

If you have only ONE VIEW as the child view in your ScrollView.

All solutions provided here assume to have multiple child views, each with an id. But what if you have only one child, e.g. a multiline Text, that grows over time and you want to have this automatic scroll feature to the bottom?

You then simply can add an identifier (in my case "1"):

VStack {
   ScrollViewReader { proxy in
       ScrollView {
           Text(historyText)
               .id(1)            // this is where to add an id
               .multilineTextAlignment(.leading)
               .padding()
       }
       .onChange(of: historyText) { _ in
            proxy.scrollTo(1, anchor: .bottom)
       }
    }
 }
LukeSideWalker
  • 7,399
  • 2
  • 37
  • 45
4

I imagine we are all making chat apps here!

This is how I ended up doing, mostly inspired by answers here with a combo twist (after lots of trying)

struct ChatItemListView: View {
    
    @StateObject var lesson: Lesson

    var body: some View {
        
        ScrollViewReader { scrollViewProxy in

            List(lesson.safeChatItems) { chatItem in
                ChatItemRowView(chatItem: chatItem).id(chatItem.id)
            }
            .onReceive(lesson.objectWillChange) { _ in
                guard !lesson.chatItems.isEmpty else { return }
                Task {
                    scrollViewProxy.scrollTo(
                        lesson.chatItems.last!.id, 
                        anchor: .top
                    )
                }
            }

        }
    }
}

The two important details here are:

  • force passing the id to the view via .id(chatItem.id)
  • putting the scrollTo action into a Task

Without those, it did not work, even if my chatItem implements Identifiable protocol.

jrmgx
  • 719
  • 8
  • 24
3

Add a textview with empty string and an id at the bottom of scroll view then use scrollTo to scroll to this view whenever something changes. Here is the code

struct ContentView: View {
@Namespace var bottomID
@State var data : [Int] = []

init(){
    var temp : [Int] = []
    for i in 0...100 {
        temp.append(i)
    }
    _data = .init(initialValue: temp)
}

var body: some View {
    
    ScrollViewReader { value in
        ScrollView {
        
            ForEach(data, id: \.self) { i in
                Text("\(i)")
            }
            
            // Empty text view with id
            Text("").id(bottomID)
        }
        .onAppear{
            withAnimation{
                // scrolling to the text value present at bottom
                value.scrollTo(bottomID)
            }
        }
    }
}

}

lastbreath
  • 383
  • 1
  • 11