How to refresh SwiftUI specific sub-view only?

I am trying to refresh only Section's body contents (Green List area) and not header view (filter buttons container) so list always start from beginning with scrollbar offset starting at zero.

I have assigned id UUID() to parent vertical ScrollView. This helps me fix scrollbar offset issue else on second time Refresh Data click, scrollbar offset doesn't go to top by default. [If I assign id to LazyVStack or Section's list content then scrollbar offset doesn't start from zero position if previous list scrolled to the bottom]

However, this solution refreshes entire scrollview causing all of it's sub-views getting refreshed instead of only green list where it loses other views original state as shows in following output gif:

Swiftui_Identity

My code is as follows:

//
//  ContentView.swift
//  UpdateListOnly
//
//  Created by Vishwanath Deshmukh on 1/15/22.
//

import SwiftUI

struct ContentView: View {
    @State private var array = ["Vish1", "Vish2", "Vish3", "Vish4", "Vish5", "Vish6", "Vish7", "Vish8", "Vish9", "Vish10", "Vish11", "Vish12", "Vish13", "Vish14", "Vish111", "Vish211", "Vish113", "Vish114", "Vish115", "Vish116", "Vish117", "Vish118", "Vish119", "Vish1110", "Vish1111", "Vish1112", "Vish1113", "Vish1114"]

    @State private var count = 1

    var body: some View {
        ZStack(alignment: .top) {
            VStack(spacing: 0) {

                Button(action: refreshData) {
                    Text("Refresh Data")
                }

                // Main vertical scrollview
                ScrollView(.vertical) {

                    // Child 1: horizontal ScrollView
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack {
                            ForEach(1..<10) { index in
                                Text("\(index)")
                                    .frame(width: 100, height: 100)
                                    .padding(.horizontal, 10)
                                    .background(Color.gray)
                            }
                        }
                    }

                    // Child 2:  Dynamic View 2: Pinned Section and body with list
                    LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                        Section(header: FilterContainer(),
                                content: {
                            // Section body list: (Need to refresh only this with scrollbar offset from zero)
                            VStack(spacing: 10) {
                                ForEach(Array(array.enumerated()), id: \.offset) { (index, itemDataModel) in
                                    Text("\(itemDataModel) - \(index)")
                                        .frame(height: 50)
                                        .frame(maxWidth: .infinity)
                                }
                            }
                            .frame(maxWidth: .infinity)
                            .background(Color.green)
                        })
                    }

                }.id(UUID())
            }

        }
    }


    private func refreshData() {
        count += 1
        if count % 2 == 0 {
                        array = ["XYZ_1", "XYZ_2", "XYZ_3", "XYZ_4", "XYZ_5", "XYZ_6", "XYZ_7", "XYZ_8"]
        } else {
            array = ["Vish1", "Vish2", "Vish3", "Vish4", "Vish5", "Vish6", "Vish7", "Vish8", "Vish9", "Vish10", "Vish11", "Vish12", "Vish13", "Vish14", "Vish111", "Vish211", "Vish113", "Vish114", "Vish115", "Vish116", "Vish117", "Vish118", "Vish119", "Vish1110", "Vish1111", "Vish1112", "Vish1113", "Vish1114"]
        }
        print(count)
    }
}


// Goes inside Section Header: Filters View
struct FilterContainer: View {
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            ScrollViewReader { proxy in
                HStack {
                    ForEach(1..<6) { count in
                        Button(action: {}, label: {
                            Text("Filter \(count)")
                        })
                            .padding()
                    }
                }
                .background(Color.white)
            }
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

First of all, I must note that you are using hack with .id(UUID()) incorrectly. Implementation of the View.body should not have side effects. You should be able to call View.body twice and get the same result. If you are generating UUID() inside the body, every time when body is called you will get a new value. You don't control when body is called, you control only how you change state. System may call body when you don't expect, and your scroll view will be recreated because of something which has nothing to do with data refresh. Correct way of doing this would be to store this UUID in @State and change that state inside refreshData().

But actually you don't need this hack at all. Starting from iOS 14 you can use ScrollViewReader and ScrollViewProxy:

Button(action: refreshData) {
    Text("Refresh Data")
}

// Main vertical scrollview
ScrollView(.vertical) {
    ScrollViewReader { proxy in
        // Child 1: horizontal ScrollView
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(1..<10) { index in
                    Text("\(index)")
                        .frame(width: 100, height: 100)
                        .padding(.horizontal, 10)
                        .background(Color.gray)
                }
            }
        }.id(1).onChange(of: count) { _ in
            proxy.scrollTo(1, anchor: .top)
        }

