DEV Community

Muhammad Rasyad Caesarardhi
Muhammad Rasyad Caesarardhi

Posted on

Customizing Calendar with FSCalendar in SwiftUI

Welcome back to my blog on front-end development, code, and iOS development. here, I will tell you about customizing the view of a calendar in SwiftUI using the FSCalendar package (https://github.com/WenchaoD/FSCalendar).

Why do we need a custom calendar?

First of all, why do we need a custom calendar? The answer is that we need a custom calendar that can show weekly views and not monthly views. second, we want to customize just part of the component from the calendar. for example, part of the background color, part of the text color, adding an image to the calendar to build streaks or adding data and events.

Duolingo Streaks Calendar

Duolingo Streaks Calendar (https://preview.redd.it/what-do-the-colors-mean-on-the-streak-calendar-v0-0iynzx8lp3pa1.png?width=337&format=png&auto=webp&s=b529e874b8610975f63c820b36665dcd434af4fe)

native calendars that are available natively in the SwiftUI can’t afford to do that. hence, we are in dire need of a custom calendar that accommodates all of it.

Luckily, we have a great repository for this custom calendar in iOS development projects. it is called FSCalendar (https://github.com/WenchaoD/FSCalendar). it is a truly great project that accommodates our needs to build a custom calendar with images, custom text color, custom data, and events.

Custom calendar implementation using FSCalendar

Custom calendar implementation using FSCalendar (https://cloud.githubusercontent.com/assets/5186464/10262249/4fabae40-69f2-11e5-97ab-afbacd0a3da2.jpg)

How to import it?

now, how do we import the package? first of all, we need to open our Xcode projects. Next, add package dependencies to it.

Add package dependencies

and then input the repository URL (github.com/WenchaoD/FSCalendar) to the field “search or enter package URL”.

Search field on add package

it would look like this

FSCalendar being added to the project

and then click Add Package. wait until the fetching is finished, and we are ready to go!

How do you customize the element in it?

Creating ContentView

below here is the full code of contentView the home of our calendar:

//
//  ContentView.swift
//  Calendar
//
//  Created by Muhammad Rasyad Caesarardhi on 14/07/24.
//

import SwiftUI

struct ContentView: View {
    @State private var dictionaryOfDateWithImage: [Date: ImageResource] = [
        getYesterdayDate(): .emoAnxiety,
        getTodayDate(): .emoHappy,
        getDateBySubstracting(substractor: -6): .emoFreeze
    ]
    @State private var selectedDate: Date = .init()

    var body: some View {
        ScrollView {
            VStack {
                FSCalendarView(selectedDate: $selectedDate, dictionaryOfDateWithImage: $dictionaryOfDateWithImage)
                    .frame(height: 300)
            }.onChange(of: [selectedDate]) {
                print(formatDateToUTC7(date: selectedDate))
            }
        }
        .padding()
    }

    func formatDateToUTC7(date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        formatter.timeZone = TimeZone(secondsFromGMT: 7 * 3600)
        return formatter.string(from: date)
    }
}

// Helper Function for Date
func getYesterdayDate() -> Date {
    return getDateBySubstracting(substractor: -1)
}

func getDateBySubstracting(substractor: Int) -> Date {
    return Calendar.current.date(byAdding: .day, value: substractor, to: Date()) ?? Date()
}

func getTodayDate() -> Date {
    return Date()
}

#Preview {
    ContentView()
}

Enter fullscreen mode Exit fullscreen mode

on the above code, we do some things:

  1. we initialized State or mock Published from viewModel for the data used in the Calendar. The data type we use is a dictionary because it can be read quickly with indexing. more on this in the Calendar code.
  2. we initialized the State for the selected date
  3. we bind each state to the Calendar using $selectedDate and $dictionaryOfDateWithImage
  4. we also created a helper function to get today's date, yesterday's date, and any date by subtracting it from today's date (+1 is tomorrow, -1 is yesterday)

Creating FSCalendarView (this is the main part of the calendar)

Step 1: Adding UIViewRepresentable

Since FSCalendarView is built on top of UIKit, we need UIViewRepresentable protocol to make it works with SwiftUI

    struct FSCalendarView: UIViewRepresentable {
Enter fullscreen mode Exit fullscreen mode

Step 2: Add State Binding

Add Binding to the dictionary and selectedDate from ContentView. Dictionary of Date with image will be used to defined each date have what image to show.

    @Binding var selectedDate: Date
    @Binding var dictionaryOfDateWithImage: [Date: ImageResource]
Enter fullscreen mode Exit fullscreen mode

Step 3: Initialize coordinator and appearence

here, we pass the dictionary of date with image associate with the date to the Coordinator, which handle re-render and how to show the calendar to the view.

    // FSCalendarView
    @Binding var selectedDate: Date
    @Binding var dictionaryOfDateWithImage: [Date: ImageResource]

    func makeCoordinator() -> Coordinator {
      Coordinator(self, dateWithImageResourceDict: dictionaryOfDateWithImage)
    }

    func makeUIView(context: Context) -> FSCalendar {
      let calendar = FSCalendar()
      calendar.delegate = context.coordinator
      calendar.dataSource = context.coordinator
      calendar.appearance.selectionColor = .systemPurple
      // Remove today circle
      calendar.today = nil
      return calendar
    }
Enter fullscreen mode Exit fullscreen mode

Step 4: Coordinator

    // Coordinator
    class Coordinator: NSObject, FSCalendarDataSource, FSCalendarDelegate, FSCalendarDelegateAppearance {
            var parent: FSCalendarView

            var dateWithImageResourceDict: [Date: ImageResource]

            init(_ calender: FSCalendarView, dateWithImageResourceDict: [Date: ImageResource]) {
                self.parent = calender
                self.dateWithImageResourceDict = dateWithImageResourceDict
            }
Enter fullscreen mode Exit fullscreen mode

Step 5: Adding image to the calendar

to add image to the calendar, we need to use imageFor helper in the FSCalendar. the way it works is, FSCalendar will always loop through each date in the year (because it’s just the way it works) → O(n), and when it hits on the same day with some date in the dictionary, the dictionary will return the value immediately → O(1). and it would be so fast! thats why we use dictionary here.

    // this function didSelect is used to set the selectedDate data state
    func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
      calendar.reloadData() // Refresh the calendar to apply the changes
      parent.selectedDate = date
    }

    // this function imageFor is used to set the image for the date
    func calendar(_ calendar: FSCalendar, imageFor date: Date) -> UIImage? {
      let targetSize = CGSize(width: 50, height: 50)
      if date == parent.selectedDate {
          return nil
      }

      if let stat = dateWithImageResourceDict.first(where: { Calendar.current.isDate($0.key, inSameDayAs: date) })?.value {
          return UIImage(resource: stat).resized(to: targetSize)
      }

      return nil
    }

    // If the user move the month, then the calendar works appropriately
    // Need reload to have fill colors display correctly after calendar page changes
    func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
      calendar.reloadData()
    }
Enter fullscreen mode Exit fullscreen mode

Step 6: Adding re-render using didSet and updateUIView

now you notice, when the state dictionary data has changed over the course of the app, the calendar didn’t change! why? turns out when using UIKit in SwiftUI, we need to use didSet to ask for re-render of the view! wow!

    // Coordinator
    var parent: FSCalendarView

    var dateWithImageResourceDict: [Date: ImageResource] {
      didSet {
        print("rerender")
      }
    }

    init(_ calender: FSCalendarView, dateWithImageResourceDict: [Date: ImageResource]) {
      self.parent = calender
      self.dateWithImageResourceDict = dateWithImageResourceDict
    }
Enter fullscreen mode Exit fullscreen mode

we also need to ask updateUIView to do something first before re-render and to call the didSet in Coordinator. here we replace the dateDictionary with a new dictionary which was Bind to the state in ContentView and ask the calendar to reload the data to properly show it!

    // FSCalendarView
    func updateUIView(_ uiView: FSCalendar, context: Context) {
        // update the calendar view if necessary
        context.coordinator.dateWithImageResourceDict = dictionaryOfDateWithImage
        uiView.reloadData()
    }
Enter fullscreen mode Exit fullscreen mode

Step 7: Full code!

//
//  FSCalendarView.swift
//  Calendar
//
//  Created by Muhammad Rasyad Caesarardhi on 14/07/24.
//

import FSCalendar
import SwiftUI
import UIKit

struct FSCalendarView: UIViewRepresentable {
    @Binding var selectedDate: Date
    @Binding var dictionaryOfDateWithImage: [Date: ImageResource]

    func makeCoordinator() -> Coordinator {
        Coordinator(self, dateWithImageResourceDict: dictionaryOfDateWithImage)
    }

    func makeUIView(context: Context) -> FSCalendar {
        let calendar = FSCalendar()
        calendar.delegate = context.coordinator
        calendar.dataSource = context.coordinator
        calendar.appearance.selectionColor = .systemPurple
        // Remove today circle
        calendar.today = nil
        return calendar
    }

    func updateUIView(_ uiView: FSCalendar, context: Context) {
        // update the calendar view if necessary
        context.coordinator.dateWithImageResourceDict = dictionaryOfDateWithImage
        uiView.reloadData()
    }

    // MARK: - Coordinator

    class Coordinator: NSObject, FSCalendarDataSource, FSCalendarDelegate, FSCalendarDelegateAppearance {
        var parent: FSCalendarView

        var dateWithImageResourceDict: [Date: ImageResource] {
            didSet {
                print("rerender")
            }
        }

        init(_ calender: FSCalendarView, dateWithImageResourceDict: [Date: ImageResource]) {
            self.parent = calender
            self.dateWithImageResourceDict = dateWithImageResourceDict
        }

        func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
            calendar.reloadData() // Refresh the calendar to apply the changes
            parent.selectedDate = date
        }

        func calendar(_ calendar: FSCalendar, imageFor date: Date) -> UIImage? {
            let targetSize = CGSize(width: 50, height: 50)
            if date == parent.selectedDate {
                return nil
            }

            if let stat = dateWithImageResourceDict.first(where: { Calendar.current.isDate($0.key, inSameDayAs: date) })?.value {
                return UIImage(resource: stat).resized(to: targetSize)
            }

            return nil
        }

        // Need reload to have fill colors display correctly after calendar page changes
        func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
            calendar.reloadData()
        }
    }
}

extension UIImage {
    func resized(to size: CGSize) -> UIImage? {
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: size))
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)