26

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
  • 323
  • 1
  • 3
  • 9
  • 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

3 Answers3

26

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
  • 577
  • 4
  • 14
  • 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
23

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
  • 8,657
  • 7
  • 36
  • 57
multitudes
  • 2,223
  • 1
  • 15
  • 27
  • 1
    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
9

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
  • 17,608
  • 6
  • 57
  • 80