        // Child 2:  Dynamic View 2: Pinned Section and body with list
        LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
            ...
        }
    }
}

Note that the view we scrolling to needs to be inside the ScrollViewReader.

1 Like

Hi @Nickolas_Pohilets

As you suggested, I given id("ParentScrollView") to parent ScrollView and included it in within ScrollViewReader. Secondly, on every refresh, I want to scroll to the first row of list. This list is present inside LazyVStack's Section body.

This solution of scrolling to top adds some bottom padding on scrollTo() where shows second row instead of 1. Also, it mess up scrollview on second time reload where is appears screen is blank for small list and shows that small list only when scrolling to the top. This is because parent scrollview has not updated and end-up showing old scrollview height with scrollbar at bottom.

Output:
ScrollViewIssue

Changed code:

var body: some View {
        ZStack(alignment: .top) {
            VStack(spacing: 0) {

                Button(action: refreshData) {
                    Text("Refresh Data")
                }

                // Main vertical scrollview
                ScrollViewReader { proxy in

                    ScrollView(.vertical) {

                        // Child 1: horizontal ScrollView
                        ScrollView(.horizontal, showsIndicators: false) {
                            HStack {
                                ForEach(1..<10) { index in
                                    Text("\(index)")
                                        .frame(width: 100, height: 100)
                                        .padding(.horizontal, 10)
                                        .background(Color.gray)
                                }
                            }
                        }

                        // Child 2:  Dynamic View 2: Pinned Section and body with list
                        LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                            Section(header: FilterContainer().id("FilterContainer"),
                                    content: {
                                // Section body list: (Need to refresh only this with scrollbar offset from zero)

                                Text("").id("topView")
                                VStack(spacing: 10) {
                                    ForEach(Array(array.enumerated()), id: \.offset) { (index, itemDataModel) in
                                        Text("\(itemDataModel) - \(index)")
                                            .frame(height: 50)
                                            .frame(maxWidth: .infinity)
                                    }
                                }
                                .onChange(of: count, perform: { newValue in
                                    proxy.scrollTo("topView", anchor: .top)
                                })
                                .frame(maxWidth: .infinity)
                                .background(Color.green)

                            })
                        }
                    }.id("ParentScrollView")
                }
            }

        }
    }


    private func refreshData() {
        count += 1
        if count % 2 == 0 {
            array = ["XYZ_1", "XYZ_2", "XYZ_3", "XYZ_4", "XYZ_5", "XYZ_6", "XYZ_7", "XYZ_8", "XYZ_9", "XYZ_10", "XYZ_11","XYZ_12", "XYZ_13", "XYZ_14", "XYZ_111", "XYZ_211", "XYZ_113",]
        } else {
            array = ["Vish1", "Vish2", "Vish3", "Vish4", "Vish5", "Vish6", "Vish7", "Vish8", "Vish9", "Vish10", "Vish11", "Vish12", "Vish13", "Vish14", "Vish111", "Vish211", "Vish113", "Vish114", "Vish115", "Vish116", "Vish117", "Vish118", "Vish119", "Vish1110", "Vish1111", "Vish1112", "Vish1113", "Vish1114"]
        }
        print(count)
    }

Looks like my above solution still not working correctly.

Thank you @Nickolas_Pohilets, as you suggested using ScrollViewReader to scroll at right place and assigning ID based on count variable helped me to refresh the sub-view.

Terms of Service

Privacy Policy

Cookie Policy