Search code examples
iosswiftswiftuilazyvstack

LazyVStack Initializes all views when one changes SwiftUI


I have a LazyVStack which I would like to only update one view and not have all others on screen reload. With more complex cells this causes a big performance hit. I have included sample code

import SwiftUI

struct ContentView: View {
    @State var items = [String]()
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(self.items, id: \.self) { item in
                    Button {
                        if let index = self.items.firstIndex(where: {$0 == item}) {
                            self.items[index] = "changed \(index)"
                        }
                    } label: {
                        cell(text: item)
                    }
                }
            }
        }
        .onAppear {
            for _ in 0...200 {
                self.items.append(NSUUID().uuidString)
            }
        }
    }
}
struct cell: View {
    let text: String
    init(text: String) {
        self.text = text
        print("init cell", text)
    }
    var body: some View {
        Text(text)
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

As you can see even when only changing 1 cell the init gets called for every cell. Is there anyway to avoid this?


Solution

  • Here is a working code, there is some points to mention, View in SwiftUI would get initialized here and there or anytime SwiftUI thinks it needed! But the body of View would get computed if really some value in body changed. It is planed to work like that, there is some exceptions as well. Like body get computed even the values that used in the body were as before with no change, I do not want inter to that topic! But in your example and in your issue, we want SwiftUI renders only the changed View, for this goal the down code works well without issue, but as you can see I used VStack, if we change VStack to LazyVStack, SwiftUI would renders some extra view due its undercover codes, and if you scroll to down and then to up, it would forget all rendered view and data in memory and it will try to render the old rendered views, so it is the nature of LazyVStack, we cannot do much about it. Apple want LazyVStack be Lazy. But you can see that LazyVStack would not render all views, but some of them that needs to works. we cannot say or know how much views get rendered in Lazy way, but for sure not all of them.

    let initializingArray: () -> [String] = {
        var items: [String] = [String]()
    
        for _ in 0...200 { items.append(UUID().uuidString) }
    
        return items
    }
    
    
    struct ContentView: View {
    
        @State var items: [String] = initializingArray()
        
        var body: some View {
            ScrollView {
                VStack {
                    ForEach(items, id: \.self) { item in
    
                        Button(action: {
                            
                            if let index = self.items.firstIndex(where: { $0 == item }) {
                                items[index] = "changed \(index)"
                            }
                            
                        }, label: {
                            ItemView(item: item)
                        })
                        
                    }
                }
            }
    
        }
    }
    
    struct ItemView: View {
        
        let item: String
        
        var body: some View {
            print("Rendering row for:", item)
            return Text(item)
            
        }
    }