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.