New in SwiftUI 3: TimelineView in SwiftUI 3 and iOS 15

DevTechie Inc
Apr 6, 2022
TimelineView is another addition to iOS 15 and gives us a view that update its content periodically with the schedule provided by you.

Timeline view itself doesn’t have any appearance but it acts like a container (just like Group). Its main responsibility is to redraw its content at scheduled points in time.

Let’s start with init for TimeLineView:

public init(_ schedule: Schedule, @ViewBuilder content: @escaping (TimelineView<Schedule, Content>.Context) -> Content)
We have two parameters for init so let’s take a look at them first:

schedule: this is a type that conforms to TimelineSchedule protocol and will determine when to update the content.

content: this is a ViewBuilder type closure that returns a view that will be updated based on the schedule. This closure also provides access to Context object which gives us access to date (the date when update occurred) and cadence (the rate at which the timeline updates the view).

Let’s start with a simple digital clock example as shown in the code below

struct SimpleTimeLineViewExample: View {
    private var dateFormatter: DateFormatter {
        let dt = DateFormatter()
        dt.dateStyle = .none
        dt.timeStyle = .medium
        return dt
    }
    
    var body: some View {
        ZStack {
            Capsule()
                .fill(.blue.opacity(0.2))
                .frame(maxHeight: 100)
            
            TimelineView(.periodic(from: Date(), by: 1)) { context in
                Text(dateFormatter.string(from: context.date))
                    .font(.title)
            }
        }
    }
}
Output:

Let’s update our code to add some flare✨

struct SimpleTimeLineViewExample: View {
    private var dateFormatter: DateFormatter {
        let dt = DateFormatter()
        dt.dateStyle = .none
        dt.timeStyle = .medium
        return dt
    }
    
    var body: some View {
        ZStack {
            Capsule()
                .fill(.blue.opacity(0.2))
                .frame(maxHeight: 100)
            
            TimelineView(.periodic(from: Date(), by: 1)) { context in
                let color = Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1), opacity: Double.random(in: 0.5...1))
                
                VStack {
                    HStack {
                        Circle()
                            .fill(color)
                            .frame(width: 10, height: 10)
                        
                        Circle()
                            .fill(color)
                            .frame(width: 10, height: 10)
                        
                        Circle()
                            .fill(color)
                            .frame(width: 10, height: 10)
                            
                    }
                    .animation(.easeInOut, value: color)
                    
                    
                    
                    HStack {
                        Circle()
                            .fill(color)
                            .frame(width: 10, height: 10)
                        
                        Circle()
                            .fill(color)
                            .frame(width: 10, height: 10)
                        
                        Circle()
                            .fill(color)
                            .frame(width: 10, height: 10)
                            
                    }
                    .animation(.easeInOut, value: color)
                }
                
                
            }
        }
    }
    
}
Output:

For TimelineSchedule, we have been using PeriodicTimelineSchedule which updates content at regular time interval. What if we want to update our view at certain times and stop when we don’t have any more updates? For that we will use ExplicitTimelineSchedule .

ExplicitTimelineSchedule

For ExplicitTimelineSchedule we will create a simple quotes screen that will display quotes at our predefined time intervals.

struct QuotesExample: View {
    var body: some View {
        TimelineView(.explicit(getDates())) { context in
            SubView(date: context.date)
        }
    }
    private func getDates() -> [Date] {
        let date = Date()
        return [date,
                date.addingTimeInterval(1.0),
                date.addingTimeInterval(2.0),
                date.addingTimeInterval(3.0),
                date.addingTimeInterval(4.0),
        ]
    }
    
    struct SubView: View {
        var date: Date
        var quotes = [
            "The greatest glory in living lies not in never falling, but in rising every time we fall. -Nelson Mandela",
            "The way to get started is to quit talking and begin doing. -Walt Disney",
            "Your time is limited, so don't waste it living someone else's life. Don't be trapped by dogma – which is living with the results of other people's thinking. -Steve Jobs" ,
            "If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success. -James Cameron"]
        
        @State var index = 0var body: some View {
            Text("\(quotes[index])")
                .font(.title)
                .onChange(of: date) { newValue in
                    index = index + 1
                }
                .animation(.easeInOut, value: index)
        }
    }
}
Output:

EveryMinuteTimelineSchedule

Another TimelineSchedule which updates on the very first second of minute is EveryMinuteTimelineSchedule .

For this schedule, first update occurs when view first appears, future updates thereafter happen as minute changes.

For this example, we will build an analog clock face with only minute and hour hands

Clock will have two hands:

enum ClockHandType {
    case hour
    case minute
}
Clock hand shape:

struct ClockHand: Shape {
    var handScale: CGFloat
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let rectLength = rect.width / 2
        let lineEnd = rectLength - (rectLength * handScale)
        
        path.move(to: CGPoint(x: rect.midX, y: rect.midY))
        path.addLine(to: CGPoint(x: rect.midX, y: rect.minY + lineEnd))
        
        return path
    }
}
Clock face:

struct ClockView: View {
    var body: some View {
        TimelineView(.everyMinute) { context in
            VStack {
                
                ZStack {
                    Text(context.date.formatted(date: .omitted, time: .shortened))
                        .offset(y: -10)
                    
                    clockHands(date: context.date)
                    
                    ForEach(0..<60) { tick in
                        self.tick(at: tick)
                    }
                }
            }
            .frame(width: 300, height: 300)
        }
    }
    
    func tick(at tick: Int) -> some View {
        VStack {
            Rectangle()
                .fill(Color.primary)
                .opacity(tick % 5 == 0 ? 1 : 0.4)
                .frame(width: 2, height: tick % 5 == 0 ? 15 : 7)
            Spacer()
        }.rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
    }
    
    @ViewBuilder
    private func clockHands(date: Date) -> some View {
        ClockHand(handScale: 0.5)
            .stroke(lineWidth: 5.0)
            .rotationEffect(angle(fromDate: date, type: .hour))
        ClockHand(handScale: 0.6)
            .stroke(lineWidth: 3.0)
            .rotationEffect(angle(fromDate: date, type: .minute))
    }
        
    private func angle(fromDate: Date, type: ClockHandType) -> Angle {
        var timeDegree = 0.0
        let calendar = Calendar.current
        
        switch type {
        case .hour:
            // we have 12 hours so we need to multiply by 5 to have a scale of 60
            timeDegree = CGFloat(calendar.component(.hour, from: fromDate)) * 5
        case .minute:
            timeDegree = CGFloat(calendar.component(.minute, from: fromDate))
        }
        return Angle(degrees: timeDegree * 360.0 / 60.0)
    }
}
Notice the use of .everyMinute

Output:

With that, we have reached the end of this article. Thank you once again for reading, if you liked it, don’t forget to subscribe our newsletter.