0

In my app I want to show the passage of time by having a "calendar" transition from one date to the next, to the next, to the next, etc. So, for example, if I want to show the date transitioning from the 18th, to the 19th, to the 20th, I will show 18 for 1 second, then fade that out, fade in 19, fade that out, then fade in 20.

I have the following to show one date animating to the next (e.g. 18 > 19th):

struct Calendar: View { @State var date: String var body: some View { VStack { Spacer() ZStack { RoundedRectangle(cornerRadius: 20) .stroke(Color.black, lineWidth: 2) .frame(width: 200, height: 200) RoundedRectangle(cornerRadius: 20) .fill(Color.red) .frame(width: 200, height: 200) .offset(y: 160) .clipped() .offset(y: -160) RoundedRectangle(cornerRadius: 20) .stroke(Color.black, lineWidth: 2) .frame(width: 200, height: 200) .offset(y: 160) .clipped() .offset(y: -160) Text(date).font(.system(size: 70.0)) .offset(y: 20) } Spacer() Spacer() }.padding() } } 

and I call this in my code using:

 ScrollView(showsIndicators: false) { VStack { Spacer() ZStack { if showseconddate == false { Calendar(date: "18").animation(.easeInOut(duration: 1.0)) .transition(.opacity) } if showseconddate == true { Calendar(date: "19").animation(.easeInOut(duration: 1.0)) .transition(.opacity) } Spacer() } }.onAppear { Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in withAnimation(Animation.linear(duration: 0.5)) { self.showseconddate.toggle() self.showfirstdate.toggle() } timer.invalidate() } } } 

This all works as intended, but I'm struggling to then expand this to a case where I want to show it transitioning through multiple dates, such as 18 > 19 >20 >21 etc. Does anyone know how to expand this, or to use an alternative solution? Any solution must fade out the old date, then fade in the new date. Many thanks!

2 Answers 2

1

Here's a relatively compact solution. Instead of relying on Bool values, it cycles through an array:

struct ContentView: View { private var dates = ["18","19","20","21","22"] @State private var dateIndex = 0 private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect() var body: some View{ ScrollView(showsIndicators: false) { VStack { Spacer() ZStack { Calendar(date: dates[dateIndex]) .transition(.opacity) .id("date-\(dateIndex)") Spacer() } }.onReceive(timer) { _ in var newIndex = dateIndex + 1 if newIndex == dates.count { newIndex = 0 } withAnimation(.easeInOut(duration: 0.5)) { dateIndex = newIndex } } } } } 
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you! I’ll try this over the weekend and let you know how I get on.
Did this work for you?
It did, thank you!
1

I had reworked your code to get the animations running, I felt it was a bit annoying to watch the entire calendar flash, so I reworked it into a CalendarPage (I renamed Calendar to CalendarPage because Calendar is a Type in Swift) and CalendarView that takes the date and overlays it on the page.

CalendarPage is your Calendar with the date var and Text() removed:

struct CalendarPage: View { var body: some View { VStack { Spacer() ZStack { RoundedRectangle(cornerRadius: 20) .stroke(Color.black, lineWidth: 2) .frame(width: 200, height: 200) RoundedRectangle(cornerRadius: 20) .fill(Color.red) .frame(width: 200, height: 200) .offset(y: 160) .clipped() .offset(y: -160) RoundedRectangle(cornerRadius: 20) .stroke(Color.black, lineWidth: 2) .frame(width: 200, height: 200) .offset(y: 160) .clipped() .offset(y: -160) } Spacer() Spacer() }.padding() } } 

CalendarView uses the timer to increment your dates until you reach the endDate, and it only effects the opacity of the date itself, not the whole calendar:

struct CalendarView: View { @State var date: Int = 0 @State var animate = false @State var calendarSize: CGFloat = 20 let endDate = 31 // This keeps the font size consistent regardless of the size of the calendar var fontSize: CGFloat { calendarSize * 0.45 } private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect() var body: some View { CalendarPage(date: date.description) .overlay(alignment: .bottom) { VStack { Text(date.description) .font(.system(size: fontSize)) .opacity(animate ? 1 : 0) } .frame(height: calendarSize * 0.8) } .frame(width: 200, height: 200) .readSize(onChange: { size in calendarSize = min(size.width, size.height) }) .onReceive(timer) { _ in date += 1 withAnimation(.linear(duration: 0.3)) { animate = true } DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { if date != endDate { withAnimation(.linear(duration: 0.2)) { animate = false } } else { timer.upstream.connect().cancel() } } } } } 

I also used a preference key to compute the height of the CalendarPage (though I could have hard coded it) using this View extension from FiveStars blog

extension View { func readSize(onChange: @escaping (CGSize) -> Void) -> some View { background( GeometryReader { geometryProxy in Color.clear .preference(key: SizePreferenceKey.self, value: geometryProxy.size) } ) .onPreferenceChange(SizePreferenceKey.self, perform: onChange) } } fileprivate struct SizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} } 

1 Comment

Thank you! I’ll try this over the weekend and let you know how I get on.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.