谷歌日历,就像SwiftUI的无限可滚动日历
无限日历
GoogleCalendar就像SwiftUI的无限可滚动日历。
InfiniteCalendar 是用 Swift 编写的无限可滚动的 iOS 日历。
受GoogleCalendar启发的UI / UX设计。受JZCalendarWeekView启发的实现。
特征
- 无限滚动
- 多卷曲类型
- 可存储的用户界面
- 支持长按手势动作
- 支持自动滚动拖动
- 支持处理多个动作
- 支持像谷歌日历这样的振动反馈
要求
- 斯威夫特 5.1
- iOS 13.0 or later
Installation
Swift Package Manager
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
Documentation
1. Initialization
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)
}
}
2. Handling
.onCurrentDateChanged
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
can be obtained.4/1 | 4/2 | 4/3
InfiniteCalendar<EventCellView, EventCell, Settings>(events: $events, settings: settings, didTapToday: $didTapToday)
.onCurrentDateChanged { date in
currentDate = date
}
.onItemSelected
This method will be called when item was tapped.
InfiniteCalendar<EventCellView, EventCell, Settings>(events: $events, settings: settings, didTapToday: $didTapToday)
.onItemSelected { item in
selectedItem = item
}
.onEventAdded
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)
}
.onEventMoved
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
}
}
.onEventCanceled
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).")
}
3. Settings
If you want to customize Settings, create SubClass of .ICViewSettings
numOfDays
Number of dispaly dates on a screen
Sample: , , numOfDays = 1
numOfDays = 3
numOfDays = 7
initDate
The display date for lounch app
scrollType
There is two kinds of scroll type, and .Section
Page
SectionType will deside scroll amount by scroll velocity. On the other hand PageType is always scroll to next / prev page with scroll gesture.
moveTimeMinInterval
Interval minutes time for drag gesture. Default value is 15. Which means when item was created/moved time will move every 15 minutes
timeRange
The display time on time header as a label. Default value is (1, 23). Which means display 1:00 ~ 23:00.
withVibrateFeedback
If vibration is needed during dragging gesture. Default value is true. Vibration feedback is almost same as GoogleCalendar.
4. Custom UI components
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
Default components
Component calss is define as typealias to customize.
public typealias TimeHeader = ICTHeader
public typealias TimeHeaderBackground = ICTHeaderBackground
public typealias DateHeader = ICDHeader
public typealias DateHeaderBackground = ICDHeaderBackground
public typealias DateHeaderCorner = ICDCorner
public typealias AllDayHeader = ICAllDayHeader
public typealias AllDayHeaderBackground = ICAllDayHeaderBackground
public typealias AllDayHeaderCorner = ICAllDayCorner
public typealias Timeline = ICTimeline
Sample custom component (Timeline)
// Timeline is SupplementaryCell type. So must SubClass of `ViewHostingSupplementaryCell`
public final class CustomTimeline: ViewHostingSupplementaryCell<CustomTimelineView> {}
public struct CustomTimelineView: ICComponentView {
// Must use ICTimelineItem
public typealias Item = ICTimelineItem
var item: Item
public init(_ item: Item) {
self.item = item
}
public var body: some View {
Rectangle()
.frame(height: 1.0)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundColor(.red)
.opacity(item.isDisplayed ? 1 : 0)
}
}
Create SubClass of ICViewSettings to set custom component.
class Settings: ICViewSettings {
typealias Timeline = CustomTimeline
init(numOfDays: Int, initDate: Date) {
super.init()
self.numOfDays = numOfDays
self.initDate = initDate
}
...
}
License
InfiniteCalendar is available under the MIT license. See the LICENSE file for more info.