// // DatePickerView.swift // DemoSwift // // Created by yaoxinpan on 2018/5/28. // Copyright © 2018年 yaoxp. All rights reserved. // import UIKit protocol NibLoadable {} extension NibLoadable { static func loadViewFromNib() -> Self { return Bundle.main.loadNibNamed("\(self)", owner: nil, options: nil)?.last as! Self } } enum DateStyle { case all // 年月日时分秒 case yearMonthDay // 年月日 case yearMonth // 年月 case hourMinuteSecond // 时分秒 case yearMonthDayHourMinute // 年月日时分 } class DatePickerView: UIView, NibLoadable, UIGestureRecognizerDelegate, UIPickerViewDelegate, UIPickerViewDataSource { // MARK: - 公开属性 /// 最小的时间,默认是1970/1/1 00:00:00 var minLimitDate: Date = Date(timeIntervalSince1970: 0) { didSet { if maxLimitDate < minLimitDate { minLimitDate = oldValue } else { if minLimitDate > scrollToDate { scrollToDate = minLimitDate } } } } /// 最大的时间,默认是当前时间10年后 var maxLimitDate: Date = Date(timeIntervalSinceNow: 60 * 60 * 24 * 365 * 10) { didSet { if maxLimitDate < minLimitDate { maxLimitDate = oldValue } else { if maxLimitDate < scrollToDate { scrollToDate = maxLimitDate } } } } /// 默认显示的当前时间 var scrollToDate: Date = Date() { didSet { if maxLimitDate < scrollToDate { scrollToDate = maxLimitDate } if minLimitDate > scrollToDate { scrollToDate = minLimitDate } } } /// 确定按钮背景色,默认蓝色 var sureButtonBackgroundColor: UIColor? { didSet { sureButton.backgroundColor = sureButtonBackgroundColor } } /// 确定按钮字体颜色,默认白色 var sureButtonTitleColor: UIColor? { didSet { sureButton.setTitleColor(sureButtonTitleColor, for: .normal) } } /// 年 背景水印,设置无色可以隐藏 var yearPlaceholderColor: UIColor? { didSet { yearLabel.textColor = yearPlaceholderColor } } // MARK: - 私有属性 @IBOutlet private weak var bottomView: UIView! @IBOutlet private weak var sureButton: UIButton! @IBOutlet private weak var yearLabel: UILabel! @IBOutlet private weak var bottomViewBottom: NSLayoutConstraint! @IBOutlet private weak var pickerView: UIPickerView! private var componentsArray: Array = [Dictionary>]() /// 显示的时间样式 private var style: DateStyle = .all private let yearKey = "年" private let monthKey = "月" private let dayKey = "日" private let hourKey = "时" private let minuteKey = "分" private let secondKey = "秒" private var completionHandler: (_ date: Date?) -> Void = {_ in } /// /// - Parameters: /// - style: 类型 /// - scrollToDate: 滚动到的时间,如果不设刚是当前时间 /// - Returns: 新的DatePickerView class func datePicker(style: DateStyle = .all, scrollToDate: Date = Date(), completionHandler: @escaping (_ date: Date?) -> Void) -> DatePickerView { let view: DatePickerView = DatePickerView.loadViewFromNib() view.style = style view.completionHandler = completionHandler view.scrollToDate = scrollToDate view.setupUI() view.setupData() return view } func show() { setupData() UIApplication.shared.keyWindow!.addSubview(self) self.frame = UIScreen.main.bounds UIView.animate(withDuration: 0.3, animations: { self.bottomViewBottom.constant = 0 self.backgroundColor = UIColor.hexRGB(0x000000, 0.5) self.layoutIfNeeded() }, completion: { (finish) in self.scrollTo(self.scrollToDate) }) } @objc func dismiss() { UIView.animate(withDuration: 0.3, animations: { self.bottomViewBottom.constant = self.bottomView.frame.size.height self.backgroundColor = UIColor.hexRGB(0x000000, 0.0) self.layoutIfNeeded() }, completion: { (finished) in self.removeFromSuperview() }) } @IBAction private func onSureButtonAction(_ sender: Any) { completionHandler(currentDate()) dismiss() } } extension DatePickerView { private func setupUI() { bottomViewBottom.constant = bottomView.frame.size.height backgroundColor = UIColor.hexRGB(0x000000, 0.0) let tap = UITapGestureRecognizer(target: self, action: #selector(dismiss)) tap.delegate = self addGestureRecognizer(tap) pickerView.delegate = self pickerView.dataSource = self self.yearLabel.text = String(self.scrollToDate.year) } private func setupData() { componentsArray = [Dictionary]() switch style { case .all: initDataWithAllStyle() case .yearMonthDay: initDataWithYearMonthDayStyle() case .yearMonth: initDataWithYearMonthStyle() case .hourMinuteSecond: initDataWithHourMinuteSecondStyle() case .yearMonthDayHourMinute: initDataWithYearMonthDayHourMinuteStyle() } } private func currentDate() -> Date? { var result = [Int]() for index in 0.. maxLimitDate { scrollToDate = maxLimitDate } if scrollToDate < minLimitDate { scrollToDate = minLimitDate } switch style { case .all: allStyleScrollTo(scrollToDate) case .yearMonthDay: yearMonthDayStyleScrollTo(scrollToDate) case .yearMonth: yearMonthStyleScrollTo(scrollToDate) case .hourMinuteSecond: hourMinuteSecondStyleScrollTo(scrollToDate) case .yearMonthDayHourMinute: yearMonthDayHourMinuteStyleScrollTo(scrollToDate) } } private func allStyleScrollTo(_ date: Date) { guard componentsArray.count == 6 else { return } if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) { pickerView.selectRow(yearIndex, inComponent: 0, animated: true) } if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) { pickerView.selectRow(monthIndex, inComponent: 1, animated: true) } if let dayIndex = componentsArray[2][dayKey]?.firstIndex(of: date.day) { pickerView.selectRow(dayIndex, inComponent: 2, animated: true) } if let hourIndex = componentsArray[3][hourKey]?.firstIndex(of: date.hour) { pickerView.selectRow(hourIndex, inComponent: 3, animated: true) } if let minuteIndex = componentsArray[4][minuteKey]?.firstIndex(of: date.minute) { pickerView.selectRow(minuteIndex, inComponent: 4, animated: true) } if let secondIndex = componentsArray[5][secondKey]?.firstIndex(of: date.second) { pickerView.selectRow(secondIndex, inComponent: 5, animated: true) } } private func yearMonthDayStyleScrollTo(_ date: Date) { guard componentsArray.count == 3 else { return } if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) { pickerView.selectRow(yearIndex, inComponent: 0, animated: true) } if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) { pickerView.selectRow(monthIndex, inComponent: 1, animated: true) } if let dayIndex = componentsArray[2][dayKey]?.firstIndex(of: date.day) { pickerView.selectRow(dayIndex, inComponent: 2, animated: true) } } private func yearMonthStyleScrollTo(_ date: Date) { guard componentsArray.count == 2 else { return } if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) { pickerView.selectRow(yearIndex, inComponent: 0, animated: true) } if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) { pickerView.selectRow(monthIndex, inComponent: 1, animated: true) } } private func hourMinuteSecondStyleScrollTo(_ date: Date) { guard componentsArray.count == 3 else { return } if let hourIndex = componentsArray[0][hourKey]?.firstIndex(of: date.hour) { pickerView.selectRow(hourIndex, inComponent: 0, animated: true) } if let minuteIndex = componentsArray[1][minuteKey]?.firstIndex(of: date.minute) { pickerView.selectRow(minuteIndex, inComponent: 1, animated: true) } if let secondIndex = componentsArray[2][secondKey]?.firstIndex(of: date.second) { pickerView.selectRow(secondIndex, inComponent: 2, animated: true) } } private func yearMonthDayHourMinuteStyleScrollTo(_ date: Date) { guard componentsArray.count == 5 else { return } if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) { pickerView.selectRow(yearIndex, inComponent: 0, animated: true) } if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) { pickerView.selectRow(monthIndex, inComponent: 1, animated: true) } if let dayIndex = componentsArray[2][dayKey]?.firstIndex(of: date.day) { pickerView.selectRow(dayIndex, inComponent: 2, animated: true) } if let hourIndex = componentsArray[3][hourKey]?.firstIndex(of: date.hour) { pickerView.selectRow(hourIndex, inComponent: 3, animated: true) } if let minuteIndex = componentsArray[4][minuteKey]?.firstIndex(of: date.minute) { pickerView.selectRow(minuteIndex, inComponent: 4, animated: true) } } } // MARK: - init data extension DatePickerView { private func initDataWithAllStyle() { componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)]) // if minLimitDate.haveSameYear(maxLimitDate) { // componentsArray.append([monthKey : Array(minLimitDate.month...maxLimitDate.month)]) // } else { componentsArray.append([monthKey : Array(1...12)]) // } // if minLimitDate.haveSameYearAndMonth(maxLimitDate) { // componentsArray.append([dayKey : Array(minLimitDate.day...maxLimitDate.day)]) // } else { componentsArray.append([dayKey : Array(1...scrollToDate.numberOfDaysInMonth())]) // } // if minLimitDate.haveSameYearMonthAndDay(maxLimitDate) { // componentsArray.append([hourKey : Array(minLimitDate.day...maxLimitDate.day)]) // } else { componentsArray.append([hourKey : Array(0...23)]) // } // if minLimitDate.haveSameYearMonthDayAndHour(maxLimitDate) { // componentsArray.append([minuteKey : Array(minLimitDate.minute...maxLimitDate.minute)]) // } else { componentsArray.append([minuteKey : Array(0...59)]) // } // if minLimitDate.haveSameYearMonthDayHourAndMinute(maxLimitDate) { // componentsArray.append([secondKey : Array(minLimitDate.minute...maxLimitDate.minute)]) // } else { componentsArray.append([secondKey : Array(0...59)]) // } } private func initDataWithYearMonthDayStyle() { componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)]) // if minLimitDate.haveSameYear(maxLimitDate) { // componentsArray.append([monthKey : Array(minLimitDate.month...maxLimitDate.month)]) // } else { componentsArray.append([monthKey : Array(1...12)]) // } // if minLimitDate.haveSameYearAndMonth(maxLimitDate) { // componentsArray.append([dayKey : Array(minLimitDate.day...maxLimitDate.day)]) // } else { componentsArray.append([dayKey : Array(1...scrollToDate.numberOfDaysInMonth())]) // } } private func initDataWithYearMonthStyle() { componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)]) componentsArray.append([monthKey : Array(1...12)]) } private func initDataWithHourMinuteSecondStyle() { // if let date = Date.date("00:00:00", formatter: "HH:mm:ss") { // minLimitDate = date // } // // if let date = Date.date("23:59:59", formatter: "HH:mm:ss") { // maxLimitDate = date // } componentsArray.append([hourKey : Array(0...23)]) // if minLimitDate.haveSameYearMonthDayAndHour(maxLimitDate) { // componentsArray.append([minuteKey : Array(minLimitDate.minute...maxLimitDate.minute)]) // } else { componentsArray.append([minuteKey : Array(0...59)]) // } // if minLimitDate.haveSameYearMonthDayHourAndMinute(maxLimitDate) { // componentsArray.append([secondKey : Array(minLimitDate.second...maxLimitDate.second)]) // } else { componentsArray.append([secondKey : Array(0...59)]) // } } private func initDataWithYearMonthDayHourMinuteStyle() { componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)]) // if minLimitDate.haveSameYear(maxLimitDate) { // componentsArray.append([monthKey : Array(minLimitDate.month...maxLimitDate.month)]) // } else { componentsArray.append([monthKey : Array(1...12)]) // } // if minLimitDate.haveSameYearAndMonth(maxLimitDate) { // componentsArray.append([dayKey : Array(minLimitDate.day...maxLimitDate.day)]) // } else { componentsArray.append([dayKey : Array(1...scrollToDate.numberOfDaysInMonth())]) // } // if minLimitDate.haveSameYearMonthAndDay(maxLimitDate) { // componentsArray.append([hourKey : Array(minLimitDate.day...maxLimitDate.day)]) // } else { componentsArray.append([hourKey : Array(0...23)]) // } // if minLimitDate.haveSameYearMonthDayAndHour(maxLimitDate) { // componentsArray.append([minuteKey : Array(minLimitDate.minute...maxLimitDate.minute)]) // } else { componentsArray.append([minuteKey : Array(0...59)]) // } } } // MARK: - UIGestureRecognizerDelegate extension DatePickerView { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { guard let touchView = touch.view else { return false } if touchView.isDescendant(of: bottomView) { // 点击的view是否是bottomView或者bottomView的子视图 return false } return true } } // MARK: - UIPickerViewDelegate, UIPickerViewDataSource extension DatePickerView { func numberOfComponents(in pickerView: UIPickerView) -> Int { return componentsArray.count } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { if let array = componentsArray[component].values.first { return array.count } return 0 } func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { return 40 } func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { var title: String = " " let dic = componentsArray[component] if let key = dic.keys.first { let data = dic[key] title = String(data![row]) + key } if let label = view as? UILabel { label.text = title return label } let label = UILabel() label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 17) label.text = title label.sizeToFit() return label } func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { // 月分变化了,天数要跟着变化.年份变化了,如果是2月,天数也可能变化 func scrollToValidTimeRange() { // 检查当前时间是否在最大和最小时间之前 if let currentDate = currentDate() { if currentDate > maxLimitDate { // 超过最大值,则滚回最大值 scrollTo(maxLimitDate) } else if currentDate < minLimitDate { // 小于最小值,刚滚回最小值 scrollTo(minLimitDate) } } } guard let key = componentsArray[component].keys.first, key == yearKey || key == monthKey else { // 只是滚动年或者月时,才需要处理天数的变化 scrollToValidTimeRange() return } let yearIndex = pickerView.selectedRow(inComponent: 0) if yearIndex < 0 { scrollToValidTimeRange() return } if key == yearKey { // 更新年份水印 let year = componentsArray[0][yearKey]![yearIndex] // 年份 yearLabel.text = String(year) } if key == yearKey && pickerView.selectedRow(inComponent: component + 1) != 1 { // 滚动年,月分不是2月时,不需要处理天数变化 scrollToValidTimeRange() return } let monthIndex = pickerView.selectedRow(inComponent: 1) if monthIndex < 0 { scrollToValidTimeRange() return } let year = componentsArray[0][yearKey]![yearIndex] // 年份 let month = componentsArray[1][monthKey]![monthIndex] // 月份 guard let date = Date.date(String(year) + "-" + String(month), formatter: "yyyy-MM") else { scrollToValidTimeRange() return } if self.style == .yearMonth { return } let numberOfDays = date.numberOfDaysInMonth() // 当前月的天数 if numberOfDays != 0 && numberOfDays == componentsArray[2][dayKey]!.count { // 天数没有变化,不需要任何操作 scrollToValidTimeRange() return } let newDayDic = [dayKey : Array(1...numberOfDays)] // 新的天数数据 componentsArray.replaceSubrange(2...2, with: [newDayDic]) pickerView.reloadComponent(2) scrollToValidTimeRange() } }