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!
CodePudding user response:
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
}
}
}
}
}
CodePudding user response:
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) {}
}
