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:
Will Richman 2019-04-17 22:29:13 +00:00
Родитель bd7c6308a6
Коммит 630558d7bb
15 изменённых файлов: 551 добавлений и 191 удалений

Просмотреть файл

@ -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";