Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select range #125

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions KDCalendar/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
Expand All @@ -23,23 +20,20 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hW4-le-uM3" customClass="CalendarView" customModule="CalendarView" customModuleProvider="target">
<rect key="frame" x="16" y="36" width="343" height="343"/>
<rect key="frame" x="16" y="16" width="343" height="343"/>
<color key="backgroundColor" red="0.94004629629629632" green="0.80393518518518514" blue="0.87748842592592591" alpha="0.87" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" secondItem="hW4-le-uM3" secondAttribute="height" id="773-eW-lgj"/>
</constraints>
</view>
<datePicker contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" datePickerMode="date" minuteInterval="1" translatesAutoresizingMaskIntoConstraints="NO" id="ZI5-5E-Eq0">
<datePicker contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" datePickerMode="date" translatesAutoresizingMaskIntoConstraints="NO" id="ZI5-5E-Eq0">
<rect key="frame" x="16" y="431" width="343" height="216"/>
<date key="date" timeIntervalSinceReferenceDate="482061469.277242">
<!--2016-04-11 09:57:49 +0000-->
</date>
<connections>
<action selector="onValueChange:" destination="vXZ-lx-hvc" eventType="valueChanged" id="YJA-Ef-5x9"/>
</connections>
</datePicker>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="BMl-ka-oh2">
<rect key="frame" x="16" y="387" width="343" height="30"/>
<rect key="frame" x="16" y="367" width="343" height="30"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="d9N-eH-E3w">
<rect key="frame" x="8" y="0.0" width="154" height="30"/>
Expand Down Expand Up @@ -79,18 +73,38 @@
<constraint firstItem="ybt-dF-Rdi" firstAttribute="centerX" secondItem="BMl-ka-oh2" secondAttribute="centerX" id="vAu-hH-jPT"/>
</constraints>
</view>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="IEC-Gh-pGE">
<rect key="frame" x="302" y="405" width="51" height="31"/>
<connections>
<action selector="rangleSelectionEnabledToggled:" destination="vXZ-lx-hvc" eventType="valueChanged" id="HA1-Qc-RLd"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="enable range selection" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iqZ-iY-gSd">
<rect key="frame" x="165.5" y="413.5" width="128.5" height="14.5"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="128.5" id="jH2-7N-igb"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.94178299492385786" green="0.94178299492385786" blue="0.94178299492385786" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="IEC-Gh-pGE" firstAttribute="top" secondItem="ETR-rE-QOo" secondAttribute="bottom" constant="8" id="3aC-SB-LGi"/>
<constraint firstItem="ZI5-5E-Eq0" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="16" id="6CH-G3-Mhc"/>
<constraint firstItem="BMl-ka-oh2" firstAttribute="leading" secondItem="hW4-le-uM3" secondAttribute="leading" id="7Z1-Xk-xnJ"/>
<constraint firstItem="hW4-le-uM3" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="16" id="8wO-aF-0Eb"/>
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="ZI5-5E-Eq0" secondAttribute="bottom" constant="20" id="EM0-cT-QdX"/>
<constraint firstItem="BMl-ka-oh2" firstAttribute="top" secondItem="hW4-le-uM3" secondAttribute="bottom" constant="8" id="KWY-nY-Oys"/>
<constraint firstItem="hW4-le-uM3" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="16" id="MZh-zF-zaS"/>
<constraint firstItem="BMl-ka-oh2" firstAttribute="trailing" secondItem="hW4-le-uM3" secondAttribute="trailing" id="Prb-A3-Qwc"/>
<constraint firstItem="iqZ-iY-gSd" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" priority="250" constant="165.5" id="U2e-mI-54R"/>
<constraint firstItem="IEC-Gh-pGE" firstAttribute="leading" secondItem="iqZ-iY-gSd" secondAttribute="trailing" constant="8" id="Xek-LA-BMT"/>
<constraint firstItem="iqZ-iY-gSd" firstAttribute="centerY" secondItem="IEC-Gh-pGE" secondAttribute="centerY" id="fUF-2g-vwI"/>
<constraint firstAttribute="trailing" secondItem="ZI5-5E-Eq0" secondAttribute="trailing" constant="16" id="jgi-JH-Dcg"/>
<constraint firstAttribute="trailing" secondItem="hW4-le-uM3" secondAttribute="trailing" constant="16" id="vYW-BD-MiV"/>
<constraint firstItem="IEC-Gh-pGE" firstAttribute="trailing" secondItem="ETR-rE-QOo" secondAttribute="trailing" id="zFx-dn-rD1"/>
</constraints>
</view>
<connections>
Expand Down
26 changes: 11 additions & 15 deletions KDCalendar/CalendarView/CalendarView+Delegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,20 @@
import UIKit

