1

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?

Nico Cobelo
  • 557
  • 7
  • 18
  • Approach it without thinking like how it would be done in UIKit, ie forget target-action. Set a state in ContentView like `currentDate`. The buttons would change that state. Then make the Calendar respond when the date changes - pass the date property as a Binding to MyCalendar, where the Coordinator (which should not have ref to the struct, but instead to the UIView) would respond when it changes by updating the MyCalendar structs UIView. – Cenk Bilgen Mar 11 '21 at 14:32

1 Answers1

2

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

Kishan Bhatiya
  • 2,175
  • 8
  • 14