Google Calendar-like infinite scrollable calendar for SwiftUI.
InfiniteCalendar is infinite scrollable calendar for iOS written in Swift.
UI/UX design inspired by GoogleCalendar. Implementation inspired by JZCalendarWeekView.
- Infinite scroll
- Multiple scroll type
- Custamazable UI
- Support long tap gesture actions
- Support autoScroll for drag
- Support handling multiple actions
- Support vibrate feedback like GoogleCalendar
- Swift 5.1
- iOS 13.0 or later
InfiniteCalendar is available through Swift Package Manager.
Add it to an existing Xcode project as a package dependency:
- From the File menu, select Add Packages…
- Enter "https://github.com/shohe/InfiniteCalendar.git" into the package repository URL text field
You need to define View, ViewModel and CollectionViewCell for display cell on Calendar:
- Create View complianted for CellableView protocol:
struct EventCellView: CellableView {
typealias VM = ViewModel
// MARK: ViewModel
struct ViewModel: ICEventable { ... }
// MARK: View
var viewModel: ViewModel
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
...
}
}
- Implement ViewModel complianted for ICEventable protocol:
struct ViewModel: ICEventable {
private(set) var id: String = UUID().uuidString
var text: String
var startDate: Date
var endDate: Date?
var intraStartDate: Date
var intraEndDate: Date
var editState: EditState?
var isAllDay: Bool
init(text: String, start: Date, end: Date?, isAllDay: Bool = false, editState: EditState? = nil) {
...
}
// ! Make sure copy current object, otherwise view won't display properly when SwiftUI View is updated.
func copy() -> EventCellView.ViewModel {
var copy = ViewModel(text: text, start: startDate, end: endDate, isAllDay: isAllDay, editState: editState)
...
return copy
}
static func create(from eventable: EventCellView.ViewModel?, state: EditState?) -> EventCellView.ViewModel {
if var model = eventable?.copy() {
model.editState = state
return model
}
return ViewModel(text: "New", start: Date(), end: nil, editState: state)
}
}
- Create CollectionViewCell complianted for ViewHostingCell protocol:
final class EventCell: ViewHostingCell<EventCellView> {
override init(frame: CGRect) {
super.init(frame: frame)
}
}
- Use InfiniteCalendar with CustomViews:
struct ContentView: View {
@State var events: [EventCellView.VM] = []
@State var didTapToday: Bool = false
@ObservedObject var settings: ICViewSettings = ICViewSettings(
numOfDays: 3,
initDate: Date(),
scrollType: .sectionScroll,
moveTimeMinInterval: 15,
timeRange: (1, 23),
withVibration: true
)
var body: some View {
InfiniteCalendar<EventCellView, EventCell, ICViewSettings>(events: $events, settings: settings, didTapToday: $didTapToday)
}
}
This method will be called when changed currrent date displayed on calendar. The date can be get is the leftest date on current display.
ex.) Display 3 column dates, 4/1 | 4/2 | 4/3
-> 4/1
can be obtained.
InfiniteCalendar<EventCellView, EventCell, Settings>(events: $events, settings: settings, didTapToday: $didTapToday)
.onCurrentDateChanged { date in
currentDate = date
}
This method will be called when item was tapped.
InfiniteCalendar<EventCellView, EventCell, Settings>(events: $events, settings: settings, didTapToday: $didTapToday)
.onItemSelected { item in
selectedItem = item
}
This method will be called when item was created by long tap with drag gesture.
InfiniteCalendar<EventCellView, EventCell, Settings>(events: $events, settings: settings, didTapToday: $didTapToday)
.onEventAdded { item in
events.append(item)
}
This method will be called when item was created by long tap gesture on exist item.
InfiniteCalendar<EventCellView, EventCell, Settings>(events: $events, settings: settings, didTapToday: $didTapToday)
.onEventMoved { item in
if let index = events.firstIndex(where: {$0.id == item.id}) {
events[index] = item
}
}
This method will be called when canceled gesture event by some accident or issues.
InfiniteCalendar<EventCellView, EventCell, Settings>(events: $events, settings: settings, didTapToday: $didTapToday)
.onEventCanceled { item in
print("Canceled some event gesture for \(item.id).")
}
If you want to customize Settings, create SubClass of ICViewSettings
.
Number of dispaly dates on a screen
Sample: numOfDays = 1
, numOfDays = 3
, numOfDays = 7
The display position of Date only for One-day layout.
Sample: datePosition = .top
, datePosition = .left
The display date for lounch app
There is two kinds of scroll type, Section
and Page
.
SectionType will deside scroll amount by scroll velocity. On the other hand PageType is always scroll to next / prev page with scroll gesture.
Interval minutes time for drag gesture. Default value is 15. Which means when item was created/moved time will move every 15 minutes
The display time on time header as a label. Default value is (1, 23). Which means display 1:00 ~ 23:00.
If vibration is needed during dragging gesture. Default value is true. Vibration feedback is almost same as GoogleCalendar.
You can customize each components on the bellow.
- SupplementaryCell (Use as SupplementaryCell in CollectionView)
- TimeHeader
- DateHeader
- DateHeaderCorner
- AllDayHeader
- AllDayHeaderCorner
- Timeline
- DecorationCell (Use as DecorationCell in CollectionView)
- TimeHeaderBackground
- DateHeaderBackground
- AllDayHeaderBackground
Associatedtypes
Component class is define as typealias to customize.
//* When you customize, set two of classes to custom class you created.
// TimeHeader
associatedtype TimeHeaderView: ICTimeHeaderView
associatedtype TimeHeader: ICTimeHeader<TimeHeaderView>
// DateHeader
associatedtype DateHeaderView: ICDateHeaderView
associatedtype DateHeader: ICDateHeader<DateHeaderView>
// DateHeaderCorner
associatedtype DateCornerView: ICDateCornerView
associatedtype DateCorner: ICDateCorner<DateCornerView>
// AllDayHeader
associatedtype AllDayHeaderView: ICAllDayHeaderView
associatedtype AllDayHeader: ICAllDayHeader<AllDayHeaderView>
// AllDayHeaderCorner
associatedtype AllDayCornerView: ICAllDayCornerView
associatedtype AllDayCorner: ICAllDayCorner<AllDayCornerView>
// Timeline
associatedtype TimelineView: ICTimelineView
associatedtype Timeline: ICTimeline<TimelineView>
// TimeHeaderBackground
associatedtype TimeHeaderBackgroundView: ICTimeHeaderBackgroundView
associatedtype TimeHeaderBackground: ICTimeHeaderBackground<TimeHeaderBackgroundView>
// DateHeaderBackground
associatedtype DateHeaderBackgroundView: ICDateHeaderBackgroundView
associatedtype DateHeaderBackground: ICDateHeaderBackground<DateHeaderBackgroundView>
// AllDayHeaderBackground
associatedtype AllDayHeaderBackgroundView: ICAllDayHeaderBackgroundView
associatedtype AllDayHeaderBackground: ICAllDayHeaderBackground<AllDayHeaderBackgroundView>
All you need is 4 steps.
- Create CustomView and Cell for wrap the View
- Create CustomSetting class
- Set the typealiases of each classes to View and Cell you created
- Set the CustomSetting class to InfiniteCalendar
e.g. Customize DateHeader component.
// DateHeader should be inherited `ICDateHeader`.
class CustomDateHeader: ICDateHeader<CustomDateHeaderView> {}
// CustomView should be inherited `ICDateHeaderView`.
struct CustomDateHeaderView: ICDateHeaderView {
public typealias Item = ICDateHeaderItem
var item: Item
public init(_ item: Item) {
self.item = item
}
public var body: some View {
...
}
}
Create SubClass inherited ICSettings
.
class CustomSettings: ICSettings {
@Published public var numOfDays: Int = 1
@Published public var initDate: Date = Date()
@Published public var scrollType: ScrollType = .pageScroll
@Published public var moveTimeMinInterval: Int = 15
@Published public var timeRange: (startTime: Int, endTime: Int) = (1, 23)
@Published public var withVibrateFeedback: Bool = true
required public init() {}
...
}
class CustomSettings: ICSettings {
typealias DateHeaderView = CustomDateHeaderView
typealias DateHeader = CustomDateHeader
...
}
...
@State var events: [EventCellView.VM] = SampleData().events
@State var didTapToday: Bool = false
@ObservedObject var settings: CustomSettings = CustomSettings()
var body: some View {
InfiniteCalendar<EventCellView, EventCell, CustomSettings>(events: $events, settings: settings, didTapToday: $didTapToday)
}
...
InfiniteCalendar is available under the MIT license. See the LICENSE file for more info.