extension CalendarView: UICollectionViewDelegateFlowLayout {

public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

guard let date = self.dateFromIndexPath(indexPath) else { return }

if let index = selectedIndexPaths.firstIndex(of: indexPath) {

delegate?.calendar(self, didDeselectDate: date)
if enableDeslection {
handleCollectionViewDidDeselectDate(date)
if enableDeselection {
// bug: when deselecting the second to last item programmatically, during
// didDeselectDate delegation, the index returned is out of the bounds of
// the selectedIndexPaths array. This guard prevents the crash
guard index < selectedIndexPaths.count, index < selectedDates.count else {
return
}
selectedIndexPaths.remove(at: index)
selectedDates.remove(at: index)
}
Expand All @@ -44,17 +49,8 @@ extension CalendarView: UICollectionViewDelegateFlowLayout {
self.reloadData()
return
}

if !multipleSelectionEnable {
selectedIndexPaths.removeAll()
selectedDates.removeAll()
}

selectedIndexPaths.append(indexPath)
selectedDates.append(date)

let eventsForDaySelected = eventsByIndexPath[indexPath] ?? []
delegate?.calendar(self, didSelectDate: date, withEvents: eventsForDaySelected)

handleCollectionViewDidSelectDate(date, indexPath: indexPath)
}

self.reloadData()
Expand Down
129 changes: 120 additions & 9 deletions KDCalendar/CalendarView/CalendarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@ import UIKit
import EventKit
#endif

struct EventLocation {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was dead code. Not referenced in the project

let title: String
let latitude: Double
let longitude: Double
}

public struct CalendarEvent {
public let title: String
public let startDate: Date
Expand Down Expand Up @@ -84,6 +78,12 @@ extension CalendarViewDelegate {
}

public class CalendarView: UIView {

public enum MultipleSelectionMode {
case single
case multiple
case range
}

public let cellReuseIdentifier = "CalendarDayCell"

Expand Down Expand Up @@ -144,8 +144,19 @@ public class CalendarView: UIView {
// MARK: - public

public internal(set) var displayDate: Date?
public var multipleSelectionEnable = true
public var enableDeslection = true

@available(*, deprecated, message: "Use `multipleSelectionMode` instead. `multipleSelectionEnable` equates to `multipleSelectionMode == .multiple`")
public var multipleSelectionEnable: Bool {
set (val) {
self.multipleSelectionMode = .multiple
}
get {
return self.multipleSelectionMode == .multiple
}
}

public var multipleSelectionMode: MultipleSelectionMode = .multiple
public var enableDeselection = true
public var marksWeekends = true

public var delegate: CalendarViewDelegate?
Expand Down Expand Up @@ -330,7 +341,98 @@ public class CalendarView: UIView {
internal func updateStyle() {
self.headerView?.style = style
}


internal func clearAllSelected() {
let wereSelectedDates = selectedDates.map{ $0 }
selectedIndexPaths.removeAll()
selectedDates.removeAll()
self.reloadData()
wereSelectedDates.forEach {delegate?.calendar(self, didDeselectDate: $0)}
}

// soft lock on selecting range
private var selectingRange = false

internal func selectRange(for dateSelected: Date) {
// while selecting range, let it finish
guard !selectingRange else {
return
}

let sortedDates = selectedDates.sorted()
// if more than one date, select a range
guard sortedDates.count > 1,
let startDate = sortedDates.first,
let endDate = sortedDates.last else {
return
}

// start selecting range
selectingRange = true
// complete selecting range upon exit
defer {selectingRange = false}
// count the days
let days = Calendar.current.dateComponents([.day], from: startDate, to: endDate).day ?? 1
// map to range of dates
let dates = stride(from: 0, to: days, by: 1)
.map { Calendar.current.date(byAdding: .day, value: $0, to: startDate) ?? Date() }
// select each date
dates.forEach { candiDate in
if !sortedDates.contains(candiDate) {
// selectDate will call delegate `calendar(_:, didSelectDate:, withEvents:)`
selectDate(candiDate)
}
}

}

internal func deselectRange(for unselectedDate: Date) {
guard selectedDates.count > 1,
let startDate = selectedDates.min(),
let endDate = selectedDates.max(),
startDate.compare(unselectedDate) == .orderedAscending,
unselectedDate.compare(endDate) == .orderedAscending else {
delegate?.calendar(self, didDeselectDate: unselectedDate)
return
}

clearAllSelected()

}

internal func handleCollectionViewDidSelectDate(_ date: Date, indexPath: IndexPath) {
// for single select, clear the selection
switch multipleSelectionMode {
case .single:
clearAllSelected()
case .multiple, .range:
break
}

selectedIndexPaths.append(indexPath)
selectedDates.append(date)

// for range select, select the range in the middle
switch multipleSelectionMode {
case .single, .multiple:
break
case .range:
selectRange(for: date)
}

let eventsForDaySelected = eventsByIndexPath[indexPath] ?? []
delegate?.calendar(self, didSelectDate: date, withEvents: eventsForDaySelected)
}

internal func handleCollectionViewDidDeselectDate(_ date: Date) {
switch multipleSelectionMode {
case .range where selectedDates.count > 1:
deselectRange(for: date)
case .single, .multiple, .range:
delegate?.calendar(self, didDeselectDate: date)
}
}

func scrollViewOffset(for date: Date) -> CGPoint {
var point = CGPoint.zero

Expand Down Expand Up @@ -473,6 +575,15 @@ extension CalendarView {
goToMonthWithOffet(-1)
}

/*
method: - clearAllSelectedDates
function: - clear all selected dates
*/
public func clearAllSelectedDates() {
clearAllSelected()
}


#if KDCALENDAR_EVENT_MANAGER_ENABLED

public func loadEvents(onComplete: ((Error?) -> Void)? = nil) {
Expand Down
14 changes: 8 additions & 6 deletions KDCalendar/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ import EventKit

class ViewController: UIViewController {


private let defaultSelectionMode: CalendarView.MultipleSelectionMode = .multiple
@IBOutlet weak var calendarView: CalendarView!
@IBOutlet weak var datePicker: UIDatePicker!



@IBAction func rangleSelectionEnabledToggled(_ sender: UISwitch) {
calendarView.multipleSelectionMode = sender.isOn ? .range : defaultSelectionMode
}

override func viewDidLoad() {

super.viewDidLoad()
Expand Down Expand Up @@ -70,7 +73,7 @@ class ViewController: UIViewController {
calendarView.delegate = self

calendarView.direction = .horizontal
calendarView.multipleSelectionEnable = false
calendarView.multipleSelectionMode = defaultSelectionMode
calendarView.marksWeekends = true


Expand Down Expand Up @@ -127,8 +130,7 @@ class ViewController: UIViewController {

override var prefersStatusBarHidden: Bool {
return true
}

}
}


Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Just the files from the **CalendarView/** subfolder to your project.

# Setup

The calendar is a `UIView` and can be added either programmatically or via a XIB/Storyboard. **If doing the latter, make sure that the Module is selected to be 'KDCalendar'**.
The calendar is a `UIView` and can be added either programmatically or via a XIB/Storyboard. **If doing the latter, make sure that the Module is selected to be 'KDCalendar' ('CalendarView_iOS') if installed via Carthage)**.

![IB Screenshot](https://github.com/mmick66/CalendarView/blob/master/Assets/Screenshot.png)

Expand Down Expand Up @@ -81,7 +81,7 @@ Note: The dates should be in UTC (same as GMT)

# How to Use

You would want to implement the delegate functions inside your view controller as they appear in the example project.
You would want to implement the delegate functions inside your view controller as they appear in the example project. Don't forget to assign your class as the `CalendarView` delegate.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that boggled me for a minute in the beginning. The example project set me straight


Say you want to be able to scroll 3 months into the past, then:

Expand Down Expand Up @@ -129,12 +129,18 @@ Similarly you can deselect:
self.calendarView.deselectDate(date)
```

You can get all the dates that were selected, either manually or programatically using:
You can get all the dates that were selected, either manually or programmatically using:

```Swift
self.calendarView.selectedDates
```

To support selecting a range of dates where any selection will automatically select all dates in between, use:

```Swift
self.calendarView.multipleSelectionMode = .range
```

### Layout

The calendar supports two basic layouts. Set the `direction` property to `.horizontal` or `.vertical`:
Expand Down