skip to Main Content

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


  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
                    }
                }
            }
        } 
    }
    
    Login or Signup to reply.
  2. 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) {}
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search