Merged PR 224715: MSDatePicker Range Support
Adds support for picking a range of dates or times with `MSDatePicker`. When presenting a picker with `MSDatePicker`, calling `present(from presentingController:with:for:endDate:)` with an `endDate` included will set the picker to duration mode. Also includes some miscellaneous bug fixes for `MSDatePickerController` and `MSDateTimePickerController`. ![Screen Shot 1.png](https://onedrive.visualstudio.com/4dcbf0bc-c3cd-49c8-a7c3-ec1924691d9b/_apis/git/repositories/93ac71ee-b53a-4fc6-a8c4-d46a80d4ca39/pullRequests/224715/attachments/Screen%20Shot%201.png) ![Screen Shot 2.png](https://onedrive.visualstudio.com/4dcbf0bc-c3cd-49c8-a7c3-ec1924691d9b/_apis/git/repositories/93ac71ee-b53a-4fc6-a8c4-d46a80d4ca39/pullRequests/224715/attachments/Screen%20Shot%202.png) Related work items: #659380, #678429
This commit is contained in:
Родитель
bd7c6308a6
Коммит
630558d7bb
|
@ -26,7 +26,18 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
codeCoverageEnabled = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "YES"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A5CEC15020D980B20016922A"
|
||||
BuildableName = "OfficeUIFabric.framework"
|
||||
BlueprintName = "OfficeUIFabric"
|
||||
ReferencedContainer = "container:../OfficeUIFabric.xcodeproj">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
|
|
|
@ -10,33 +10,76 @@ class MSDateTimePickerDemoController: DemoController {
|
|||
private let dateLabel = MSLabel(style: .headline)
|
||||
private let dateTimePicker = MSDateTimePicker()
|
||||
|
||||
private var startDate: Date?
|
||||
private var endDate: Date?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
dateTimePicker.delegate = self
|
||||
dateLabel.text = "No date selected"
|
||||
dateLabel.adjustsFontSizeToFitWidth = true
|
||||
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 time range picker", action: #selector(presentDateTimeRangePicker)))
|
||||
container.addArrangedSubview(UIView())
|
||||
container.addArrangedSubview(createButton(title: "Reset selected dates", action: #selector(resetDates)))
|
||||
}
|
||||
|
||||
@objc func presentDatePicker() {
|
||||
dateTimePicker.present(from: self, with: .date)
|
||||
dateTimePicker.present(from: self, with: .date, startDate: startDate ?? Date())
|
||||
}
|
||||
|
||||
@objc func presentDateTimePicker() {
|
||||
dateTimePicker.present(from: self, with: .dateTime)
|
||||
dateTimePicker.present(from: self, with: .dateTime, startDate: startDate ?? Date())
|
||||
}
|
||||
|
||||
@objc func presentDateRangePicker() {
|
||||
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)
|
||||
}
|
||||
|
||||
@objc func presentDateTimeRangePicker() {
|
||||
let startDate = self.startDate ?? Date()
|
||||
let endDate = self.endDate ?? Calendar.current.date(byAdding: .hour, value: 1, to: startDate) ?? startDate
|
||||
dateTimePicker.present(from: self, with: .dateTimeRange, startDate: startDate, endDate: endDate)
|
||||
}
|
||||
|
||||
@objc func resetDates() {
|
||||
startDate = nil
|
||||
endDate = nil
|
||||
dateLabel.text = "No date selected"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MSDateTimePickerDemoController: MSDatePickerDelegate
|
||||
|
||||
extension MSDateTimePickerDemoController: MSDateTimePickerDelegate {
|
||||
func dateTimePicker(_ dateTimePicker: MSDateTimePicker, didPickDate date: Date) {
|
||||
var compactness = MSDateStringCompactness.longDaynameDayMonthYear
|
||||
if dateTimePicker.mode == .dateTime {
|
||||
compactness = .longDaynameDayMonthHoursColumnsMinutes
|
||||
func dateTimePicker(_ dateTimePicker: MSDateTimePicker, didPickStartDate startDate: Date, endDate: Date) {
|
||||
guard let mode = dateTimePicker.mode else {
|
||||
fatalError("Received delegate call when mode = nil")
|
||||
}
|
||||
dateLabel.text = String.dateString(from: date, compactness: compactness)
|
||||
self.startDate = startDate
|
||||
let compactness: MSDateStringCompactness
|
||||
if mode.singleSelection {
|
||||
if mode.includesTime {
|
||||
compactness = .longDaynameDayMonthHoursColumnsMinutes
|
||||
} else {
|
||||
compactness = .longDaynameDayMonthYear
|
||||
}
|
||||
dateLabel.text = String.dateString(from: startDate, compactness: compactness)
|
||||
} else {
|
||||
self.endDate = endDate
|
||||
if mode.includesTime {
|
||||
compactness = .shortDaynameShortMonthnameHoursColumnsMinutes
|
||||
} else {
|
||||
compactness = .shortDaynameDayShortMonthYear
|
||||
}
|
||||
dateLabel.text = String.dateString(from: startDate, compactness: compactness) + " - " + String.dateString(from: endDate, compactness: compactness)
|
||||
}
|
||||
|
||||
dateTimePicker.dismiss()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import OfficeUIFabric
|
||||
|
||||
class MSDatePickerControllerTests: XCTestCase {
|
||||
static let testDateInterval: TimeInterval = 1551903381
|
||||
|
||||
let startDate = Date(timeIntervalSince1970: MSDatePickerControllerTests.testDateInterval)
|
||||
let endDate = Date(timeIntervalSince1970: MSDatePickerControllerTests.testDateInterval).adding(days: 1)
|
||||
|
||||
func testDateRangeInit() {
|
||||
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange)
|
||||
XCTAssertEqual(datePicker.startDate, startDate.startOfDay)
|
||||
XCTAssertEqual(datePicker.endDate, endDate.startOfDay)
|
||||
}
|
||||
|
||||
func testDateRangeStart() {
|
||||
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange)
|
||||
guard case .range(let startIndex, _) = datePicker.selectionManager.selectionState else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
let indexPath = IndexPath(item: startIndex.item + 1, section: startIndex.section)
|
||||
datePicker.didTapItem(at: indexPath)
|
||||
XCTAssertEqual(datePicker.startDate, startDate.adding(days: 1).startOfDay)
|
||||
XCTAssertEqual(datePicker.endDate, endDate.adding(days: 1).startOfDay)
|
||||
}
|
||||
|
||||
func testDateRangeEnd() {
|
||||
let datePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: .dateRange, selectionMode: .end)
|
||||
guard case .range(_, let endIndex) = datePicker.selectionManager.selectionState else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
let indexPath = IndexPath(item: endIndex.item + 1, section: endIndex.section)
|
||||
datePicker.didTapItem(at: indexPath)
|
||||
XCTAssertEqual(datePicker.startDate, startDate.startOfDay)
|
||||
XCTAssertEqual(datePicker.endDate, endDate.adding(days: 1).startOfDay)
|
||||
}
|
||||
|
||||
func testSelectionManagerEnd() {
|
||||
let dataSource = MSCalendarViewDataSource(styleDataSource: MockMSCalendarViewStyleDataSource())
|
||||
let selectionManager = MSDatePickerSelectionManager(
|
||||
dataSource: dataSource,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
selectionMode: .end
|
||||
)
|
||||
|
||||
guard case .range(_, let endIndex) = selectionManager.selectionState else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
|
||||
let nextIndexPath = IndexPath(item: endIndex.item + 1, section: endIndex.section)
|
||||
selectionManager.setSelectedIndexPath(nextIndexPath)
|
||||
|
||||
XCTAssertEqual(selectionManager.endDate, endDate.adding(days: 1).startOfDay)
|
||||
|
||||
guard case .range = selectionManager.selectionState else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
|
||||
// Test transition from ranged to single date and back
|
||||
let previousIndexPath = IndexPath(item: endIndex.item - 1, section: endIndex.section)
|
||||
selectionManager.setSelectedIndexPath(previousIndexPath)
|
||||
|
||||
XCTAssertEqual(selectionManager.endDate, startDate.startOfDay)
|
||||
|
||||
guard case .single = selectionManager.selectionState else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
|
||||
selectionManager.setSelectedIndexPath(nextIndexPath)
|
||||
|
||||
XCTAssertEqual(selectionManager.endDate, endDate.adding(days: 1).startOfDay)
|
||||
|
||||
guard case .range = selectionManager.selectionState else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MSDatePickerController
|
||||
|
||||
class MockMSCalendarViewStyleDataSource: MSCalendarViewStyleDataSource {
|
||||
func calendarViewDataSource(_ dataSource: MSCalendarViewDataSource, textStyleForDayWithStart dayStartDate: Date, end: Date, dayStartComponents: DateComponents, todayComponents: DateComponents) -> MSCalendarViewDayCellTextStyle {
|
||||
if dayStartComponents.dateIsTodayOrLater(todayDateComponents: todayComponents) {
|
||||
return .primary
|
||||
} else {
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
func calendarViewDataSource(_ dataSource: MSCalendarViewDataSource, backgroundStyleForDayWithStart dayStartDate: Date, end: Date, dayStartComponents: DateComponents, todayComponents: DateComponents
|
||||
) -> MSCalendarViewDayCellBackgroundStyle {
|
||||
if dayStartComponents.dateIsTodayOrLater(todayDateComponents: todayComponents) {
|
||||
return .primary
|
||||
} else {
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
func calendarViewDataSource(_ dataSource: MSCalendarViewDataSource, selectionStyleForDayWithStart dayStartDate: Date, end: Date) -> MSCalendarViewDayCellSelectionStyle {
|
||||
return .normal
|
||||
}
|
||||
}
|
|
@ -57,6 +57,7 @@
|
|||
B4E782C521793BB900A7DFCE /* MSActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E782C421793BB900A7DFCE /* MSActivityIndicatorView.swift */; };
|
||||
B4E782C72179509A00A7DFCE /* MSCenteredLabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E782C62179509A00A7DFCE /* MSCenteredLabelCell.swift */; };
|
||||
B4EF53C3215AF1AB00573E8F /* MSPersona.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EF53C2215AF1AB00573E8F /* MSPersona.swift */; };
|
||||
FD053A352224CA33009B6378 /* MSDatePickerControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD053A342224CA33009B6378 /* MSDatePickerControllerTests.swift */; };
|
||||
FD0D29D62151A3D700E8655E /* MSCardPresenterNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0D29D52151A3D700E8655E /* MSCardPresenterNavigationController.swift */; };
|
||||
FD256C5B2183B90B00EC9588 /* MSDatePickerSelectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD256C5A2183B90B00EC9588 /* MSDatePickerSelectionManager.swift */; };
|
||||
FD36F1A9216C0A6900CECBC6 /* MSCardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1AF9221487225001AE720 /* MSCardPresentationController.swift */; };
|
||||
|
@ -163,6 +164,7 @@
|
|||
B4E782C421793BB900A7DFCE /* MSActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSActivityIndicatorView.swift; sourceTree = "<group>"; };
|
||||
B4E782C62179509A00A7DFCE /* MSCenteredLabelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSCenteredLabelCell.swift; sourceTree = "<group>"; };
|
||||
B4EF53C2215AF1AB00573E8F /* MSPersona.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPersona.swift; sourceTree = "<group>"; };
|
||||
FD053A342224CA33009B6378 /* MSDatePickerControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSDatePickerControllerTests.swift; sourceTree = "<group>"; };
|
||||
FD0D29D52151A3D700E8655E /* MSCardPresenterNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSCardPresenterNavigationController.swift; sourceTree = "<group>"; };
|
||||
FD256C5A2183B90B00EC9588 /* MSDatePickerSelectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSDatePickerSelectionManager.swift; sourceTree = "<group>"; };
|
||||
FD4F2A1A2148937100C437D6 /* MSPageCardPresenterController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPageCardPresenterController.swift; sourceTree = "<group>"; };
|
||||
|
@ -384,6 +386,7 @@
|
|||
A5CEC15E20D980B30016922A /* OfficeUIFabric.Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD053A342224CA33009B6378 /* MSDatePickerControllerTests.swift */,
|
||||
A5CEC15F20D980B30016922A /* OfficeUIFabricTests.swift */,
|
||||
A5CEC16120D980B30016922A /* Info.plist */,
|
||||
);
|
||||
|
@ -827,6 +830,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5CEC16020D980B30016922A /* OfficeUIFabricTests.swift in Sources */,
|
||||
FD053A352224CA33009B6378 /* MSDatePickerControllerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -216,9 +216,9 @@ extension MSCalendarViewDataSource: UICollectionViewDataSource {
|
|||
|
||||
if index >= 0 && index < standaloneMonthSymbols.count {
|
||||
monthLabelText = standaloneMonthSymbols[index]
|
||||
} else {
|
||||
monthLabelText = ""
|
||||
}
|
||||
|
||||
monthLabelText = ""
|
||||
} else {
|
||||
monthLabelText = String.dateString(from: firstDayStartDateOfWeek, compactness: .longMonthNameFullYear)
|
||||
}
|
||||
|
|
|
@ -7,24 +7,36 @@ import Foundation
|
|||
|
||||
// MARK: MSCalendarConfiguration
|
||||
|
||||
@objcMembers
|
||||
open class MSCalendarConfiguration: NSObject {
|
||||
private struct Constants {
|
||||
static let baseReferenceStartTimestamp: TimeInterval = 1420416000 // January 3 2015
|
||||
static let baseReferenceEndTimestamp: TimeInterval = 1736035200 // January 5 2025
|
||||
static let startYearsAgo: Int = -3
|
||||
static let endYearsInterval: Int = 10
|
||||
}
|
||||
|
||||
// swiftlint:disable:next explicit_type_interface
|
||||
@objc public static let `default` = MSCalendarConfiguration()
|
||||
public static let `default` = MSCalendarConfiguration()
|
||||
|
||||
private struct DateConstants {
|
||||
static let referenceStartTimestamp: TimeInterval = 1262476800 // January 3 2010
|
||||
static let referenceEndTimestamp: TimeInterval = 1578182400 // January 5 2020
|
||||
}
|
||||
open var firstWeekday: Int = Calendar.current.firstWeekday
|
||||
|
||||
@objc open var referenceStartDate: Date {
|
||||
return Date(timeIntervalSince1970: DateConstants.referenceStartTimestamp)
|
||||
}
|
||||
let referenceStartDate: Date
|
||||
let referenceEndDate: Date
|
||||
|
||||
@objc open var referenceEndDate: Date {
|
||||
return Date(timeIntervalSince1970: DateConstants.referenceEndTimestamp)
|
||||
}
|
||||
init(calendar: Calendar = .current) {
|
||||
// Compute a start date (January 1st on a year a default number of years ago)
|
||||
let yearsAgo = calendar.dateByAdding(years: Constants.startYearsAgo, to: Date())
|
||||
var components = calendar.dateComponents([.year, .month, .day], from: yearsAgo)
|
||||
components.month = 1
|
||||
components.day = 1
|
||||
|
||||
@objc open var accessibilityShouldAnnounceIndicatorLevel: Bool {
|
||||
return true
|
||||
guard let slidingStartDate = calendar.date(from: components) else {
|
||||
fatalError("Cannot construct date from years ago components")
|
||||
}
|
||||
|
||||
referenceStartDate = slidingStartDate
|
||||
referenceEndDate = calendar.dateByAdding(years: Constants.endYearsInterval, to: slidingStartDate)
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,35 +14,37 @@ import Foundation
|
|||
|
||||
// MARK: - MSDatePickerController
|
||||
|
||||
/**
|
||||
* Represents a date picker, that enables the user to scroll through years vertically week by week.
|
||||
* The user can select a date or a range of dates.
|
||||
*/
|
||||
/// Represents a date picker, that enables the user to scroll through years vertically week by week.
|
||||
/// The user can select a date or a range of dates.
|
||||
class MSDatePickerController: UIViewController, DateTimePicker {
|
||||
private struct Constants {
|
||||
static let idealWidth: CGFloat = 343
|
||||
// TODO: Make title button width dynamic
|
||||
static let titleButtonWidth: CGFloat = 160
|
||||
static let preloadAvailabilityDaysOffset: Int = 30
|
||||
}
|
||||
|
||||
// Temporary date property for single date selection and DateTimePicker conformance. Will remove when DateTimePicker is refactored to include start and end date.
|
||||
/// The currently selected whole date. Automatically changes to start of day when set.
|
||||
var date: Date {
|
||||
get {
|
||||
return startDate
|
||||
}
|
||||
set {
|
||||
let startDate = newValue.startOfDay
|
||||
setup(startDate: startDate, endDate: startDate.adding(hours: 23, minutes: 59))
|
||||
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)
|
||||
}
|
||||
updateSelectionOfVisibleCells()
|
||||
updateNavigationBar()
|
||||
}
|
||||
}
|
||||
|
||||
var firstWeekday: Int = Calendar.current.firstWeekday
|
||||
|
||||
var startDate: Date { return selectionManager.selectedDates.startDate }
|
||||
var endDate: Date { return selectionManager.selectedDates.endDate }
|
||||
var endDate = Date() {
|
||||
didSet {
|
||||
endDate = mode.includesTime ? endDate : endDate.startOfDay
|
||||
selectionManager.endDate = endDate
|
||||
updateSelectionOfVisibleCells()
|
||||
updateNavigationBar()
|
||||
}
|
||||
}
|
||||
|
||||
var visibleDates: (startDate: Date, endDate: Date)? {
|
||||
let contentOffset = calendarView.collectionView.contentOffset
|
||||
|
@ -53,15 +55,18 @@ class MSDatePickerController: UIViewController, DateTimePicker {
|
|||
return selectionManager.selectionMode == .start ? startDate : endDate
|
||||
}
|
||||
|
||||
private(set) var selectionManager: MSDatePickerSelectionManager!
|
||||
|
||||
weak var delegate: DateTimePickerDelegate?
|
||||
|
||||
private let mode: MSDateTimePickerMode
|
||||
|
||||
private var titleView: MSTwoLinesTitleView!
|
||||
private let subtitle: String?
|
||||
|
||||
private var monthOverlayIsShown: Bool = false
|
||||
private var reloadDataAfterOverlayIsNeeded: Bool = false
|
||||
|
||||
private var selectionManager: MSDatePickerSelectionManager!
|
||||
private let calendarView = MSCalendarView()
|
||||
private var calendarViewDataSource: MSCalendarViewDataSource!
|
||||
|
||||
|
@ -70,21 +75,28 @@ class MSDatePickerController: UIViewController, DateTimePicker {
|
|||
/// Creates and sets up a calendar-style date picker, with a specified date shown first.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - startDate: A date object for the start day or day/time to be initially selected. Until range implemented, changes to start of day.
|
||||
/// - endDate: An optional date object for an end day or day/time to be initially selected. Until range implemented, ignored.
|
||||
/// - startDate: A date object for the start day or day/time to be initially selected.
|
||||
/// - 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.
|
||||
/// - subtitle: An optional string describing an optional subtitle for this date picker.
|
||||
init(startDate: Date, endDate: Date? = nil, selectionMode: MSDatePickerSelectionManager.SelectionMode = .start, subtitle: String? = nil) {
|
||||
init(startDate: Date, endDate: Date, mode: MSDateTimePickerMode, selectionMode: MSDatePickerSelectionManager.SelectionMode = .start, subtitle: String? = nil) {
|
||||
self.subtitle = subtitle
|
||||
self.mode = mode
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
defer {
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
}
|
||||
|
||||
calendarViewDataSource = MSCalendarViewDataSource(styleDataSource: self)
|
||||
|
||||
let startDate = startDate.startOfDay
|
||||
selectionManager = MSDatePickerSelectionManager(
|
||||
dataSource: calendarViewDataSource,
|
||||
startDate: startDate,
|
||||
endDate: startDate.adding(hours: 23, minutes: 59),
|
||||
endDate: endDate,
|
||||
selectionMode: selectionMode
|
||||
)
|
||||
|
||||
|
@ -95,27 +107,10 @@ class MSDatePickerController: UIViewController, DateTimePicker {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup(startDate: Date, endDate: Date) {
|
||||
let needsScroll = startDate != self.startDate
|
||||
|
||||
selectionManager = MSDatePickerSelectionManager(
|
||||
dataSource: calendarViewDataSource,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
selectionMode: selectionManager.selectionMode
|
||||
)
|
||||
|
||||
reloadData()
|
||||
|
||||
if needsScroll {
|
||||
scrollToStartDate(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
calendarView.weekdayHeadingView.setup(horizontalSizeClass: traitCollection.horizontalSizeClass, firstWeekday: firstWeekday)
|
||||
calendarView.weekdayHeadingView.setup(horizontalSizeClass: traitCollection.horizontalSizeClass, firstWeekday: MSCalendarConfiguration.default.firstWeekday)
|
||||
|
||||
let collectionView = calendarView.collectionView
|
||||
|
||||
|
@ -162,7 +157,9 @@ class MSDatePickerController: UIViewController, DateTimePicker {
|
|||
private func initNavigationBar() {
|
||||
if let image = UIImage.staticImageNamed("checkmark-blue-25x25"),
|
||||
let landscapeImage = UIImage.staticImageNamed("checkmark-blue-thin-20x20") {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, landscapeImagePhone: landscapeImage, style: .plain, target: self, action: #selector(handleDidTapDone))
|
||||
let doneButton = UIBarButtonItem(image: image, landscapeImagePhone: landscapeImage, style: .plain, target: self, action: #selector(handleDidTapDone))
|
||||
doneButton.accessibilityLabel = "Accessibility.Done.Label".localized
|
||||
navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
navigationItem.titleView = titleView
|
||||
}
|
||||
|
@ -211,7 +208,7 @@ class MSDatePickerController: UIViewController, DateTimePicker {
|
|||
let collectionView = calendarView.collectionView
|
||||
let maxStartContentOffsetY = collectionView.contentSize.height - collectionView.height
|
||||
let startPoint = CGPoint(x: contentOffset.x, y: min(max(contentOffset.y, 0), maxStartContentOffsetY))
|
||||
let endPoint = CGPoint(x: collectionView.width, y: startPoint.y + collectionView.height - 1)
|
||||
let endPoint = CGPoint(x: collectionView.width - 1, y: startPoint.y + collectionView.height - 1)
|
||||
|
||||
guard let startIndexPath = collectionView.indexPathForItem(at: startPoint),
|
||||
let endIndexPath = collectionView.indexPathForItem(at: endPoint) else {
|
||||
|
@ -230,7 +227,7 @@ class MSDatePickerController: UIViewController, DateTimePicker {
|
|||
}
|
||||
|
||||
@objc private func handleDidTapDone() {
|
||||
delegate?.dateTimePicker(self, didPickDate: date)
|
||||
delegate?.dateTimePicker(self, didPickStartDate: startDate, endDate: endDate)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,17 +260,33 @@ extension MSDatePickerController: UICollectionViewDelegate {
|
|||
didTapItem(at: indexPath)
|
||||
}
|
||||
|
||||
private func didTapItem(at indexPath: IndexPath) {
|
||||
func didTapItem(at indexPath: IndexPath) {
|
||||
// Update selection of visible cells
|
||||
selectionManager.setSelectedIndexPath(indexPath)
|
||||
updateDates()
|
||||
updateSelectionOfVisibleCells()
|
||||
|
||||
let (startDate, _) = selectionManager.selectedDates
|
||||
delegate?.dateTimePicker(self, didSelectDate: startDate)
|
||||
delegate?.dateTimePicker(self, didSelectStartDate: startDate, endDate: endDate)
|
||||
|
||||
updateNavigationBar()
|
||||
}
|
||||
|
||||
private func updateDates() {
|
||||
// If time is included, combine the day/month/year components of selectionManager with the time components from the old date. Ensures we don't lose the time, since selection manager only holds day releated components.
|
||||
if mode.includesTime, let newStartDate = selectionManager.startDate.combine(withTime: startDate) {
|
||||
startDate = newStartDate
|
||||
} else {
|
||||
startDate = selectionManager.startDate
|
||||
}
|
||||
if mode.singleSelection {
|
||||
endDate = startDate
|
||||
} else if mode.includesTime, let newEndDate = selectionManager.endDate.combine(withTime: endDate) {
|
||||
endDate = newEndDate
|
||||
} else {
|
||||
endDate = selectionManager.endDate
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSelectionOfVisibleCells() {
|
||||
for visibleIndexPath in calendarView.collectionView.indexPathsForVisibleItems {
|
||||
updateSelectionOfCell(at: visibleIndexPath)
|
||||
|
@ -292,25 +305,6 @@ extension MSDatePickerController: UICollectionViewDelegate {
|
|||
collectionView.deselectItem(at: indexPath, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func changeMonthOverlayVisibility(_ visible: Bool) {
|
||||
guard visible != monthOverlayIsShown else {
|
||||
return
|
||||
}
|
||||
monthOverlayIsShown = visible
|
||||
|
||||
for monthBannerViewValue in calendarViewDataSource.monthBannerViewSet {
|
||||
if let monthBannerView = monthBannerViewValue.nonretainedObjectValue as? MSCalendarViewMonthBannerView {
|
||||
monthBannerView.setVisible(visible, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
for cell in calendarView.collectionView.visibleCells {
|
||||
if let dayCell = cell as? MSCalendarViewDayCell {
|
||||
dayCell.setVisualState(visible ? .fadedWithDots : .normal, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MSDatePickerController: UIScrollViewDelegate
|
||||
|
@ -334,6 +328,25 @@ extension MSDatePickerController: UIScrollViewDelegate {
|
|||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
changeMonthOverlayVisibility(false)
|
||||
}
|
||||
|
||||
private func changeMonthOverlayVisibility(_ visible: Bool) {
|
||||
guard visible != monthOverlayIsShown else {
|
||||
return
|
||||
}
|
||||
monthOverlayIsShown = visible
|
||||
|
||||
for monthBannerViewValue in calendarViewDataSource.monthBannerViewSet {
|
||||
if let monthBannerView = monthBannerViewValue.nonretainedObjectValue as? MSCalendarViewMonthBannerView {
|
||||
monthBannerView.setVisible(visible, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
for cell in calendarView.collectionView.visibleCells {
|
||||
if let dayCell = cell as? MSCalendarViewDayCell {
|
||||
dayCell.setVisualState(visible ? .fadedWithDots : .normal, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MSDatePickerController: MSCalendarViewLayoutDelegate
|
||||
|
|
|
@ -19,7 +19,7 @@ class MSDatePickerSelectionManager {
|
|||
case range(IndexPath, IndexPath)
|
||||
}
|
||||
|
||||
var selectionState: SelectionState
|
||||
private(set) var selectionState: SelectionState
|
||||
let selectionMode: SelectionMode
|
||||
|
||||
var startDateIndexPath: IndexPath {
|
||||
|
@ -31,8 +31,34 @@ class MSDatePickerSelectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
var startDate: Date {
|
||||
get {
|
||||
return dataSource.dayStart(forDayAt: selectedIndexPaths.startIndexPath)
|
||||
}
|
||||
set {
|
||||
setSelectedIndexPath(dataSource.indexPath(forDayWithStart: newValue), mode: .start)
|
||||
}
|
||||
}
|
||||
var endDate: Date {
|
||||
get {
|
||||
return dataSource.dayStart(forDayAt: selectedIndexPaths.endIndexPath)
|
||||
}
|
||||
set {
|
||||
setSelectedIndexPath(dataSource.indexPath(forDayWithStart: newValue), mode: .end)
|
||||
}
|
||||
}
|
||||
|
||||
private let dataSource: MSCalendarViewDataSource
|
||||
|
||||
private var selectedIndexPaths: (startIndexPath: IndexPath, endIndexPath: IndexPath) {
|
||||
switch selectionState {
|
||||
case let .single(selectedIndexPath):
|
||||
return (selectedIndexPath, selectedIndexPath)
|
||||
case let .range(startIndexPath, endIndexPath):
|
||||
return (startIndexPath, endIndexPath)
|
||||
}
|
||||
}
|
||||
|
||||
init(dataSource: MSCalendarViewDataSource, startDate: Date, endDate: Date, selectionMode: SelectionMode) {
|
||||
self.dataSource = dataSource
|
||||
self.selectionMode = selectionMode
|
||||
|
@ -47,28 +73,13 @@ class MSDatePickerSelectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
var selectedDates: (startDate: Date, endDate: Date) {
|
||||
let startDate = dataSource.dayStart(forDayAt: selectedIndexPaths.startIndexPath)
|
||||
let endDate = dataSource.dayStart(forDayAt: selectedIndexPaths.endIndexPath)
|
||||
|
||||
return (startDate, endDate)
|
||||
func setSelectedIndexPath(_ indexPath: IndexPath, mode: SelectionMode? = nil) {
|
||||
selectionState = selectionState(for: indexPath, mode: mode)
|
||||
}
|
||||
|
||||
private var selectedIndexPaths: (startIndexPath: IndexPath, endIndexPath: IndexPath) {
|
||||
switch selectionState {
|
||||
case let .single(selectedIndexPath):
|
||||
return (selectedIndexPath, selectedIndexPath)
|
||||
case let .range(startIndexPath, endIndexPath):
|
||||
return (startIndexPath, endIndexPath)
|
||||
}
|
||||
}
|
||||
|
||||
func setSelectedIndexPath(_ indexPath: IndexPath) {
|
||||
selectionState = selectionState(for: indexPath)
|
||||
}
|
||||
|
||||
func selectionState(for indexPath: IndexPath) -> SelectionState {
|
||||
if selectionMode == .start {
|
||||
func selectionState(for indexPath: IndexPath, mode: SelectionMode? = nil) -> SelectionState {
|
||||
let mode = mode ?? selectionMode
|
||||
if mode == .start {
|
||||
switch selectionState {
|
||||
case .single:
|
||||
return .single(indexPath)
|
||||
|
|
|
@ -5,8 +5,16 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
// MARK: MSDateTimePickerController
|
||||
// MARK: MSDateTimePickerControllerMode
|
||||
|
||||
enum MSDateTimePickerControllerMode {
|
||||
case single, start, end
|
||||
}
|
||||
|
||||
// MARK: - MSDateTimePickerController
|
||||
|
||||
/// A view controller that allows a user to select either a date or a combination of date and time using a custom control similar in appearance to UIDatePicker.
|
||||
/// Has support for a start and end time/date.
|
||||
class MSDateTimePickerController: UIViewController, DateTimePicker {
|
||||
private struct Constants {
|
||||
static let idealRowCount: Int = 7
|
||||
|
@ -14,11 +22,50 @@ class MSDateTimePickerController: UIViewController, DateTimePicker {
|
|||
static let titleButtonWidth: CGFloat = 160
|
||||
}
|
||||
|
||||
var mode: MSDateTimePickerViewMode { return dateTimePickerView.mode }
|
||||
|
||||
var date: Date {
|
||||
var startDate: Date {
|
||||
didSet {
|
||||
dateTimePickerView.setDate(date, animated: false)
|
||||
startDate = startDate.rounded(toNearestMinutes: MSDateTimePickerViewDataSourceConstants.minuteInterval) ?? startDate
|
||||
switch mode {
|
||||
case .single:
|
||||
dateTimePickerView.setDate(startDate, animated: false)
|
||||
endDate = startDate
|
||||
case .start:
|
||||
dateTimePickerView.setDate(startDate, animated: false)
|
||||
case .end:
|
||||
break
|
||||
}
|
||||
updateNavigationBar()
|
||||
}
|
||||
}
|
||||
var endDate: Date {
|
||||
didSet {
|
||||
if mode != .single {
|
||||
endDate = endDate.rounded(toNearestMinutes: MSDateTimePickerViewDataSourceConstants.minuteInterval) ?? endDate
|
||||
}
|
||||
switch mode {
|
||||
case .single:
|
||||
endDate = startDate
|
||||
case .start:
|
||||
break
|
||||
case .end:
|
||||
dateTimePickerView.setDate(endDate, animated: false)
|
||||
updateNavigationBar()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var mode: MSDateTimePickerControllerMode {
|
||||
didSet {
|
||||
switch mode {
|
||||
case .start:
|
||||
dateTimePickerView.setDate(startDate, animated: false)
|
||||
case .end:
|
||||
dateTimePickerView.setDate(endDate, animated: false)
|
||||
case .single:
|
||||
dateTimePickerView.setDate(startDate, animated: false)
|
||||
endDate = startDate
|
||||
}
|
||||
updateNavigationBar()
|
||||
}
|
||||
}
|
||||
|
@ -27,19 +74,27 @@ class MSDateTimePickerController: UIViewController, DateTimePicker {
|
|||
|
||||
private let dateTimePickerView: MSDateTimePickerView
|
||||
private let titleView = MSTwoLinesTitleView()
|
||||
private var segmentedControl: MSSegmentedControl?
|
||||
|
||||
// TODO: Add availability back in? - contactAvailabilitySummaryDataSource: ContactAvailabilitySummaryDataSource?,
|
||||
init(date: Date, showsTime: Bool = true) {
|
||||
self.date = date
|
||||
init(startDate: Date, endDate: Date, mode: MSDateTimePickerMode) {
|
||||
self.mode = mode.singleSelection ? .single : .start
|
||||
self.startDate = startDate.rounded(toNearestMinutes: MSDateTimePickerViewDataSourceConstants.minuteInterval) ?? startDate
|
||||
self.endDate = self.mode == .single ? self.startDate : (endDate.rounded(toNearestMinutes: MSDateTimePickerViewDataSourceConstants.minuteInterval) ?? endDate)
|
||||
|
||||
let datePickerMode: MSDateTimePickerViewMode = showsTime ? .dateTime : .date(startYear: MSDateTimePickerViewMode.defaultStartYear, endYear: MSDateTimePickerViewMode.defaultEndYear)
|
||||
let datePickerMode: MSDateTimePickerViewMode = mode.includesTime ? .dateTime : .date(startYear: MSDateTimePickerViewMode.defaultStartYear, endYear: MSDateTimePickerViewMode.defaultEndYear)
|
||||
dateTimePickerView = MSDateTimePickerView(mode: datePickerMode)
|
||||
dateTimePickerView.setDate(self.startDate, animated: false)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
dateTimePickerView.addTarget(self, action: #selector(handleDidSelectDate(_:)), for: .valueChanged)
|
||||
|
||||
updateNavigationBar()
|
||||
|
||||
if self.mode != .single {
|
||||
initSegmentedControl(includesTime: mode.includesTime)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
@ -50,32 +105,47 @@ class MSDateTimePickerController: UIViewController, DateTimePicker {
|
|||
super.viewDidLoad()
|
||||
view.backgroundColor = MSColors.background
|
||||
|
||||
if let segmentedControl = segmentedControl {
|
||||
view.addSubview(segmentedControl)
|
||||
}
|
||||
view.addSubview(dateTimePickerView)
|
||||
initNavigationBar()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
dateTimePickerView.frame = view.bounds
|
||||
if let segmentedControl = segmentedControl {
|
||||
segmentedControl.frame = CGRect(x: 0, y: 0, width: view.width, height: segmentedControl.intrinsicContentSize.height)
|
||||
}
|
||||
let verticalOffset = segmentedControl?.height ?? 0
|
||||
dateTimePickerView.frame = CGRect(x: 0, y: verticalOffset, width: view.width, height: view.height - verticalOffset)
|
||||
}
|
||||
|
||||
override func accessibilityPerformEscape() -> Bool {
|
||||
dismiss(accept: true)
|
||||
dismiss()
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO: Refactor this to reuse for any modal that needs a cancel/confirm
|
||||
private func initSegmentedControl(includesTime: Bool) {
|
||||
let titles = includesTime ? ["MSDateTimePicker.StartTime".localized, "MSDateTimePicker.EndTime".localized] : ["MSDateTimePicker.StartDate".localized, "MSDateTimePicker.EndDate"]
|
||||
segmentedControl = MSSegmentedControl(items: titles)
|
||||
segmentedControl?.addTarget(self, action: #selector(handleDidSelectStartEnd(_:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
// TODO: Refactor this to reuse for any modal that needs a confirm
|
||||
private func initNavigationBar() {
|
||||
if let image = UIImage.staticImageNamed("checkmark-blue-25x25"),
|
||||
let landscapeImage = UIImage.staticImageNamed("checkmark-blue-thin-20x20") {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, landscapeImagePhone: landscapeImage, style: .plain, target: self, action: #selector(handleDidTapDone))
|
||||
let doneButton = UIBarButtonItem(image: image, landscapeImagePhone: landscapeImage, style: .plain, target: self, action: #selector(handleDidTapDone))
|
||||
doneButton.accessibilityLabel = "Accessibility.Done.Label".localized
|
||||
navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
navigationItem.titleView = titleView
|
||||
}
|
||||
|
||||
private func updateNavigationBar() {
|
||||
let title = String.dateString(from: date, compactness: .shortDaynameShortMonthnameDay)
|
||||
let titleDate = mode == .end ? endDate : startDate
|
||||
let title = String.dateString(from: titleDate, compactness: .shortDaynameShortMonthnameDay)
|
||||
titleView.setup(title: title)
|
||||
updateTitleFrame()
|
||||
}
|
||||
|
@ -91,21 +161,33 @@ class MSDateTimePickerController: UIViewController, DateTimePicker {
|
|||
}
|
||||
}
|
||||
|
||||
private func dismiss(accept: Bool) {
|
||||
if accept {
|
||||
delegate?.dateTimePicker(self, didPickDate: date)
|
||||
}
|
||||
private func dismiss() {
|
||||
delegate?.dateTimePicker(self, didPickStartDate: startDate, endDate: endDate)
|
||||
presentingViewController?.dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc private func handleDidSelectDate(_ datePicker: MSDateTimePickerView) {
|
||||
date = datePicker.date
|
||||
delegate?.dateTimePicker(self, didSelectDate: date)
|
||||
switch mode {
|
||||
case .single:
|
||||
startDate = datePicker.date
|
||||
case .start:
|
||||
let duration = endDate.timeIntervalSince(startDate)
|
||||
endDate = datePicker.date.addingTimeInterval(duration)
|
||||
startDate = datePicker.date
|
||||
case .end:
|
||||
endDate = datePicker.date
|
||||
}
|
||||
delegate?.dateTimePicker(self, didSelectStartDate: startDate, endDate: endDate)
|
||||
}
|
||||
|
||||
@objc private func handleDidSelectStartEnd(_ segmentedControl: MSSegmentedControl) {
|
||||
mode = segmentedControl.selectedSegmentIndex == 0 ? .start : .end
|
||||
}
|
||||
|
||||
@objc private func handleDidTapDone(_ item: UIBarButtonItem) {
|
||||
dismiss(accept: true)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MSDateTimePickerController: MSCardPresentable
|
||||
|
|
|
@ -59,8 +59,6 @@ class MSDateTimePickerView: UIControl {
|
|||
return gradientLayer
|
||||
}()
|
||||
|
||||
private var amPMValue: MSDateTimePickerViewAMPM?
|
||||
|
||||
init(mode: MSDateTimePickerViewMode) {
|
||||
self.mode = mode
|
||||
|
||||
|
@ -112,8 +110,6 @@ class MSDateTimePickerView: UIControl {
|
|||
componentsByType[.timeAMPM]?.select(item: amPM, animated: animated, userInitiated: false)
|
||||
componentsByType[.timeHour]?.select(item: dateComponents.hour, animated: animated, userInitiated: false)
|
||||
componentsByType[.timeMinute]?.select(item: dateComponents.minute, animated: animated, userInitiated: false)
|
||||
|
||||
updateAMPM(forHour: dateComponents.hour!)
|
||||
}
|
||||
|
||||
func setDayOfMonth(_ dayOfMonth: MSDayOfMonth, animated: Bool) {
|
||||
|
@ -240,22 +236,21 @@ class MSDateTimePickerView: UIControl {
|
|||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
private func updateAMPM(forHour hour: Int) {
|
||||
// When we scroll through the hours in 12 hour format. Each time we go to a next cycle, we switch to Am/Pm
|
||||
// This makes sure we switch to the right one and we do not switch if the user changed Am/Pm themselve
|
||||
|
||||
guard let component = componentsByType[.timeAMPM], let selectedIndexPath = component.selectedIndexPath else {
|
||||
amPMValue = nil
|
||||
private func updateHourForAMPM() {
|
||||
guard let amPM = componentsByType[.timeAMPM]?.selectedItem as? MSDateTimePickerViewAMPM else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let amPMValue = component.dataSource.item(forRowAtIndex: selectedIndexPath.row) as? MSDateTimePickerViewAMPM else {
|
||||
assertionFailure("updateAMPM > amPM value not found")
|
||||
return
|
||||
guard var hour = componentsByType[.timeHour]?.selectedItem as? Int else {
|
||||
fatalError("updateHourForAMPM > hour value not found")
|
||||
}
|
||||
switch amPM {
|
||||
case .am:
|
||||
hour = hour >= 12 ? hour - 12 : hour
|
||||
case .pm:
|
||||
hour = hour >= 12 ? hour : hour + 12
|
||||
}
|
||||
|
||||
let moduloIsEven = ((hour - 1) / 12) % 2 == 0
|
||||
self.amPMValue = moduloIsEven ? amPMValue : (amPMValue == .am ? .pm : .am)
|
||||
componentsByType[.timeHour]?.select(item: hour, animated: false, userInitiated: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,25 +264,26 @@ extension MSDateTimePickerView: MSDateTimePickerViewComponentDelegate {
|
|||
}
|
||||
|
||||
// Scrolling through hours in 12 hours format
|
||||
guard type == .timeHour, userInitiated, let indexPath = component.tableView.middleIndexPath, componentTypes.contains(.timeAMPM) else {
|
||||
return
|
||||
}
|
||||
guard let amPMComponent = componentsByType[.timeAMPM], let amPMValue = amPMValue else {
|
||||
assertionFailure("dateTimePickerComponent > amPM value not found")
|
||||
return
|
||||
}
|
||||
if type == .timeHour {
|
||||
guard userInitiated, let indexPath = component.tableView.middleIndexPath, componentTypes.contains(.timeAMPM) else {
|
||||
return
|
||||
}
|
||||
guard let amPMComponent = componentsByType[.timeAMPM] else {
|
||||
fatalError("dateTimePickerComponent > amPM value not found")
|
||||
}
|
||||
|
||||
// Switch between am and pm every cycle
|
||||
let moduloIsEven = (indexPath.row / 12) % 2 == 0
|
||||
let newValue = moduloIsEven ? amPMValue : (amPMValue == .am ? .pm : .am)
|
||||
// Switch between am and pm every cycle
|
||||
let moduloIsEven = (indexPath.row / 12) % 2 == 0
|
||||
let newValue: MSDateTimePickerViewAMPM = moduloIsEven ? .am : .pm
|
||||
|
||||
amPMComponent.select(item: newValue, animated: true, userInitiated: false)
|
||||
amPMComponent.select(item: newValue, animated: true, userInitiated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func dateTimePickerComponent(_ component: MSDateTimePickerViewComponent, didSelectItemAtIndexPath indexPath: IndexPath, userInitiated: Bool) {
|
||||
if userInitiated {
|
||||
if type(of: component) == .timeAMPM, let hour = componentsByType[.timeHour]?.selectedItem as? Int {
|
||||
updateAMPM(forHour: hour)
|
||||
if type(of: component) == .timeAMPM {
|
||||
updateHourForAMPM()
|
||||
}
|
||||
updateDate()
|
||||
}
|
||||
|
|
|
@ -145,13 +145,13 @@ extension MSDateTimePickerViewComponent: UIScrollViewDelegate {
|
|||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
// Snap to the nearest cell
|
||||
var targetOffset = targetContentOffset.pointee
|
||||
let offsetY = targetOffset.y
|
||||
let offsetY = targetOffset.y + scrollView.contentInset.top
|
||||
let cellHeight = MSDateTimePickerViewComponentCell.idealHeight
|
||||
|
||||
let prevY = floor(offsetY / cellHeight) * cellHeight
|
||||
let nextY = (floor(offsetY / cellHeight) + 1) * cellHeight
|
||||
let prevY = floor(offsetY / cellHeight) * cellHeight - scrollView.contentInset.top
|
||||
let nextY = (floor(offsetY / cellHeight) + 1) * cellHeight - scrollView.contentInset.top
|
||||
|
||||
if offsetY < prevY + cellHeight / 2 {
|
||||
if targetOffset.y < prevY + cellHeight / 2 {
|
||||
// Snap Up
|
||||
targetOffset = CGPoint(x: 0, y: prevY)
|
||||
} else {
|
||||
|
|
|
@ -354,15 +354,11 @@ private class MSDateTimePickerViewHourDataSource: MSDateTimePickerViewDataSource
|
|||
}
|
||||
|
||||
func indexPath(forItem item: Any) -> IndexPath? {
|
||||
guard var hour = item as? Int, hour >= 0 && hour < 24 else {
|
||||
guard let hour = item as? Int, hour >= 0 && hour < 24 else {
|
||||
assertionFailure("indexPathForItem > invalid argument")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !Date.has24HourFormat() && hour > 12 {
|
||||
hour -= 12
|
||||
}
|
||||
|
||||
return IndexPath(row: MSDateTimePickerViewDataSourceConstants.infiniteRowCount / 2 + hour, section: 0)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,33 +10,44 @@ import Foundation
|
|||
@objc public enum MSDateTimePickerMode: Int {
|
||||
case date
|
||||
case dateTime
|
||||
case dateRange
|
||||
case dateTimeRange
|
||||
|
||||
public var includesTime: Bool { return self == .dateTime || self == .dateTimeRange }
|
||||
public var singleSelection: Bool { return self == .date || self == .dateTime }
|
||||
}
|
||||
|
||||
// MARK: - MSDateTimePickerDelegate
|
||||
|
||||
@objc public protocol MSDateTimePickerDelegate: class {
|
||||
/// Allows a class to be notified when a user confirms their selected date
|
||||
func dateTimePicker(_ dateTimePicker: MSDateTimePicker, didPickDate date: Date)
|
||||
func dateTimePicker(_ dateTimePicker: MSDateTimePicker, didPickStartDate startDate: Date, endDate: Date)
|
||||
}
|
||||
|
||||
// MARK: - DateTimePicker
|
||||
|
||||
protocol DateTimePicker: class {
|
||||
var date: Date { get set }
|
||||
var startDate: Date { get set }
|
||||
var endDate: Date { get set }
|
||||
var delegate: DateTimePickerDelegate? { get set }
|
||||
}
|
||||
|
||||
// MARK: - DateTimePickerDelegate
|
||||
|
||||
protocol DateTimePickerDelegate: class {
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didPickDate date: Date)
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didSelectDate date: Date)
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didPickStartDate startDate: Date, endDate: Date)
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didSelectStartDate startDate: Date, endDate: Date)
|
||||
}
|
||||
|
||||
// MARK: - MSDateTimePicker
|
||||
|
||||
/// Manages the presentation and coordination of different date and time pickers
|
||||
public class MSDateTimePicker: NSObject {
|
||||
private struct Constants {
|
||||
static let defaultDateDaysRange: Int = 1
|
||||
static let defaultDateTimeHoursRange: Int = 1
|
||||
}
|
||||
|
||||
public private(set) var mode: MSDateTimePickerMode?
|
||||
@objc public weak var delegate: MSDateTimePickerDelegate?
|
||||
|
||||
|
@ -48,19 +59,24 @@ public class MSDateTimePicker: NSObject {
|
|||
/// - Parameters:
|
||||
/// - presentingController: The view controller that is presenting these pickers
|
||||
/// - mode: Enum describing which mode of pickers should be presented
|
||||
/// - date: The initial date selected on the presented pickers
|
||||
@objc public func present(from presentingController: UIViewController, with mode: MSDateTimePickerMode, for date: Date = Date()) {
|
||||
/// - startDate: The initial date selected on the presented pickers
|
||||
/// - endDate: An optional end date to pick a range of dates. Ignored if mode is `.date` or `.dateTime`. If the mode selected is either `.dateRange` or `.dateTimeRange`, and this is omitted, it will be set to a default 1 day or 1 hour range, respectively.
|
||||
@objc public func present(from presentingController: UIViewController, with mode: MSDateTimePickerMode, startDate: Date = Date(), endDate: Date? = nil) {
|
||||
self.presentingController = presentingController
|
||||
self.mode = mode
|
||||
if UIAccessibility.isVoiceOverRunning {
|
||||
presentDateTimePickerForAccessibility(initialDate: date, showsTime: mode == .dateTime)
|
||||
presentDateTimePickerForAccessibility(startDate: startDate, endDate: endDate ?? startDate)
|
||||
return
|
||||
}
|
||||
self.mode = mode
|
||||
switch mode {
|
||||
case .date:
|
||||
presentDatePicker(initialDate: date)
|
||||
presentDatePicker(startDate: startDate, endDate: startDate)
|
||||
case .dateTime:
|
||||
presentDateTimePicker(initialDate: date)
|
||||
presentDateTimePicker(startDate: startDate, endDate: startDate)
|
||||
case .dateRange:
|
||||
presentDatePicker(startDate: startDate, endDate: endDate ?? startDate.adding(days: Constants.defaultDateDaysRange))
|
||||
case .dateTimeRange:
|
||||
presentDateTimePicker(startDate: startDate, endDate: endDate ?? startDate.adding(hours: Constants.defaultDateTimeHoursRange))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,19 +87,43 @@ public class MSDateTimePicker: NSObject {
|
|||
presentingController = nil
|
||||
}
|
||||
|
||||
private func presentDatePicker(initialDate: Date) {
|
||||
let datePicker = MSDatePickerController(startDate: initialDate)
|
||||
present([datePicker])
|
||||
private func presentDatePicker(startDate: Date, endDate: Date) {
|
||||
guard let mode = mode else {
|
||||
fatalError("Mode not set when presenting date picker")
|
||||
}
|
||||
let startDate = startDate.startOfDay
|
||||
let endDate = endDate.startOfDay
|
||||
if mode == .dateRange {
|
||||
let startDatePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: mode, selectionMode: .start, subtitle: "MSDateTimePicker.StartDate".localized)
|
||||
let endDatePicker = MSDatePickerController(startDate: startDate, endDate: endDate, mode: mode, selectionMode: .end, subtitle: "MSDateTimePicker.EndDate".localized)
|
||||
present([startDatePicker, endDatePicker])
|
||||
} else {
|
||||
let datePicker = MSDatePickerController(startDate: startDate, endDate: startDate, mode: mode)
|
||||
present([datePicker])
|
||||
}
|
||||
}
|
||||
|
||||
private func presentDateTimePicker(initialDate: Date) {
|
||||
let datePicker = MSDatePickerController(startDate: initialDate)
|
||||
let dateTimePicker = MSDateTimePickerController(date: initialDate, showsTime: true)
|
||||
present([datePicker, dateTimePicker])
|
||||
private func presentDateTimePicker(startDate: Date, endDate: Date) {
|
||||
guard let mode = mode else {
|
||||
fatalError("Mode not set when presenting date time picker")
|
||||
}
|
||||
// If we are not presenting a range, or if we have a range, but it is within the same calendar day, present both dateTimePicker and datePicker. Otherwise, present just a dateTimePicker
|
||||
if mode == .dateTime || Calendar.current.isDate(startDate, inSameDayAs: endDate) {
|
||||
let dateTimePicker = MSDateTimePickerController(startDate: startDate, endDate: endDate, mode: mode)
|
||||
// Create datePicker second to pick up the time that dateTimePicker rounded to the nearest minute interval
|
||||
let datePicker = MSDatePickerController(startDate: dateTimePicker.startDate, endDate: dateTimePicker.endDate, mode: mode)
|
||||
present([datePicker, dateTimePicker])
|
||||
} else {
|
||||
let dateTimePicker = MSDateTimePickerController(startDate: startDate, endDate: endDate, mode: mode)
|
||||
present([dateTimePicker])
|
||||
}
|
||||
}
|
||||
|
||||
private func presentDateTimePickerForAccessibility(initialDate: Date, showsTime: Bool) {
|
||||
let dateTimePicker = MSDateTimePickerController(date: initialDate, showsTime: showsTime)
|
||||
private func presentDateTimePickerForAccessibility(startDate: Date, endDate: Date) {
|
||||
guard let mode = mode else {
|
||||
fatalError("Mode not set when presenting date time picker for accessibility")
|
||||
}
|
||||
let dateTimePicker = MSDateTimePickerController(startDate: startDate, endDate: endDate, mode: mode)
|
||||
present([dateTimePicker])
|
||||
}
|
||||
|
||||
|
@ -103,16 +143,17 @@ public class MSDateTimePicker: NSObject {
|
|||
// MARK: - MSDateTimePicker: DateTimePickerDelegate
|
||||
|
||||
extension MSDateTimePicker: DateTimePickerDelegate {
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didPickDate date: Date) {
|
||||
delegate?.dateTimePicker(self, didPickDate: date)
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didPickStartDate startDate: Date, endDate: Date) {
|
||||
delegate?.dateTimePicker(self, didPickStartDate: startDate, endDate: endDate)
|
||||
}
|
||||
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didSelectDate date: Date) {
|
||||
func dateTimePicker(_ dateTimePicker: DateTimePicker, didSelectStartDate startDate: Date, endDate: Date) {
|
||||
guard let presentedPickers = presentedPickers else {
|
||||
return
|
||||
}
|
||||
for picker in presentedPickers where picker !== dateTimePicker {
|
||||
picker.date = date
|
||||
picker.startDate = startDate
|
||||
picker.endDate = endDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,19 @@ extension Date {
|
|||
return _has24HourFormat!
|
||||
}
|
||||
|
||||
func combine(withTime time: Date) -> Date? {
|
||||
let calendar = Calendar.current
|
||||
|
||||
var dateComponents = calendar.dateComponents([.year, .month, .day], from: self)
|
||||
let timeComponents = calendar.dateComponents([.hour, .minute, .second], from: time)
|
||||
|
||||
dateComponents.hour = timeComponents.hour
|
||||
dateComponents.minute = timeComponents.minute
|
||||
dateComponents.second = timeComponents.second
|
||||
|
||||
return calendar.date(from: dateComponents)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a new date from `self` by going back in time until the first moment of that day
|
||||
*
|
||||
|
@ -73,6 +86,17 @@ extension Date {
|
|||
// Return the original date in case of error
|
||||
return self
|
||||
}
|
||||
|
||||
func rounded(toNearestMinutes nearestMinutes: Int) -> Date? {
|
||||
let calendar = Calendar.current
|
||||
var dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: self)
|
||||
guard let minutes = dateComponents.minute else {
|
||||
return nil
|
||||
}
|
||||
let nearestMinutes = Double(nearestMinutes)
|
||||
dateComponents.minute = Int(ceil(Double(minutes) / nearestMinutes) * nearestMinutes)
|
||||
return calendar.date(from: dateComponents)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Components
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"Accessibility.Alert" = "Alert";
|
||||
"Accessibility.Dismiss.Label" = "Dismiss";
|
||||
"Accessibility.Dismiss.Hint" = "Double tap to dismiss";
|
||||
"Accessibility.Done.Label" = "Done";
|
||||
"Accessibility.Select.Hint" = "Double tap to select";
|
||||
"Accessibility.Selected.Hint" = "Double tap to see details";
|
||||
"Accessibility.Selected.Value" = "Selected";
|
||||
|
@ -87,3 +88,15 @@
|
|||
|
||||
// MSPersonaListView
|
||||
"MSPersonaListView.SearchDirectory" = "Search Directory";
|
||||
|
||||
/* MSDateTimePicker label for the start time of a date range */
|
||||
"MSDateTimePicker.StartTime" = "Start Time";
|
||||
|
||||
/* MSDateTimePicker label for the end time of a date range */
|
||||
"MSDateTimePicker.EndTime" = "End Time";
|
||||
|
||||
/* MSDateTimePicker label for the start date of a date range */
|
||||
"MSDateTimePicker.StartDate" = "Start Date";
|
||||
|
||||
/* MSDateTimePicker label for the end date of a date range */
|
||||
"MSDateTimePicker.EndDate" = "End Date";
|
||||
|
|
Загрузка…
Ссылка в новой задаче