Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Commit

Permalink
Merged PR 282534: DatePicker - Tabbed mode for startDate/endDate
Browse files Browse the repository at this point in the history
Adds a tabbed mode to `MSDateTimePicker` and `MSDatePickerController` to allow for start and end date picking in a single View Controller.

* Adds new enum `MSDateTimePicker.RangePresentation` which is now optionally provided in `MSDateTimePicker.present()`. Defaults to `.paged`. Passing `.tabbed` will potentially change which controllers are presented and will use a single page with tabs of `MSDatePickerController` for selecting start and end date.

Related work items: #726021, #739256
  • Loading branch information
willrichman committed Jun 26, 2019
1 parent 915a963 commit d0083b7
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class MSDateTimePickerDemoController: DemoController {
container.addArrangedSubview(dateLabel)
container.addArrangedSubview(createButton(title: "Show date picker", action: #selector(presentDatePicker)))
container.addArrangedSubview(createButton(title: "Show date time picker", action: #selector(presentDateTimePicker)))
container.addArrangedSubview(createButton(title: "Show date range picker", action: #selector(presentDateRangePicker)))
container.addArrangedSubview(createButton(title: "Show date range picker (paged)", action: #selector(presentDateRangePicker)))
container.addArrangedSubview(createButton(title: "Show date range picker (tabbed)", action: #selector(presentTabbedDateRangePicker)))
container.addArrangedSubview(createButton(title: "Show date time range picker", action: #selector(presentDateTimeRangePicker)))
container.addArrangedSubview(createButton(title: "Show picker with custom subtitles", action: #selector(presentCustomSubtitlePicker)))
container.addArrangedSubview(UIView())
Expand Down Expand Up @@ -61,6 +62,12 @@ class MSDateTimePickerDemoController: DemoController {
dateTimePicker.present(from: self, with: .dateRange, startDate: startDate, endDate: endDate)
}

@objc func presentTabbedDateRangePicker() {
let startDate = self.startDate ?? Date()
let endDate = self.endDate ?? Calendar.current.date(byAdding: .day, value: 1, to: startDate) ?? startDate
dateTimePicker.present(from: self, with: .dateRange, startDate: startDate, endDate: endDate, dateRangePresentation: .tabbed)
}

@objc func presentDateTimeRangePicker() {
let startDate = self.startDate ?? Date()
let endDate = self.endDate ?? Calendar.current.date(byAdding: .hour, value: 1, to: startDate) ?? startDate
Expand Down
6 changes: 3 additions & 3 deletions OfficeUIFabric.Tests/MSDatePickerControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ class MSDatePickerControllerTests: XCTestCase {
let endDate = Date(timeIntervalSince1970: MSDatePickerControllerTests.testDateInterval).adding(days: 1)

func testDateRangeInit() {
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange)
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange, rangePresentation: .paged, titles: nil)
XCTAssertEqual(datePicker.startDate, startDate.startOfDay)
XCTAssertEqual(datePicker.endDate, endDate.startOfDay)
}

func testDateRangeStart() {
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange)
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange, rangePresentation: .paged, titles: nil)
guard case .range(let startIndex, _) = datePicker.selectionManager.selectionState else {
XCTFail()
return
Expand All @@ -31,7 +31,7 @@ class MSDatePickerControllerTests: XCTestCase {
}

func testDateRangeEnd() {
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange, selectionMode: .end)
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange, selectionMode: .end, rangePresentation: .paged, titles: nil)
guard case .range(_, let endIndex) = datePicker.selectionManager.selectionState else {
XCTFail()
return
Expand Down
7 changes: 6 additions & 1 deletion OfficeUIFabric/Calendar/Views/MSCalendarViewDayCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,12 @@ private class MSSelectionOverlayView: UIView {
struct Constants {
static let highlightedOrSelectedCircleMargin: CGFloat = 5.0
}
var selectionType: MSCalendarViewDayCellSelectionType = .singleSelection

var selectionType: MSCalendarViewDayCellSelectionType = .singleSelection {
didSet {
setupActiveViews()
}
}
var selectionStyle: MSCalendarViewDayCellSelectionStyle = .normal

// TODO: Add different colors for availability?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,15 @@ class MSDatePickerController: UIViewController, DateTimePicker {
static let idealWidth: CGFloat = 343
// TODO: Make title button width dynamic
static let titleButtonWidth: CGFloat = 160
static let calendarHeightStyle: MSCalendarViewHeightStyle = .extraTall
}

var startDate = Date() {
didSet {
startDate = mode.includesTime ? startDate : startDate.startOfDay
selectionManager.startDate = startDate
// If endDate goes past the visible dates, scroll the startDate up
if let visibleDates = visibleDates,
selectionManager.endDate > visibleDates.endDate {
scrollToStartDate(animated: true)
if !entireRangeIsVisible {
scrollToFocusDate(animated: true)
}
updateSelectionOfVisibleCells()
updateNavigationBar()
Expand All @@ -41,6 +40,9 @@ class MSDatePickerController: UIViewController, DateTimePicker {
didSet {
endDate = mode.includesTime ? endDate : endDate.startOfDay
selectionManager.endDate = endDate
if !entireRangeIsVisible {
scrollToFocusDate(animated: true)
}
updateSelectionOfVisibleCells()
updateNavigationBar()
}
Expand All @@ -63,13 +65,23 @@ class MSDatePickerController: UIViewController, DateTimePicker {

private var titleView: MSTwoLinesTitleView!
private let customTitle: String?
private let subtitle: String?
private let customSubtitle: String?
private let customStartTabTitle: String?
private let customEndTabTitle: String?

private var monthOverlayIsShown: Bool = false
private var reloadDataAfterOverlayIsNeeded: Bool = false

private let calendarView = MSCalendarView()
private var calendarViewDataSource: MSCalendarViewDataSource!
private var segmentedControl: MSSegmentedControl?

private var entireRangeIsVisible: Bool {
guard let visibleDates = visibleDates else {
return false
}
return selectionManager.endDate <= visibleDates.endDate && selectionManager.startDate >= visibleDates.startDate
}

// TODO: Add availability back in? - contactAvailabilitySummaryDataSource: ContactAvailabilitySummaryDataSource?,

Expand All @@ -80,12 +92,22 @@ class MSDatePickerController: UIViewController, DateTimePicker {
/// - endDate: A date object for an end day or day/time to be initially selected.
/// - datePickerMode: The MSDateTimePicker mode this is presented in.
/// - selectionMode: The side (start or end) of the current range to be selected on this picker.
/// - title: An optional string describing a title to override the default date label in the titleView
/// - subtitle: An optional string describing an optional subtitle for this date picker.
init(startDate: Date, endDate: Date, mode: MSDateTimePickerMode, selectionMode: MSDatePickerSelectionManager.SelectionMode = .start, title: String? = nil, subtitle: String? = nil) {
self.customTitle = title
self.subtitle = subtitle
/// - rangePresentation: The `DateRangePresentation` in which this controller is being presented if `mode` is `.dateRange` or `.dateTimeRange`.
/// - titles: A `Titles` object that holds strings for use in overriding the default picker title, subtitle, and tab titles. If title is not provided, titleview will show currently selected date. If tab titles are not provided, they will default to "Start Date" and "End Date".
init(startDate: Date, endDate: Date, mode: MSDateTimePickerMode, selectionMode: MSDatePickerSelectionManager.SelectionMode = .start, rangePresentation: MSDateTimePicker.DateRangePresentation, titles: MSDateTimePicker.Titles?) {
if !mode.singleSelection && rangePresentation == .paged {
customTitle = selectionMode == .start ? titles?.startTitle : titles?.endTitle
customSubtitle = selectionMode == .start ?
titles?.startSubtitle ?? "MSDateTimePicker.StartDate".localized :
titles?.endSubtitle ?? "MSDateTimePicker.EndDate".localized
} else {
customTitle = titles?.dateTitle
customSubtitle = titles?.dateSubtitle
}
customStartTabTitle = titles?.startTab
customEndTabTitle = titles?.endTab
self.mode = mode

super.init(nibName: nil, bundle: nil)

defer {
Expand All @@ -104,6 +126,10 @@ class MSDatePickerController: UIViewController, DateTimePicker {
)

initTitleView()

if !mode.singleSelection && rangePresentation == .tabbed {
initSegmentedControl()
}
}

required init?(coder aDecoder: NSCoder) {
Expand All @@ -129,6 +155,10 @@ class MSDatePickerController: UIViewController, DateTimePicker {

calendarView.collectionViewLayout.delegate = self

view.backgroundColor = MSColors.background
if let segmentedControl = segmentedControl {
view.addSubview(segmentedControl)
}
view.addSubview(calendarView)

initNavigationBar()
Expand All @@ -137,16 +167,26 @@ class MSDatePickerController: UIViewController, DateTimePicker {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

scrollToStartDate(animated: false)
scrollToFocusDate(animated: false)

// Hide default bottom border of navigation bar
navigationController?.navigationBar.hideBottomBorder()
if segmentedControl == nil {
// Hide default bottom border of navigation bar
navigationController?.navigationBar.hideBottomBorder()
}
}

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()

calendarView.frame = view.bounds
var calendarFrame = view.bounds
if let segmentedControl = segmentedControl {
var frame = calendarFrame
frame.size.height = segmentedControl.intrinsicContentSize.height
calendarFrame = calendarFrame.inset(by: UIEdgeInsets(top: frame.height, left: 0, bottom: 0, right: 0))

segmentedControl.frame = frame
}
calendarView.frame = calendarFrame
}

private func initTitleView() {
Expand All @@ -167,9 +207,16 @@ class MSDatePickerController: UIViewController, DateTimePicker {
navigationItem.titleView = titleView
}

private func initSegmentedControl() {
let titles = [customStartTabTitle ?? "MSDateTimePicker.StartDate".localized,
customEndTabTitle ?? "MSDateTimePicker.EndDate".localized]
segmentedControl = MSSegmentedControl(items: titles)
segmentedControl?.addTarget(self, action: #selector(handleDidSelectStartEnd(_:)), for: .valueChanged)
}

private func updateNavigationBar() {
let title = customTitle ?? String.dateString(from: focusDate, compactness: .shortDaynameShortMonthnameDay)
titleView.setup(title: title, subtitle: subtitle)
titleView.setup(title: title, subtitle: customSubtitle)
updateTitleFrame()
}

Expand All @@ -184,8 +231,22 @@ class MSDatePickerController: UIViewController, DateTimePicker {
}
}

private func scrollToStartDate(animated: Bool) {
let targetIndexPath = IndexPath(item: 0, section: max(selectionManager.startDateIndexPath.section - 1, 0))
private func scrollToFocusDate(animated: Bool) {
let numberOfRows = Int(calendarView.rows(for: Constants.calendarHeightStyle))
let selectionFitsInCalendar = selectionManager.endDateIndexPath.section - selectionManager.startDateIndexPath.section <= numberOfRows - 1
let focusDateRow: Int
let rowOffset: Int
if selectionManager.selectionMode == .start || selectionFitsInCalendar {
focusDateRow = selectionManager.startDateIndexPath.section
rowOffset = 1
} else {
focusDateRow = selectionManager.endDateIndexPath.section
rowOffset = max(numberOfRows - 2, 1)
}
guard focusDateRow < calendarView.collectionView.numberOfSections else {
return
}
let targetIndexPath = IndexPath(item: 0, section: max(focusDateRow - rowOffset, 0))
calendarView.collectionView.scrollToItem(at: targetIndexPath, at: [.top], animated: animated)
// TODO: Notify of visible date?
}
Expand Down Expand Up @@ -226,12 +287,20 @@ class MSDatePickerController: UIViewController, DateTimePicker {
// MARK: Handlers

@objc private func handleTitleButtonTapped() {
scrollToStartDate(animated: true)
scrollToFocusDate(animated: true)
}

@objc private func handleDidTapDone() {
dismiss()
}

@objc private func handleDidSelectStartEnd(_ segmentedControl: MSSegmentedControl) {
selectionManager.selectionMode = segmentedControl.selectedSegmentIndex == 0 ? .start : .end
updateNavigationBar()
if let visibleDates = visibleDates, focusDate > visibleDates.endDate || focusDate < visibleDates.startDate {
scrollToFocusDate(animated: false)
}
}
}

// MARK: - MSDatePickerController: UICollectionViewDelegate
Expand All @@ -252,7 +321,7 @@ extension MSDatePickerController: UICollectionViewDelegate {

dayCell.setVisualState((monthOverlayIsShown ? .fadedWithDots : .normal), animated: false)

updateSelectionOfVisibleCells()
updateSelectionOfCell(dayCell, at: indexPath)
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
Expand All @@ -267,7 +336,6 @@ extension MSDatePickerController: UICollectionViewDelegate {
// Update selection of visible cells
selectionManager.setSelectedIndexPath(indexPath)
updateDates()
updateSelectionOfVisibleCells()

delegate?.dateTimePicker(self, didSelectStartDate: startDate, endDate: endDate)

Expand Down Expand Up @@ -299,13 +367,27 @@ extension MSDatePickerController: UICollectionViewDelegate {
private func updateSelectionOfCell(at indexPath: IndexPath) {
let collectionView = calendarView.collectionView

if let selectionType = selectionManager.selectionType(for: indexPath),
let cell = collectionView.cellForItem(at: indexPath) as? MSCalendarViewDayCell {
cell.setSelectionType(selectionType)
guard let cell = collectionView.cellForItem(at: indexPath) as? MSCalendarViewDayCell else {
return
}

collectionView.selectItem(at: indexPath, animated: false, scrollPosition: UICollectionView.ScrollPosition())
updateSelectionOfCell(cell, at: indexPath)
}

private func updateSelectionOfCell(_ cell: MSCalendarViewDayCell, at indexPath: IndexPath) {
let collectionView = calendarView.collectionView

if let selectionType = selectionManager.selectionType(for: indexPath) {
cell.setSelectionType(selectionType)
if !cell.isSelected {
cell.isSelected = true
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
}
} else {
collectionView.deselectItem(at: indexPath, animated: false)
if cell.isSelected {
cell.isSelected = false
collectionView.deselectItem(at: indexPath, animated: false)
}
}
}
}
Expand Down Expand Up @@ -393,6 +475,9 @@ extension MSDatePickerController: MSCalendarViewStyleDataSource {

extension MSDatePickerController: MSCardPresentable {
func idealSize() -> CGSize {
return CGSize(width: Constants.idealWidth, height: calendarView.height(for: .extraTall, in: view.bounds))
return CGSize(
width: Constants.idealWidth,
height: calendarView.height(for: Constants.calendarHeightStyle, in: view.bounds) + (segmentedControl?.height ?? 0)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class MSDatePickerSelectionManager {
}

private(set) var selectionState: SelectionState
let selectionMode: SelectionMode
var selectionMode: SelectionMode

var startDateIndexPath: IndexPath {
switch selectionState {
Expand All @@ -31,6 +31,15 @@ class MSDatePickerSelectionManager {
}
}

var endDateIndexPath: IndexPath {
switch selectionState {
case let .single(selectedIndexPath):
return selectedIndexPath
case let .range(_, endIndexPath):
return endIndexPath
}
}

var startDate: Date {
get {
return dataSource.dayStart(forDayAt: selectedIndexPaths.startIndexPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ class MSDateTimePickerController: UIViewController, DateTimePicker {
initNavigationBar()
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let segmentedControl = segmentedControl {
segmentedControl.frame = CGRect(x: 0, y: 0, width: view.width, height: segmentedControl.intrinsicContentSize.height)
}
Expand Down Expand Up @@ -187,6 +187,9 @@ class MSDateTimePickerController: UIViewController, DateTimePicker {
startDate = datePicker.date
case .end:
endDate = datePicker.date
if endDate < startDate {
startDate = endDate
}
}
delegate?.dateTimePicker(self, didSelectStartDate: startDate, endDate: endDate)
}
Expand All @@ -205,7 +208,9 @@ class MSDateTimePickerController: UIViewController, DateTimePicker {

extension MSDateTimePickerController: MSCardPresentable {
func idealSize() -> CGSize {
let height = MSDateTimePickerViewLayout.height(forRowCount: Constants.idealRowCount) + (segmentedControl?.height ?? 0)
return CGSize(width: Constants.idealWidth, height: height)
return CGSize(
width: Constants.idealWidth,
height: MSDateTimePickerViewLayout.height(forRowCount: Constants.idealRowCount) + (segmentedControl?.height ?? 0)
)
}
}
Loading

0 comments on commit d0083b7

Please sign in to comment.