'How can I use TimelineView in Swift 5 to refresh a text every second?

I'm pretty new at swift coding and I'm trying to do a very simple project: just a clock that shows the time. I'm using TimelineView to refresh the time every second, but it's not working. This is my code:

import SwiftUI

struct ContentView: View {

@State var hour: Int = Calendar.current.component(.hour, from: Date())
@State var minute: Int = Calendar.current.component(.minute, from: Date())
@State var second: Int = Calendar.current.component(.second, from: Date())


var body: some View {
    ZStack {
        VStack{
            Spacer()
            HStack {
                
                TimelineView(.periodic(from: .now, by: 1)) { timeline in
                    
                    Text(String(hour))
                    Text(String(minute))
                    Text(String(second))
                }
            }
            
            Spacer()
        }


    }
}
}

struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       Group {
       ContentView()
.previewInterfaceOrientation(.portrait)
       }
   }
}

Since my hour, minute and second variables are @State and I'm using the TimelineView, they should refresh every second, shouldn't they?

I'm very confused and I would appreciate some help. Thank you very much.



Solution 1:[1]

You have to observe changes in the timeline.

Here I used onChange and update the value of min, sec, and hour.

struct TimerView: View {
    var date: Date
    
    @State var hour: Int = Calendar.current.component(.hour, from: Date())
    @State var minute: Int = Calendar.current.component(.minute, from: Date())
    @State var second: Int = Calendar.current.component(.second, from: Date())
    
    var body: some View {
        VStack {
            Text(String(hour))
            Text(String(minute))
            Text(String(second))
        }
        .onChange(of: date) { _ in
            second += 1
            if second > 59 {
                minute += 1
                second = 0
                if minute > 59 {
                    hour += 1
                    minute = 0
                    if hour > 23 {
                        hour = 0
                    }
                }
            }
        }
    }
}
struct ContentView: View { var body: some View {
        ZStack {
            VStack{
                Spacer()
                HStack {
                    TimelineView(.periodic(from: .now, by: 1)) { timeline in
                        TimerView(date: timeline.date)
                    }
                }
                Spacer()
            }
        }
    }
}

Solution 2:[2]

As the documentation says (https://developer.apple.com/documentation/swiftui/timelineview):

A timeline view acts as a container with no appearance of its own. Instead, it redraws the content it contains at scheduled points in time

The content it contains is defined in the closure you provide:

TimelineView(...) { timeline in
    // content which gets redrawn
}

Inside this closure you have access to a TimelineView.Context (https://developer.apple.com/documentation/swiftui/timelineview/context). With the help of this context, you can access the date which triggered the update / redraw like so:

TimelineView(.periodic(from: .now, by: 1)) { timeline in
    Text("\(timeline.date)")
}

This will produce the following output:
enter image description here

To improve formatting, you could use a DateFormatter (https://developer.apple.com/documentation/foundation/dateformatter):

struct ContentView: View {
    private let dateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .none
        dateFormatter.timeStyle = .medium
        return dateFormatter
    }()
    
    var body: some View {
        TimelineView(.periodic(from: .now, by: 1)) { timeline in
            Text("\(dateFormatter.string(from: timeline.date))")
        }
    }
}

enter image description here

Solution 3:[3]

Just make your hour-minute-second as computed property not @State

struct ContentView: View {
    var hour: Int {
        Calendar.current.component(.hour, from: Date())
    }
    var minute: Int {
        Calendar.current.component(.minute, from: Date())
    }
    var second: Int {
        Calendar.current.component(.second, from: Date())
    }
    var body: some View {
        TimelineView(.periodic(from: .now, by: 1.0)) { timeline in
            HStack {
                Text(String(hour))
                Text(String(minute))
                Text(String(second))
            }
        }
    }
}

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 Raja Kishan
Solution 2
Solution 3 meomeomeo