62

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

8 Answers 8

56

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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
Can confirm this, it will only scroll if the view is tagged with .id() modifier
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.
This did the trick for me. Adding explicit ID's and then scrolling to the last one. Thanks!!
47

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

2 Comments

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].
Simplest so the best answer as don't need to play with the id or anchor. Works with horizontal ScrollView of course. I didn't notice any difference but maybe better to follow Apple documentation and take out the ScrollViewReader from nested position to wrapping.
28

There is a feature that comes with iOS 17: defaultScrollAnchor(_:)

ScrollView {
    ForEach(0..<50) { i in
        Text("Item \(i)")
            .frame(maxWidth: .infinity)
            .padding()
            .background(.blue)
            .clipShape(.rect(cornerRadius: 25))
    }
}
.defaultScrollAnchor(.bottom)

4 Comments

People need to vote and make this go higher because this is the right answer for xcode 15.4 as well. Thank you, Burak!
this is not handy for chat conversation view, i was happy with it, but when keyboard opens or Textfield expands it doesnt work properly
This is a nice simply soltion, but for me it didn't work bc when there were just a few items I wanted them to be top aligned. Only when enough content filled the scroll view did I want it to start bottom up scrolling. The ScrollViewReader.scrollTo combined with my top alignment in swiftui solved my particular use case.
@VaibhavDwivedi I vote down because currently 4% of users use iOS 16. In May 2024 this number was more than 12% but you suggest just to drop their support.
27

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.

3 Comments

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.
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 you need to explore View.onPreferenceChange
11

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

Comments

6

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

}

Comments

6

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.

1 Comment

Can you please provide explanation as to why this solution works and other solutions didn't?
0

Here is the example, make sure to set the anchor then you can achieve your behavior easily

 import SwiftUI

  struct ContentView: View {

    @Namespace private var topID
    @Namespace private var bottomID
    
    var body: some View {
        VStack(spacing: 20) {
            HStack {
                Button("Scroll to Top") {
                    withAnimation {
                        proxy.scrollTo(topID, anchor: .top)
                    }
                }
                
                Button("Scroll to Bottom") {
                    withAnimation {
                        proxy.scrollTo(bottomID, anchor: .bottom)
                    }
                }
            }
            
            ScrollViewReader { proxy in
                ScrollView {
                    VStack(spacing: 20) {
                        Text("Top Text")
                            .id(topID)
                            .background(Color.yellow)
                            .padding()
                        
                        ForEach(0..<30) { i in
                            Text("Dynamic Text \(i)")
                                .padding()
                                .background(i % 2 == 0 ? Color.blue : Color.green)
                                .foregroundColor(.white)
                        }
                        
                        Text("Bottom Text")
                            .id(bottomID)
                            .background(Color.red)
                            .padding()
                    }
                }
            }
        }
        .padding()
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.