Search code examples
iosswiftswiftuiswiftui-list

Scroll List to a row in SwiftUI


I implemented a list control. I want to scroll the row to center when it is selected. My row contains one image and two texts. Below is my code.

var body: some View {
    ScrollViewReader { scrollView in
        List(videos, id: \.id) { video in
            VStack(alignment: .leading) {
                WebImage(url: URL(string: video.thumbnailURL))
                    .resizable()
                    .placeholder {
                        Rectangle().foregroundColor(.clear)
                    }
                    .transition(.fade(duration: 0.5))
                    .aspectRatio(contentMode: .fit)
                    .frame(minHeight:100.0, alignment: .center)
                Text(video.title)
                    .foregroundColor(.white)
                    .padding(.horizontal, 5)
                    .lineLimit(1)
                    .font(Font.custom("Montserrat Bold", size: 14))
                Text(video.artist)
                    .foregroundColor(.white)
                    .padding(.horizontal, 5)
                    .lineLimit(1)
                    .font(Font.custom("Montserrat Medium", size: 14))
                    .padding(.bottom, 5)
                
            }.background(Color.black)
                .listRowBackground(Color.clear)
                .onTapGesture {
                    selectedVideoId = video.id
                    delegate?.videoSelected(video: video)
                }
                .border((selectedVideoId == video.id) ? Color(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003) : Color.clear, width: (selectedVideoId == video.id) ? 1.5 : 0.0)
            
        }.padding(.horizontal, 20.0)
            .padding(.bottom, 10.0)
            .background(Color.clear)
            .listStyle(.plain)
            .onChange(of: selectedVideoId) { vid in
                scrollView.scrollTo(1, anchor: .top)
            }
    }
}

The .onChange(of: selectedVideoId) closure gets called when a row is selected. But it doesn't scroll to the first row.


Solution

  • ScrollViewReader works with the id of your model items. You should not use the key-path-based initializer id: \.id.

    Conform your model type to Identifiable:

    struct Video: Identifiable {
        let id: UUID = UUID()
        let title: String
        let artist: String
        // [...] whatever else you have here
    }
    

    Use your Identifiable model items, assign the id to each view & scrollTo(selectedVideoId):

    var body: some View {
            VStack {
                ScrollViewReader { scrollView in
                    List(v.videos) { video in // << use id here
                        VStack(alignment: .leading) {
                            WebImage(url: URL(string: video.thumbnailURL))
                                .resizable()
                                .placeholder {
                                    Rectangle().foregroundColor(.clear)
                                }
                                .transition(.fade(duration: 0.5))
                                .aspectRatio(contentMode: .fit)
                                .frame(minHeight:100.0, alignment: .center)
                            Text(video.title)
                                .foregroundColor(.white)
                                .padding(.horizontal, 5)
                                .lineLimit(1)
                                .font(Font.custom("Montserrat Bold", size: 14))
                            Text(video.artist)
                                .foregroundColor(.white)
                                .padding(.horizontal, 5)
                                .lineLimit(1)
                                .font(Font.custom("Montserrat Medium", size: 14))
                                .padding(.bottom, 5)
                        }
                        .id(video.id) // << assign the id to each view
                        .background(Color.black)
                        .listRowBackground(Color.clear)
                        .onTapGesture {
                            selectedVideoId = video.id
                            delegate?.videoSelected(video: video)
                        }
                        .border((selectedVideoId == video.id) ? Color(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003) : Color.clear, width: (selectedVideoId == video.id) ? 1.5 : 0.0)
                        
                    }
                    .padding(.horizontal, 20.0)
                    .padding(.bottom, 10.0)
                    .background(Color.clear)
                    .listStyle(.plain)
                    .onChange(of: selectedVideoId) { vid in
                        print("new selected: \(selectedVideoId)")
                        withAnimation {
                            scrollView.scrollTo(selectedVideoId, anchor: .top) 
                            // << scroll to id
                        }
                    }
                }
            }
            .padding()
        }