civil-and-structural-engineering
Creating a Custom Calendar View in Ios with Uicollectionview
Table of Contents
Creating a custom calendar view in iOS can dramatically improve the user experience by giving you full control over date selection, event display, and visual design. While Apple's built-in date picker works for simple input, a custom calendar built with UICollectionView offers unmatched flexibility for scheduling, booking, or diary apps. This guide walks you through building a production-ready calendar from scratch, covering everything from layout configuration to event markers and performance tuning.
Understanding UICollectionView
UICollectionView is a powerful UIKit component designed for displaying ordered data in a flexible grid layout. Unlike UITableView which is strictly vertical, a collection view allows any arrangement of items – perfect for the seven‑column grid of a monthly calendar. It manages cell reuse automatically, supports custom layouts, and handles user interaction through delegates and data sources.
For a calendar view, UICollectionViewFlowLayout is the simplest starting point. It lays out cells in a grid with configurable spacing. You can subclass it for more advanced behaviour like sticky headers or section‑based layouts. The two core protocols you will implement are UICollectionViewDataSource (providing cell count and configuration) and UICollectionViewDelegate (handling selection and sizing).
Setting Up the Collection View
Begin by adding a UICollectionView to your view controller. You can do this in Interface Builder or programmatically. Programmatic setup gives you more control and is easier to reuse across different screens.
First, define your view controller to conform to the necessary protocols:
class CalendarViewController: UIViewController,
UICollectionViewDataSource,
UICollectionViewDelegate,
UICollectionViewDelegateFlowLayout {
var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
setupCalendarCollectionView()
}
}
Create a helper method to configure the collection view. Use a UICollectionViewFlowLayout with zero spacing to achieve a seamless calendar grid:
private func setupCalendarCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.sectionInset = .zero
collectionView = UICollectionView(
frame: view.bounds,
collectionViewLayout: layout
)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(
DateCell.self,
forCellWithReuseIdentifier: DateCell.reuseIdentifier
)
view.addSubview(collectionView)
}
Registering a custom cell is essential because a calendar displays date numbers, selection states, and event indicators. Create a subclass of UICollectionViewCell – we will design it shortly.
Designing the Calendar Grid Layout
The layout must display exactly seven columns to represent the days of the week. Use the delegate method to dynamically size cells based on the collection view's width:
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
let totalWidth = collectionView.bounds.width
let width = totalWidth / 7
// Make cells square by default
return CGSize(width: width, height: width)
}
Adding weekday headers improves usability. You can implement a section header using UILabel and the collection view’s supplementary view mechanism. Create a UICollectionReusableView subclass for the header and register it:
collectionView.register(
CalendarHeaderView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: CalendarHeaderView.reuseIdentifier
)
In the flow layout delegate, return a header size. The header view should display the weekday abbreviations (e.g., Sun, Mon, Tue…). This approach keeps the header outside the scrollable grid, making it always visible.
Building the Data Model
A solid data model is the foundation of any calendar. You need to represent a month's days, including padding days from the previous and next month to fill the grid rows. Use Calendar and DateComponents from Foundation.
Calculating Days in a Month
Given a reference month (e.g., the currently displayed month), compute the number of days, the weekday of the first day, and the weekday of the last day. This information determines the padding required:
struct MonthModel {
let monthDate: Date
let daysCount: Int
let firstWeekday: Int // 1 = Sunday, 2 = Monday...
let lastWeekday: Int
}
Access the current calendar with Calendar.current and use range(of:in:for:) to get the number of days. The weekday of the first day is obtained via component(.weekday, from: firstDateOfMonth).
Handling Previous and Next Month Overflow
The total cells needed is 7 * number of rows. Most months span 4 to 6 weeks. Calculate the number of padding days from the previous month: if the first weekday is Sunday (1), no padding is needed. If it's Tuesday (3), two days from the previous month appear at the start of the grid.
Build an array of CalendarDay objects. Each object stores the date, whether it belongs to the current month, and any event data:
struct CalendarDay {
let date: Date
let isCurrentMonth: Bool
var events: [Event] = []
var isToday: Bool { Calendar.current.isDateInToday(date) }
var isSelected: Bool = false
}
Populate the array by iterating from the first visible date (which may be in the previous month) to the last visible date. Use Calendar.current.date(byAdding:value:to:) to move forward day by day.
Creating a Custom Collection View Cell
Design a cell that displays the date number and visually indicates selection, today, and events. A simple implementation uses a UILabel plus a UIView for the selection circle:
class DateCell: UICollectionViewCell {
static let reuseIdentifier = "DateCell"
let dateLabel = UILabel()
let selectionView = UIView()
let dotView = UIView() // event indicator
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
selectionView.backgroundColor = .clear
selectionView.layer.cornerRadius = bounds.height / 2
selectionView.clipsToBounds = true
selectionView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(selectionView)
dateLabel.textAlignment = .center
dateLabel.font = UIFont.systemFont(ofSize: 16)
dateLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(dateLabel)
dotView.backgroundColor = .systemBlue
dotView.layer.cornerRadius = 3
dotView.isHidden = true
dotView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(dotView)
NSLayoutConstraint.activate([
selectionView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
selectionView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
selectionView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.8),
selectionView.heightAnchor.constraint(equalTo: selectionView.widthAnchor),
dateLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
dateLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
dotView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
dotView.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 4),
dotView.widthAnchor.constraint(equalToConstant: 6),
dotView.heightAnchor.constraint(equalToConstant: 6)
])
}
func configure(with day: CalendarDay) {
dateLabel.text = "\(Calendar.current.component(.day, from: day.date))"
dateLabel.textColor = day.isCurrentMonth ? .label : .secondaryLabel
selectionView.backgroundColor = day.isSelected ? .tintColor : .clear
dateLabel.textColor = day.isSelected ? .white : dateLabel.textColor
dotView.isHidden = day.events.isEmpty
}
}
Populating the Calendar with Data
In the UICollectionViewDataSource method, return the total number of cells (padded count). Use the CalendarDay array to configure each cell:
func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
return visibleDays.count
}
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: DateCell.reuseIdentifier,
for: indexPath
) as! DateCell
let day = visibleDays[indexPath.item]
cell.configure(with: day)
return cell
}
Marking today can be done by checking Calendar.current.isDateInToday(day.date) inside the cell configuration. You might add a bold font or a subtle background colour.
Handling User Interaction
Implement didSelectItemAt to toggle selection and update the data model. For a single‑selection calendar, deselect the previously selected day and select the tapped one:
func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
let tappedDay = visibleDays[indexPath.item]
guard tappedDay.isCurrentMonth else {
// Optionally navigate to that month
return
}
// Deselect previous
if let prevIndex = selectedIndex {
var prevDay = visibleDays[prevIndex]
prevDay.isSelected = false
collectionView.reloadItems(at: [IndexPath(item: prevIndex, section: 0)])
}
// Select new
selectedIndex = indexPath.item
var newDay = visibleDays[indexPath.item]
newDay.isSelected = true
collectionView.reloadItems(at: [indexPath])
// Notify delegate or perform action
dateSelected?(newDay.date)
}
For range selection (e.g., booking a trip), track the start and end indices, reload the cells in between, and visually connect them with a custom background.
Customizing the Appearance
Make the calendar match your app's theme by customizing colors, fonts, and spacing. For example, you can style weekends differently:
if isWeekend {
dateLabel.textColor = .systemRed
} else {
dateLabel.textColor = .label
}
Use Calendar.current.component(.weekday, from: day.date) to determine if the day is Saturday or Sunday. You can also dim dates that are in the past or disable future dates depending on your use case.
Holiday styling can be achieved through an array of special dates. When configuring the cell, check if the day is a holiday and apply a different background colour or an emoji.
Implementing Month Navigation
Users expect to swipe or tap buttons to change months. Add left/right arrow buttons in the navigation bar or a header above the collection view. When a new month is selected, recompute the visibleDays array and reload the collection view:
@objc func goToNextMonth() {
referenceMonth = Calendar.current.date(
byAdding: .month,
value: 1,
to: referenceMonth
)!
computeVisibleDays()
collectionView.reloadData()
// Update header month label
}
Animated transitions improve the experience. Use UIView.transition(with:duration:options:animations:) with a cross‑fade or slide transition. Alternatively, implement a paging‑style collection view with a custom layout that snaps to each month.
Adding Events and Markers
A calendar view becomes truly useful when it shows events, tasks, or reminders. Fetch your app’s event data (from Core Data, CloudKit, or a network API) and map each event to its date. Then, in the visibleDays model, assign the events array.
To display event dots, add a small coloured circle below the date label (as done in the cell above). For multiple events, you might show stacked dots or a badge. If there are many events, consider a different indicator like a filled background.
For calendars from EventKit, use `EKEventStore` to request access and fetch events for the visible month. Cache the results to avoid repeated fetching when the month stays the same.
Performance Considerations
A smooth calendar, especially one that spans many months, requires attention to performance:
- Cell reuse –
UICollectionViewalready does this, but ensure your cells are lightweight. Avoid complex view hierarchies inside each cell. - Minimal date formatting – Pre‑format the day numbers and month labels. Avoid creating new
DateFormatterobjects every time; create a static formatter once. - Prefetching – Implement
UICollectionViewDataSourcePrefetchingto load event data and compute the next month’s days in advance. - Image caching – If you display icons or thumbnails on calendar days, use an
NSCacheor a dedicated caching library.
Conclusion
Building a custom calendar with UICollectionView gives you complete freedom over appearance and behaviour. Starting with a solid data model and a well‑designed cell, you can add month navigation, event indicators, and selection logic with minimal effort. The same architecture scales from a simple date picker to a full‑featured planner complete with drag‑and‑drop events. For further reading, explore Apple’s documentation on NSCalendar and DateFormatter, or check out open‑source calendar libraries to see alternative implementations. With the techniques described here, you are ready to craft a calendar that feels perfectly at home in your iOS app.