'Why Timer Change Will Trigger LazyVGrid View Update?

import SwiftUI

struct ContentView: View {
    @State private var set = Set<Int>()
    @State private var count = "10"
    private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
    
    @State private var timer:Timer? = nil
    @State private var time = 0
    
    
    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(Array(set)) { num in
                        Text(String(num))
                    }
                }
            }
            .frame(width: 400, height: 400, alignment: .center)
            
            HStack{
                TextField("Create \(count) items", text: $count)
                
                Button {
                    createSet(count: Int(count)!)
                } label: {
                    Text("Create")
                }
            }
            
            if let _ = timer {
                Text(String(time))
                    .font(.title2)
                    .foregroundColor(.green)
            }
            
            HStack {
                Button {
                    time = 100
                    
                    let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
                        time -= 10
                        
                        if time == 0 {
                            self.timer?.invalidate()
                            self.timer = nil
                        }
                    }
                    
                    self.timer = timer
                } label: {
                    Text("Start Timer")
                }
                
                Button {
                    self.timer?.invalidate()
                    self.timer = nil
                } label: {
                    Text("Stop Timer")
                }
            }
        }
        .padding()
    }
    
    private func createSet(count:Int) {
        set.removeAll(keepingCapacity: true)
        
        repeat {
            let num = Int.random(in: 1...10000)
            set.insert(num)
        } while set.count < count
    }
}

extension Int:Identifiable {
    public var id:Self { self }
}

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

I made a break point on Text(String(num)). Every time the timer was trigger, the GridView updated. Why this happened? As the model of grid didn't change.

Updated

If I put the timer in another view, the grid view wouldn't be trigger.

import SwiftUI

struct ContentView: View {
    @State private var set = Set<Int>()
    @State private var count = "10"
    private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
    
    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(Array(set)) { num in
                        Text(String(num))
                    }
                }
            }
            .frame(width: 400, height: 400, alignment: .center)
            
            HStack{
                TextField("Create \(count) items", text: $count)
                
                Button {
                    createSet(count: Int(count)!)
                } label: {
                    Text("Create")
                }
            }
            
            TimerView()
        }
        .padding()
    }
    
    private func createSet(count:Int) {
        set.removeAll(keepingCapacity: true)
        
        repeat {
            let num = Int.random(in: 1...10000)
            set.insert(num)
        } while set.count < count
    }
}

extension Int:Identifiable {
    public var id:Self { self }
}

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

struct TimerView: View {
    @State private var timer:Timer? = nil
    @State private var time = 0
    
    var body: some View {
        if let _ = timer {
            Text(String(time))
                .font(.title2)
                .foregroundColor(.green)
        }
        
        HStack {
            Button {
                time = 100
                
                let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
                    time -= 10
                    
                    if time == 0 {
                        self.timer?.invalidate()
                        self.timer = nil
                    }
                }
                
                self.timer = timer
            } label: {
                Text("Start Timer")
            }
            
            Button {
                self.timer?.invalidate()
                self.timer = nil
            } label: {
                Text("Stop Timer")
            }
        }
    }
}

struct TimerView_Previews: PreviewProvider {
    static var previews: some View {
        TimerView()
    }
}


Solution 1:[1]

That´s pretty much how SwiftUI works. Every change to a @State var triggers the View to reevaluate. If you put your ForEach in another view it will only reevaluate if you change a var that changes that view. E.g. set or columns.

struct ExtractedView: View {
    var columns: [GridItem]
    var set: Set<Int>
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns) {
                ForEach(Array(set)) { num in
                    Text(String(num))
                }
            }
        }
        .frame(width: 400, height: 400, alignment: .center)
    }
}

It is encouraged in SwiftUI to make many small Views. The system driving this is pretty good in identifying what needs to be changed and what not. There is a very good WWDC video describing this.

WWDC

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 burnsi