swiftswiftui

Swift UI update clock without view refresh


I am trying to present a basic time that shows the live time with seconds. When the view appears I call a function that recursively toggles a clockID every 1 second. And the clockId is applied to the Text element such as:

Text(formatTime(date: Date())).font(.caption).id(clockID)

This allows the formatTime function to be called when the clockID changes every second and thus shows the new time. But in doing so it seems that the entire view is redrawn when clockID is toggled. The causes the view to not be very smooth, and when a menu is opened it has a flicking effect. Whats a more efficient way to display the time?

func formatTime(date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "h:mm:ss"
    return formatter.string(from: date)
}
func clockTogg() {
        clockID = UUID()
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            if appeared {
                clockTogg()
            }
        }
    }

Solution

  • You could try this approach, of isolating the changing part of a View into a separate View. This avoids the whole parent view from updating when a variable (in the sub View) is changed.

    For example:

    struct ContentView: View {
        let items = ["one", "two", "three"]
    
        var body: some View {
            let _ = print("---> ContentView recalculated") //<-- for testing
            VStack {
                TimeView() // <-- only this view is updated
                List(items, id: \.self) { item in
                    Text(item)
                }
            }
        }
    }
    
    struct TimeView: View {
        @State private var timeNow = ""
    
        let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
        let dateFormatter = DateFormatter()
    
        func updateTime() {
            dateFormatter.dateFormat = "LLLL dd, hh:mm:ss a"
            timeNow = dateFormatter.string(from: Date())
        }
    
        var body: some View {
            let _ = print("---> TimeView recalculated") //<-- for testing
            Text(timeNow)
                .onAppear{ updateTime() }
                .onReceive(timer) { _ in
                    updateTime()
                }
        }
    }
    

    Another example using the same approach:

    struct ContentView: View {
        let items = ["one", "two", "three"]
        @State private var timeNow = ""
        
        var body: some View {
            let _ = print("---> ContentView recalculated") //<-- for testing
            VStack {
                TimeView(timeNow: $timeNow) // <-- only this view is updated
                    .onAppear{ updateTime() }
                    .onReceive(timer) { _ in
                        updateTime()
                    }
                
                List(items, id: \.self) { item in
                    Text(item)
                }
            }
        }
        
        let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
        let dateFormatter = DateFormatter()
    
        func updateTime() {
            dateFormatter.dateFormat = "LLLL dd, hh:mm:ss a"
            timeNow = dateFormatter.string(from: Date())
        }
        
    }
    
    struct TimeView: View {
        @Binding var timeNow: String
    
        var body: some View {
            let _ = print("---> TimeView recalculated") //<-- for testing
            Text(timeNow)
        }
    }