DatePickerView.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. //
  2. // DatePickerView.swift
  3. // DemoSwift
  4. //
  5. // Created by yaoxinpan on 2018/5/28.
  6. // Copyright © 2018年 yaoxp. All rights reserved.
  7. //
  8. import UIKit
  9. protocol NibLoadable {}
  10. extension NibLoadable {
  11. static func loadViewFromNib() -> Self {
  12. return Bundle.main.loadNibNamed("\(self)", owner: nil, options: nil)?.last as! Self
  13. }
  14. }
  15. enum DateStyle {
  16. case all // 年月日时分秒
  17. case yearMonthDay // 年月日
  18. case yearMonth // 年月
  19. case hourMinuteSecond // 时分秒
  20. case yearMonthDayHourMinute // 年月日时分
  21. }
  22. class DatePickerView: UIView, NibLoadable, UIGestureRecognizerDelegate, UIPickerViewDelegate, UIPickerViewDataSource {
  23. // MARK: - 公开属性
  24. /// 最小的时间,默认是1970/1/1 00:00:00
  25. var minLimitDate: Date = Date(timeIntervalSince1970: 0) {
  26. didSet {
  27. if maxLimitDate < minLimitDate {
  28. minLimitDate = oldValue
  29. } else {
  30. if minLimitDate > scrollToDate {
  31. scrollToDate = minLimitDate
  32. }
  33. }
  34. }
  35. }
  36. /// 最大的时间,默认是当前时间10年后
  37. var maxLimitDate: Date = Date(timeIntervalSinceNow: 60 * 60 * 24 * 365 * 10) {
  38. didSet {
  39. if maxLimitDate < minLimitDate {
  40. maxLimitDate = oldValue
  41. } else {
  42. if maxLimitDate < scrollToDate {
  43. scrollToDate = maxLimitDate
  44. }
  45. }
  46. }
  47. }
  48. /// 默认显示的当前时间
  49. var scrollToDate: Date = Date() {
  50. didSet {
  51. if maxLimitDate < scrollToDate {
  52. scrollToDate = maxLimitDate
  53. }
  54. if minLimitDate > scrollToDate {
  55. scrollToDate = minLimitDate
  56. }
  57. }
  58. }
  59. /// 确定按钮背景色,默认蓝色
  60. var sureButtonBackgroundColor: UIColor? {
  61. didSet {
  62. sureButton.backgroundColor = sureButtonBackgroundColor
  63. }
  64. }
  65. /// 确定按钮字体颜色,默认白色
  66. var sureButtonTitleColor: UIColor? {
  67. didSet {
  68. sureButton.setTitleColor(sureButtonTitleColor, for: .normal)
  69. }
  70. }
  71. /// 年 背景水印,设置无色可以隐藏
  72. var yearPlaceholderColor: UIColor? {
  73. didSet {
  74. yearLabel.textColor = yearPlaceholderColor
  75. }
  76. }
  77. // MARK: - 私有属性
  78. @IBOutlet private weak var bottomView: UIView!
  79. @IBOutlet private weak var sureButton: UIButton!
  80. @IBOutlet private weak var yearLabel: UILabel!
  81. @IBOutlet private weak var bottomViewBottom: NSLayoutConstraint!
  82. @IBOutlet private weak var pickerView: UIPickerView!
  83. private var componentsArray: Array<Dictionary> = [Dictionary<String, Array<Int>>]()
  84. /// 显示的时间样式
  85. private var style: DateStyle = .all
  86. private let yearKey = "年"
  87. private let monthKey = "月"
  88. private let dayKey = "日"
  89. private let hourKey = "时"
  90. private let minuteKey = "分"
  91. private let secondKey = "秒"
  92. private var completionHandler: (_ date: Date?) -> Void = {_ in }
  93. ///
  94. /// - Parameters:
  95. /// - style: 类型
  96. /// - scrollToDate: 滚动到的时间,如果不设刚是当前时间
  97. /// - Returns: 新的DatePickerView
  98. class func datePicker(style: DateStyle = .all, scrollToDate: Date = Date(), completionHandler: @escaping (_ date: Date?) -> Void) -> DatePickerView {
  99. let view: DatePickerView = DatePickerView.loadViewFromNib()
  100. view.style = style
  101. view.completionHandler = completionHandler
  102. view.scrollToDate = scrollToDate
  103. view.setupUI()
  104. view.setupData()
  105. return view
  106. }
  107. func show() {
  108. setupData()
  109. UIApplication.shared.keyWindow!.addSubview(self)
  110. self.frame = UIScreen.main.bounds
  111. UIView.animate(withDuration: 0.3, animations: {
  112. self.bottomViewBottom.constant = 0
  113. self.backgroundColor = UIColor.hexRGB(0x000000, 0.5)
  114. self.layoutIfNeeded()
  115. }, completion: { (finish) in
  116. self.scrollTo(self.scrollToDate)
  117. })
  118. }
  119. @objc func dismiss() {
  120. UIView.animate(withDuration: 0.3, animations: {
  121. self.bottomViewBottom.constant = self.bottomView.frame.size.height
  122. self.backgroundColor = UIColor.hexRGB(0x000000, 0.0)
  123. self.layoutIfNeeded()
  124. }, completion: { (finished) in
  125. self.removeFromSuperview()
  126. })
  127. }
  128. @IBAction private func onSureButtonAction(_ sender: Any) {
  129. completionHandler(currentDate())
  130. dismiss()
  131. }
  132. }
  133. extension DatePickerView {
  134. private func setupUI() {
  135. bottomViewBottom.constant = bottomView.frame.size.height
  136. backgroundColor = UIColor.hexRGB(0x000000, 0.0)
  137. let tap = UITapGestureRecognizer(target: self, action: #selector(dismiss))
  138. tap.delegate = self
  139. addGestureRecognizer(tap)
  140. pickerView.delegate = self
  141. pickerView.dataSource = self
  142. self.yearLabel.text = String(self.scrollToDate.year)
  143. }
  144. private func setupData() {
  145. componentsArray = [Dictionary]()
  146. switch style {
  147. case .all:
  148. initDataWithAllStyle()
  149. case .yearMonthDay:
  150. initDataWithYearMonthDayStyle()
  151. case .yearMonth:
  152. initDataWithYearMonthStyle()
  153. case .hourMinuteSecond:
  154. initDataWithHourMinuteSecondStyle()
  155. case .yearMonthDayHourMinute:
  156. initDataWithYearMonthDayHourMinuteStyle()
  157. }
  158. }
  159. private func currentDate() -> Date? {
  160. var result = [Int]()
  161. for index in 0..<pickerView.numberOfComponents {
  162. result.append(componentsArray[index].values.first![pickerView.selectedRow(inComponent: index)])
  163. }
  164. switch style {
  165. case .all:
  166. guard result.count == 6 else { return nil }
  167. let dateStr = String(result[0]) + "-" + String(result[1]) + "-" + String(result[2]) + " " +
  168. String(result[3]) + ":" + String(result[4]) + ":" + String(result[5])
  169. return Date.date(dateStr, formatter: "yyyy-MM-dd HH:mm:ss")
  170. case .yearMonthDay:
  171. guard result.count == 3 else { return nil }
  172. let dateStr = String(result[0]) + "-" + String(result[1]) + "-" + String(result[2])
  173. return Date.date(dateStr, formatter: "yyyy-MM-dd")
  174. case .yearMonth:
  175. guard result.count == 2 else { return nil }
  176. let dateStr = String(result[0]) + "-" + String(result[1])
  177. return Date.date(dateStr, formatter: "yyyy-MM")
  178. case .hourMinuteSecond:
  179. guard result.count == 3 else { return nil }
  180. let dateStr = String(result[0]) + ":" + String(result[1]) + ":" + String(result[2])
  181. return Date.date(dateStr, formatter: "HH:mm:ss")
  182. case .yearMonthDayHourMinute:
  183. guard result.count == 5 else { return nil }
  184. let dateStr = String(result[0]) + "-" + String(result[1]) + "-" + String(result[2]) + " " +
  185. String(result[3]) + ":" + String(result[4])
  186. return Date.date(dateStr, formatter: "yyyy-MM-dd HH:mm")
  187. }
  188. }
  189. }
  190. // MARK: - scroll to date
  191. extension DatePickerView {
  192. private func scrollTo(_ date: Date) {
  193. var scrollToDate = date
  194. if scrollToDate > maxLimitDate {
  195. scrollToDate = maxLimitDate
  196. }
  197. if scrollToDate < minLimitDate {
  198. scrollToDate = minLimitDate
  199. }
  200. switch style {
  201. case .all:
  202. allStyleScrollTo(scrollToDate)
  203. case .yearMonthDay:
  204. yearMonthDayStyleScrollTo(scrollToDate)
  205. case .yearMonth:
  206. yearMonthStyleScrollTo(scrollToDate)
  207. case .hourMinuteSecond:
  208. hourMinuteSecondStyleScrollTo(scrollToDate)
  209. case .yearMonthDayHourMinute:
  210. yearMonthDayHourMinuteStyleScrollTo(scrollToDate)
  211. }
  212. }
  213. private func allStyleScrollTo(_ date: Date) {
  214. guard componentsArray.count == 6 else {
  215. return
  216. }
  217. if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) {
  218. pickerView.selectRow(yearIndex, inComponent: 0, animated: true)
  219. }
  220. if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) {
  221. pickerView.selectRow(monthIndex, inComponent: 1, animated: true)
  222. }
  223. if let dayIndex = componentsArray[2][dayKey]?.firstIndex(of: date.day) {
  224. pickerView.selectRow(dayIndex, inComponent: 2, animated: true)
  225. }
  226. if let hourIndex = componentsArray[3][hourKey]?.firstIndex(of: date.hour) {
  227. pickerView.selectRow(hourIndex, inComponent: 3, animated: true)
  228. }
  229. if let minuteIndex = componentsArray[4][minuteKey]?.firstIndex(of: date.minute) {
  230. pickerView.selectRow(minuteIndex, inComponent: 4, animated: true)
  231. }
  232. if let secondIndex = componentsArray[5][secondKey]?.firstIndex(of: date.second) {
  233. pickerView.selectRow(secondIndex, inComponent: 5, animated: true)
  234. }
  235. }
  236. private func yearMonthDayStyleScrollTo(_ date: Date) {
  237. guard componentsArray.count == 3 else {
  238. return
  239. }
  240. if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) {
  241. pickerView.selectRow(yearIndex, inComponent: 0, animated: true)
  242. }
  243. if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) {
  244. pickerView.selectRow(monthIndex, inComponent: 1, animated: true)
  245. }
  246. if let dayIndex = componentsArray[2][dayKey]?.firstIndex(of: date.day) {
  247. pickerView.selectRow(dayIndex, inComponent: 2, animated: true)
  248. }
  249. }
  250. private func yearMonthStyleScrollTo(_ date: Date) {
  251. guard componentsArray.count == 2 else {
  252. return
  253. }
  254. if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) {
  255. pickerView.selectRow(yearIndex, inComponent: 0, animated: true)
  256. }
  257. if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) {
  258. pickerView.selectRow(monthIndex, inComponent: 1, animated: true)
  259. }
  260. }
  261. private func hourMinuteSecondStyleScrollTo(_ date: Date) {
  262. guard componentsArray.count == 3 else {
  263. return
  264. }
  265. if let hourIndex = componentsArray[0][hourKey]?.firstIndex(of: date.hour) {
  266. pickerView.selectRow(hourIndex, inComponent: 0, animated: true)
  267. }
  268. if let minuteIndex = componentsArray[1][minuteKey]?.firstIndex(of: date.minute) {
  269. pickerView.selectRow(minuteIndex, inComponent: 1, animated: true)
  270. }
  271. if let secondIndex = componentsArray[2][secondKey]?.firstIndex(of: date.second) {
  272. pickerView.selectRow(secondIndex, inComponent: 2, animated: true)
  273. }
  274. }
  275. private func yearMonthDayHourMinuteStyleScrollTo(_ date: Date) {
  276. guard componentsArray.count == 5 else {
  277. return
  278. }
  279. if let yearIndex = componentsArray[0][yearKey]?.firstIndex(of: date.year) {
  280. pickerView.selectRow(yearIndex, inComponent: 0, animated: true)
  281. }
  282. if let monthIndex = componentsArray[1][monthKey]?.firstIndex(of: date.month) {
  283. pickerView.selectRow(monthIndex, inComponent: 1, animated: true)
  284. }
  285. if let dayIndex = componentsArray[2][dayKey]?.firstIndex(of: date.day) {
  286. pickerView.selectRow(dayIndex, inComponent: 2, animated: true)
  287. }
  288. if let hourIndex = componentsArray[3][hourKey]?.firstIndex(of: date.hour) {
  289. pickerView.selectRow(hourIndex, inComponent: 3, animated: true)
  290. }
  291. if let minuteIndex = componentsArray[4][minuteKey]?.firstIndex(of: date.minute) {
  292. pickerView.selectRow(minuteIndex, inComponent: 4, animated: true)
  293. }
  294. }
  295. }
  296. // MARK: - init data
  297. extension DatePickerView {
  298. private func initDataWithAllStyle() {
  299. componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)])
  300. // if minLimitDate.haveSameYear(maxLimitDate) {
  301. // componentsArray.append([monthKey : Array(minLimitDate.month...maxLimitDate.month)])
  302. // } else {
  303. componentsArray.append([monthKey : Array(1...12)])
  304. // }
  305. // if minLimitDate.haveSameYearAndMonth(maxLimitDate) {
  306. // componentsArray.append([dayKey : Array(minLimitDate.day...maxLimitDate.day)])
  307. // } else {
  308. componentsArray.append([dayKey : Array(1...scrollToDate.numberOfDaysInMonth())])
  309. // }
  310. // if minLimitDate.haveSameYearMonthAndDay(maxLimitDate) {
  311. // componentsArray.append([hourKey : Array(minLimitDate.day...maxLimitDate.day)])
  312. // } else {
  313. componentsArray.append([hourKey : Array(0...23)])
  314. // }
  315. // if minLimitDate.haveSameYearMonthDayAndHour(maxLimitDate) {
  316. // componentsArray.append([minuteKey : Array(minLimitDate.minute...maxLimitDate.minute)])
  317. // } else {
  318. componentsArray.append([minuteKey : Array(0...59)])
  319. // }
  320. // if minLimitDate.haveSameYearMonthDayHourAndMinute(maxLimitDate) {
  321. // componentsArray.append([secondKey : Array(minLimitDate.minute...maxLimitDate.minute)])
  322. // } else {
  323. componentsArray.append([secondKey : Array(0...59)])
  324. // }
  325. }
  326. private func initDataWithYearMonthDayStyle() {
  327. componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)])
  328. // if minLimitDate.haveSameYear(maxLimitDate) {
  329. // componentsArray.append([monthKey : Array(minLimitDate.month...maxLimitDate.month)])
  330. // } else {
  331. componentsArray.append([monthKey : Array(1...12)])
  332. // }
  333. // if minLimitDate.haveSameYearAndMonth(maxLimitDate) {
  334. // componentsArray.append([dayKey : Array(minLimitDate.day...maxLimitDate.day)])
  335. // } else {
  336. componentsArray.append([dayKey : Array(1...scrollToDate.numberOfDaysInMonth())])
  337. // }
  338. }
  339. private func initDataWithYearMonthStyle() {
  340. componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)])
  341. componentsArray.append([monthKey : Array(1...12)])
  342. }
  343. private func initDataWithHourMinuteSecondStyle() {
  344. // if let date = Date.date("00:00:00", formatter: "HH:mm:ss") {
  345. // minLimitDate = date
  346. // }
  347. //
  348. // if let date = Date.date("23:59:59", formatter: "HH:mm:ss") {
  349. // maxLimitDate = date
  350. // }
  351. componentsArray.append([hourKey : Array(0...23)])
  352. // if minLimitDate.haveSameYearMonthDayAndHour(maxLimitDate) {
  353. // componentsArray.append([minuteKey : Array(minLimitDate.minute...maxLimitDate.minute)])
  354. // } else {
  355. componentsArray.append([minuteKey : Array(0...59)])
  356. // }
  357. // if minLimitDate.haveSameYearMonthDayHourAndMinute(maxLimitDate) {
  358. // componentsArray.append([secondKey : Array(minLimitDate.second...maxLimitDate.second)])
  359. // } else {
  360. componentsArray.append([secondKey : Array(0...59)])
  361. // }
  362. }
  363. private func initDataWithYearMonthDayHourMinuteStyle() {
  364. componentsArray.append([yearKey : Array(minLimitDate.year...maxLimitDate.year)])
  365. // if minLimitDate.haveSameYear(maxLimitDate) {
  366. // componentsArray.append([monthKey : Array(minLimitDate.month...maxLimitDate.month)])
  367. // } else {
  368. componentsArray.append([monthKey : Array(1...12)])
  369. // }
  370. // if minLimitDate.haveSameYearAndMonth(maxLimitDate) {
  371. // componentsArray.append([dayKey : Array(minLimitDate.day...maxLimitDate.day)])
  372. // } else {
  373. componentsArray.append([dayKey : Array(1...scrollToDate.numberOfDaysInMonth())])
  374. // }
  375. // if minLimitDate.haveSameYearMonthAndDay(maxLimitDate) {
  376. // componentsArray.append([hourKey : Array(minLimitDate.day...maxLimitDate.day)])
  377. // } else {
  378. componentsArray.append([hourKey : Array(0...23)])
  379. // }
  380. // if minLimitDate.haveSameYearMonthDayAndHour(maxLimitDate) {
  381. // componentsArray.append([minuteKey : Array(minLimitDate.minute...maxLimitDate.minute)])
  382. // } else {
  383. componentsArray.append([minuteKey : Array(0...59)])
  384. // }
  385. }
  386. }
  387. // MARK: - UIGestureRecognizerDelegate
  388. extension DatePickerView {
  389. func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
  390. guard let touchView = touch.view else { return false }
  391. if touchView.isDescendant(of: bottomView) {
  392. // 点击的view是否是bottomView或者bottomView的子视图
  393. return false
  394. }
  395. return true
  396. }
  397. }
  398. // MARK: - UIPickerViewDelegate, UIPickerViewDataSource
  399. extension DatePickerView {
  400. func numberOfComponents(in pickerView: UIPickerView) -> Int {
  401. return componentsArray.count
  402. }
  403. func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
  404. if let array = componentsArray[component].values.first {
  405. return array.count
  406. }
  407. return 0
  408. }
  409. func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
  410. return 40
  411. }
  412. func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
  413. var title: String = " "
  414. let dic = componentsArray[component]
  415. if let key = dic.keys.first {
  416. let data = dic[key]
  417. title = String(data![row]) + key
  418. }
  419. if let label = view as? UILabel {
  420. label.text = title
  421. return label
  422. }
  423. let label = UILabel()
  424. label.textAlignment = .center
  425. label.font = UIFont.systemFont(ofSize: 17)
  426. label.text = title
  427. label.sizeToFit()
  428. return label
  429. }
  430. func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
  431. // 月分变化了,天数要跟着变化.年份变化了,如果是2月,天数也可能变化
  432. func scrollToValidTimeRange() {
  433. // 检查当前时间是否在最大和最小时间之前
  434. if let currentDate = currentDate() {
  435. if currentDate > maxLimitDate {
  436. // 超过最大值,则滚回最大值
  437. scrollTo(maxLimitDate)
  438. } else if currentDate < minLimitDate {
  439. // 小于最小值,刚滚回最小值
  440. scrollTo(minLimitDate)
  441. }
  442. }
  443. }
  444. guard let key = componentsArray[component].keys.first, key == yearKey || key == monthKey else {
  445. // 只是滚动年或者月时,才需要处理天数的变化
  446. scrollToValidTimeRange()
  447. return
  448. }
  449. let yearIndex = pickerView.selectedRow(inComponent: 0)
  450. if yearIndex < 0 {
  451. scrollToValidTimeRange()
  452. return
  453. }
  454. if key == yearKey {
  455. // 更新年份水印
  456. let year = componentsArray[0][yearKey]![yearIndex] // 年份
  457. yearLabel.text = String(year)
  458. }
  459. if key == yearKey && pickerView.selectedRow(inComponent: component + 1) != 1 {
  460. // 滚动年,月分不是2月时,不需要处理天数变化
  461. scrollToValidTimeRange()
  462. return
  463. }
  464. let monthIndex = pickerView.selectedRow(inComponent: 1)
  465. if monthIndex < 0 {
  466. scrollToValidTimeRange()
  467. return
  468. }
  469. let year = componentsArray[0][yearKey]![yearIndex] // 年份
  470. let month = componentsArray[1][monthKey]![monthIndex] // 月份
  471. guard let date = Date.date(String(year) + "-" + String(month), formatter: "yyyy-MM") else {
  472. scrollToValidTimeRange()
  473. return
  474. }
  475. if self.style == .yearMonth {
  476. return
  477. }
  478. let numberOfDays = date.numberOfDaysInMonth() // 当前月的天数
  479. if numberOfDays != 0 && numberOfDays == componentsArray[2][dayKey]!.count {
  480. // 天数没有变化,不需要任何操作
  481. scrollToValidTimeRange()
  482. return
  483. }
  484. let newDayDic = [dayKey : Array(1...numberOfDays)] // 新的天数数据
  485. componentsArray.replaceSubrange(2...2, with: [newDayDic])
  486. pickerView.reloadComponent(2)
  487. scrollToValidTimeRange()
  488. }
  489. }