swiftuifscalendar

Adding Next/ Prev buttons to FSCalendar in SwiftUI


I've been playing around with FSCalendar and it's helped me build my own customized calendar.

Because it's written in UIKit, I've had a couple of problems integrating it to my SwiftUI project, such as adding a Next and Previous button to the sides of the calendar.

This is what I have so far:

ContentView, where I used an HStack to add the buttons to the sides of my calendar

struct ContentView: View {
let myCalendar = MyCalendar()
var body: some View {
    HStack(spacing: 5) {
        Button(action: {
            myCalendar.previousTapped()
        }) { Image("back-arrow") }
        MyCalendar()
        Button(action: {
            myCalendar.nextTapped()
        }) { Image("next-arrow") }
    }
}}

And the MyCalendar struct which, in order to integrate the FSCalendar library, is a UIViewRepresentable. This is also where I added the two functions (nextTapped and previousTapped) which should change the displayed month when the Buttons are tapped:

struct MyCalendar: UIViewRepresentable {

let calendar = FSCalendar(frame: CGRect(x: 0, y: 0, width: 320, height: 300))

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}
func makeUIView(context: Context) -> FSCalendar {
    
    calendar.delegate = context.coordinator
    calendar.dataSource = context.coordinator
    
    return calendar
}

func updateUIView(_ uiView: FSCalendar, context: Context) {
}

func nextTapped() {
    let nextMonth = Calendar.current.date(byAdding: .month, value: 1, to: calendar.currentPage)
    calendar.setCurrentPage(nextMonth!, animated: true)
    print(calendar.currentPage)
}

func previousTapped() {
    let previousMonth = Calendar.current.date(byAdding: .month, value: -1, to: calendar.currentPage)
    calendar.setCurrentPage(previousMonth!, animated: true)
    print(calendar.currentPage)
}

class Coordinator: NSObject, FSCalendarDelegateAppearance, FSCalendarDataSource, FSCalendarDelegate {
    
    var parent: MyCalendar
    
    init(_ calendar: MyCalendar) {
        self.parent = calendar
    }
    
    func minimumDate(for calendar: FSCalendar) -> Date {
        return Date()
    }
    
    func maximumDate(for calendar: FSCalendar) -> Date {
        return Date().addingTimeInterval((60 * 60 * 24) * 365)
    }
}}

This is what it looks like in the simulator:

Simulator

As you can see, I've managed to print the currentPage in the terminal whenever the next or previous buttons are tapped, but the currentPage is not changing in the actual calendar. How could I fix this?


Solution

  • As you are using UIViewRepresentable protocol for bind UIView class with SwiftUI. Here you have to use ObservableObject - type of object with a publisher that emits before the object has changed.

    You can check the code below for the resulting output: (Edit / Improvement most welcomed)

    import SwiftUI
    import UIKit
    import FSCalendar
        
    class CalendarData: ObservableObject{
            
       @Published var selectedDate : Date = Date()
       @Published var titleOfMonth : Date = Date()
       @Published var crntPage: Date = Date()
            
    }
    struct ContentView: View {
        
        @ObservedObject private var calendarData = CalendarData()
        
        var strDateSelected: String {
            
            let dateFormatter = DateFormatter()
            dateFormatter.dateStyle = .medium
            dateFormatter.timeStyle = .none
            dateFormatter.locale = Locale.current
            return dateFormatter.string(from: calendarData.selectedDate)
        }
        
        var strMonthTitle: String {
            
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "MMMM yyyy"
            dateFormatter.locale = Locale.current
            return dateFormatter.string(from: calendarData.titleOfMonth)
        }
        
        var body: some View {
            
            VStack {
                
                HStack(spacing: 100) {
                    
                    Button(action: {
                                        
                        self.calendarData.crntPage = Calendar.current.date(byAdding: .month, value: -1, to: self.calendarData.crntPage)!
                        
                    }) { Image(systemName: "arrow.left") }
                        .frame(width: 35, height: 35, alignment: .leading)
                    
                    Text(strMonthTitle)
                    .font(.headline)
                    
                    Button(action: {
                        
                        self.calendarData.crntPage = Calendar.current.date(byAdding: .month, value: 1, to: self.calendarData.crntPage)!
                        
                    }) { Image(systemName: "arrow.right") }
                    .frame(width: 35, height: 35, alignment: .trailing)
                }
                
                CustomCalendar(dateSelected: $calendarData.selectedDate, mnthNm: $calendarData.titleOfMonth, pageCurrent: $calendarData.crntPage)
                    .padding()
                    .background(
                        RoundedRectangle(cornerRadius: 25.0)
                            .foregroundColor(.white)
                            .shadow(color: Color.black.opacity(0.2), radius: 10.0, x: 0.0, y: 0.0)
                    )
                    .frame(height: 350)
                    .padding(25)
                
                Text(strDateSelected)
                .font(.title)
                
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    struct CustomCalendar: UIViewRepresentable {
       
        typealias UIViewType = FSCalendar
        
        @Binding var dateSelected: Date
        @Binding var mnthNm: Date
        @Binding var pageCurrent: Date
        
        var calendar = FSCalendar()
        
        var today: Date{
            return Date()
        }
        
        func makeUIView(context: Context) -> FSCalendar {
            
            calendar.dataSource = context.coordinator
            calendar.delegate = context.coordinator
            calendar.appearance.headerMinimumDissolvedAlpha = 0
           
            return calendar
        }
        
        func updateUIView(_ uiView: FSCalendar, context: Context) {
            
            uiView.setCurrentPage(pageCurrent, animated: true) // --->> update calendar view when click on left or right button
        }
        
        func makeCoordinator() -> CustomCalendar.Coordinator {
            Coordinator(self)
        }
        
        class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource {
            
            var parent: CustomCalendar
            
            init(_ parent: CustomCalendar) {
                
                self.parent = parent
            }
    
            func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
                
                parent.dateSelected = date
            }
            
            func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
                
                parent.pageCurrent = calendar.currentPage
                parent.mnthNm = calendar.currentPage
            }
        }
    }
    

    Output:

    enter image description here