0

I have a very simple codes and I want keep it as much as possible simple, I am using a ForEach to render some simple Text, for understanding what is happening undercover I made a TextView to get notified each time this View get called by SwiftUI, unfortunately each time I add new element to my array, SwiftUI is going to render all array elements from begging to end, which I want and expecting it call TextView just for new element, So there is a way to defining an array of View/Text which would solve the issue, but that is over kill for such a simple work, I mean me and you would defiantly use ForEach in our projects, and we could use a simple Text inside ForEach or any other custom View, how we could solve this issue to stop SwiftUI initializing same thing again and again, whith this in mind that I want just use a simple String array and not going to crazy and defining a View array.

My Goal is using an simple array of String to this work without being worry to re-initializing issue.

Maybe it is time to re-think about using ForEach in your App!

SwiftUI would fall to re-rendering trap even with updating an element of the array! which is funny. so make yourself ready if you got 50 or 100 or 1000 rows and you are just updating 1 single row, swiftUI would re render the all entire your array, it does not matter you are using simple Text or your CustomView. So I would wish SwiftUI would be smart to not rendering all array again, and just making necessary render in case.

import SwiftUI

struct ContentView: View {
    
    @State private var arrayOfString: [String] = [String]()
    
    var body: some View {
        
        ForEach(arrayOfString.indices, id:\.self) { index in
            
            TextView(stringOfText: arrayOfString[index])

        }
        
        Spacer()
        
    Button("append new element") {
        
        arrayOfString.append(Int.random(in: 1...1000).description)
    }
    .padding(.bottom)
    
    Button("update first element") {

        if arrayOfString.count > 0 {
            
            arrayOfString[0] = "updated!"
            
        }
    }
    .padding(.bottom)
 
    }
    
}

struct TextView: View {
    
    let stringOfText: String
    
    init(stringOfText: String) {
        self.stringOfText = stringOfText
        print("initializing TextView for:", stringOfText)
    }
    
    var body: some View {
        
        Text(stringOfText)
        
    }

}

enter image description here

ios coder
  • 1
  • 4
  • 31
  • 91

3 Answers3

2

Initializing and rendering are not the same thing. The views get initialized, but not necessarily re-rendered.

Try this with your original ContentView:

struct TextView: View {
    
    let stringOfText: String
    
    init(stringOfText: String) {
        self.stringOfText = stringOfText
        print("initializing TextView for:", stringOfText)
    }
    
    var body: some View {
        print("rendering TextView for:", stringOfText)
        return Text(stringOfText)
    }

}

You'll see that although the views get initialized, they do not in fact get re-rendered.

If you go back to your ContentView, and add dynamic IDs to each element:

TextView(stringOfText: arrayOfString[index]).id(UUID()) 

You'll see that in this case, they actually do get re-rendered.

jnpdx
  • 45,847
  • 6
  • 64
  • 94
0

You are always iterating from index 0, so that’s an expected outcome. If you want forEach should only execute for newly added item, you need to specify correct range. Check code below-:

import SwiftUI

struct ContentViewsss: View {
    
    @State private var arrayOfString: [String] = [String]()
    
    var body: some View {
        
        if arrayOfString.count > 0 {
            ForEach(arrayOfString.count...arrayOfString.count, id:\.self) { index in
                
                TextView(stringOfText: arrayOfString[index - 1])
                
            }
        }
        
        Spacer()
        
        Button("append new element") {
            
            arrayOfString.append(Int.random(in: 1...1000).description)
        }
        
    }
    
}

struct TextView: View {
    
    let stringOfText: String
    
    init(stringOfText: String) {
        self.stringOfText = stringOfText
        print("initializing TextView for:", stringOfText)
    }
    
    var body: some View {
        
        Text(stringOfText)
        
    }
    
}
Tushar Sharma
  • 2,839
  • 1
  • 16
  • 38
  • thanks, but I think you did not understand my question, I do not want just render last or newly Text! I want render all, and stop re-rendering in every new adding process – ios coder Feb 21 '21 at 13:26
  • When you re-render by adding, body refresh take place, and if your for loop starts from index 0, it will definitely loop over all items. I guess you can’t avoid it. But the solution by @Asperi will help save you memory after you scroll of the screen, as it will dequeue off-screen cell and reuse it. – Tushar Sharma Feb 21 '21 at 13:53
  • I know what you mean about saving memory, but as I said I want solve this issue basically, not bring down the expense of using ForEach, also as i said this issue is solvable with defining a View array instead of String array, but why SwiftUI is not clever to stop re rendering issue – ios coder Feb 21 '21 at 13:58
  • From my understanding (may be I am not right) that’s why views in SwiftUI are struct. A size of struct is whatever it hold and nothing else, no base class initialisations required. It’s in short very fast, and swiftUI still offers many customisation for memory just like use of LazyVStack and others which is even good. – Tushar Sharma Feb 21 '21 at 14:08
  • the issue that we are facing in my question is not even connected to **Lazy** of rendering, it is all about stop rendering the already rendered View! Also As I said, SwiftUI would understand the issue and fix it if we define a View array, but with Simple String array, SwiftUI would fall to re-rendering trap so easily. the **Lazy** topic comes to play a role when we got tones of views or strings, but in my case that is not the issue. – ios coder Feb 21 '21 at 14:13
0

You need to use LazyVStack here

LazyVStack {
    ForEach(arrayOfString.indices, id:\.self) { index in
        TextView(stringOfText: arrayOfString[index])
    }
}

so it reuse view that goes out of visibility area.

Also note that SwiftUI creates view here and there very often because they are just value type and we just should not put anything heavy into custom view init.

The important thing is not to re-render view when it is not visible or not changed and exactly this thing is what you should think about. First is solved by Lazy* container, second is by Equatable protocol (see next for details https://stackoverflow.com/a/60483313/12299030)

Asperi
  • 228,894
  • 20
  • 464
  • 690