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 (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 (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.
and then input the repository URL (github.com/WenchaoD/FSCalendar) to the field “search or enter package URL”.
it would look like this
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()
}
on the above code, we do some things:
- 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.
- we initialized the State for the selected date
- we bind each state to the Calendar using $selectedDate and $dictionaryOfDateWithImage
- 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 {
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]
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
}
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
}
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()
}
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
}
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()
}
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))
}
}
}
Top comments (0)