Created
August 9, 2018 20:44
-
-
Save nathanmrtns/7d74c7c55f9f90d7c480d7e6a354d804 to your computer and use it in GitHub Desktop.
A DatePicker of Month and Year (only). The user has 2 options: 1 - Show month and year or 2 - show only year
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import UIKit | |
| protocol MIPickerDelegate: AnyObject { | |
| func miPicker(_ amDatePicker: MIPicker, didSelect date: Date) | |
| //optional func miPickerDidCancelSelection(_ amDatePicker: MIPicker) | |
| } | |
| class MIPicker: UIView, UIPickerViewDelegate, UIPickerViewDataSource { | |
| // MARK: - Config | |
| struct Config { | |
| fileprivate let contentHeight: CGFloat = 220 | |
| fileprivate let bouncingOffset: CGFloat = 0 | |
| var type: String? | |
| var confirmButtonTitle = NSLocalizedString("done", comment: "done") | |
| var cancelButtonTitle = NSLocalizedString("cancel", comment: "cancel") | |
| var options: [[String]] = [Utils.getInternationalizedMonths(), | |
| Utils.generateYearOptions(maxYear: Calendar.current.component(.year, from: Date()))] | |
| var selectedMonthRow: Int? | |
| var selectedYearRow: Int? | |
| var maxYearRow: Int? | |
| var headerHeight: CGFloat = 50 | |
| var animationDuration: TimeInterval = 0.25 | |
| var contentBackgroundColor: UIColor = UIColor.white | |
| var headerBackgroundColor: UIColor = UIColor(red: 244 / 255.0, green: 244 / 255.0, blue: 244 / 255.0, alpha: 1) | |
| var confirmButtonColor: UIColor = UIColor.ApplicationColor.wineRed | |
| var cancelButtonColor: UIColor = UIColor.ApplicationColor.wineRed | |
| var overlayBackgroundColor: UIColor = UIColor.white | |
| } | |
| var config = Config() | |
| weak var delegate: MIPickerDelegate? | |
| // MARK: - IBOutlets | |
| @IBOutlet weak var datePicker: UIPickerView! | |
| @IBOutlet weak var confirmButton: UIButton! | |
| @IBOutlet weak var cancelButton: UIButton! | |
| @IBOutlet weak var headerView: UIView! | |
| @IBOutlet weak var backgroundView: UIView! | |
| @IBOutlet weak var headerViewHeightConstraint: NSLayoutConstraint! | |
| var bottomConstraint: NSLayoutConstraint! | |
| var overlayButton: UIButton! | |
| var isPickerBeingShown = false | |
| var selectedDate: Date? | |
| // MARK: - Init | |
| static func getFromNib() -> MIPicker { | |
| return UINib.init(nibName: String(describing: self), bundle: nil) | |
| .instantiate(withOwner: self, options: nil).last as! MIPicker // swiftlint:disable:this force_cast | |
| } | |
| override func awakeFromNib() { | |
| super.awakeFromNib() | |
| datePicker.dataSource = self | |
| datePicker.delegate = self | |
| config.selectedMonthRow = config.options[0].count - 1 | |
| config.selectedYearRow = config.options[1].count - 1 | |
| config.maxYearRow = config.options[1].count - 1 | |
| } | |
| // MARK: - IBAction | |
| @IBAction func confirmButtonDidTapped(_ sender: AnyObject) { | |
| var rows: [Int] = [] | |
| for component in 0...datePicker.numberOfComponents - 1 { | |
| rows.append(datePicker.selectedRow(inComponent: component)) | |
| } | |
| if config.type == "month" { | |
| config.selectedMonthRow = rows[0] | |
| config.selectedYearRow = rows[1] | |
| } else { | |
| config.selectedYearRow = rows[0] | |
| } | |
| let userCalendar = Calendar.current | |
| var components = DateComponents() | |
| let options = config.options | |
| components.year = Int(options[1][config.selectedYearRow!]) | |
| components.month = config.type == "month" ? config.selectedMonthRow! + 1 : 1 | |
| components.day = 1 | |
| let date = userCalendar.date(from: components)! | |
| selectedDate = date | |
| dismiss() | |
| delegate?.miPicker(self, didSelect: selectedDate!) | |
| } | |
| @IBAction func cancelButtonDidTapped(_ sender: AnyObject) { | |
| dismiss() | |
| //delegate?.miPickerDidCancelSelection(self) | |
| } | |
| // MARK: - Private | |
| fileprivate func setup(_ parentVC: UIViewController) { | |
| // Loading configuration | |
| headerViewHeightConstraint.constant = config.headerHeight | |
| setHeaderColors() | |
| // Overlay view constraints setup | |
| overlayButton = UIButton(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, | |
| height: UIScreen.main.bounds.height)) | |
| overlayButton.backgroundColor = config.overlayBackgroundColor | |
| overlayButton.alpha = 0 | |
| overlayButton.addTarget(self, action: #selector(cancelButtonDidTapped(_:)), for: .touchUpInside) | |
| if !overlayButton.isDescendant(of: parentVC.view) { parentVC.view.addSubview(overlayButton) } | |
| overlayButton.translatesAutoresizingMaskIntoConstraints = false | |
| parentVC.view.addConstraints([ | |
| NSLayoutConstraint(item: overlayButton, attribute: .bottom, relatedBy: .equal, | |
| toItem: parentVC.view, attribute: .bottom, multiplier: 1, constant: 0), | |
| NSLayoutConstraint(item: overlayButton, attribute: .top, relatedBy: .equal, | |
| toItem: parentVC.view, attribute: .top, multiplier: 1, constant: 0), | |
| NSLayoutConstraint(item: overlayButton, attribute: .leading, relatedBy: .equal, | |
| toItem: parentVC.view, attribute: .leading, multiplier: 1, constant: 0), | |
| NSLayoutConstraint(item: overlayButton, attribute: .trailing, relatedBy: .equal, | |
| toItem: parentVC.view, attribute: .trailing, multiplier: 1, constant: 0) | |
| ] | |
| ) | |
| // Setup picker constraints | |
| //config.contentHeight + config.headerHeight | |
| frame = CGRect(x: 0, y: UIScreen.main.bounds.height, width: UIScreen.main.bounds.width, | |
| height: config.contentHeight + config.headerHeight) | |
| translatesAutoresizingMaskIntoConstraints = false | |
| bottomConstraint = NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, | |
| toItem: parentVC.view, attribute: .bottom, multiplier: 1, constant: 0) | |
| if !isDescendant(of: parentVC.view) { parentVC.view.addSubview(self) } | |
| parentVC.view.addConstraints([ | |
| //bottomConstraint, | |
| NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: parentVC.view, | |
| attribute: .leading, multiplier: 1, constant: 0), | |
| NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: parentVC.view, | |
| attribute: .trailing, multiplier: 1, constant: 0) | |
| ] | |
| ) | |
| addConstraint( | |
| NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, | |
| attribute: .notAnAttribute, multiplier: 1.0, constant: frame.height) | |
| ) | |
| move(goUp: false) | |
| } | |
| func setHeaderColors() { | |
| confirmButton.setTitle(config.confirmButtonTitle, for: UIControlState()) | |
| cancelButton.setTitle(config.cancelButtonTitle, for: UIControlState()) | |
| confirmButton.setTitleColor(config.confirmButtonColor, for: UIControlState()) | |
| cancelButton.setTitleColor(config.cancelButtonColor, for: UIControlState()) | |
| headerView.backgroundColor = config.headerBackgroundColor | |
| backgroundView.backgroundColor = config.contentBackgroundColor | |
| } | |
| fileprivate func move(goUp: Bool) { | |
| bottomConstraint.constant = goUp ? config.bouncingOffset : config.contentHeight + config.headerHeight | |
| } | |
| // MARK: - Public | |
| func show(inVC parentVC: UIViewController, completion: (() -> Void)? = nil) { | |
| isPickerBeingShown = true | |
| parentVC.view.endEditing(true) | |
| setup(parentVC) | |
| move(goUp: false) | |
| UIView.animate( | |
| withDuration: config.animationDuration, delay: 0, usingSpringWithDamping: 0.7, | |
| initialSpringVelocity: 5, options: .curveEaseIn, animations: { | |
| parentVC.view.layoutIfNeeded() | |
| self.overlayButton.alpha = 1 | |
| }, completion: { (_) in | |
| completion?() | |
| } | |
| ) | |
| if config.type == "month" { | |
| let date = Date() | |
| let calendar = Calendar.current | |
| let month = calendar.component(.month, from: date) - 1 | |
| if config.selectedMonthRow == nil && config.selectedYearRow == nil { | |
| datePicker.selectRow(config.maxYearRow!, inComponent: 1, animated: false) | |
| datePicker.selectRow(config.selectedMonthRow!, inComponent: 0, animated: false) | |
| } else { | |
| if config.selectedYearRow != nil { | |
| datePicker.selectRow(config.selectedYearRow!, inComponent: 1, animated: false) | |
| } else { | |
| datePicker.selectRow(config.maxYearRow!, inComponent: 1, animated: false) | |
| } | |
| if config.selectedMonthRow != nil && config.selectedMonthRow! > month { | |
| datePicker.selectRow(month, inComponent: 0, animated: false) | |
| } else { | |
| datePicker.selectRow(config.selectedMonthRow!, inComponent: 0, animated: false) | |
| } | |
| } | |
| } else { | |
| if config.selectedYearRow != nil { | |
| datePicker.selectRow(config.selectedYearRow!, inComponent: 0, animated: false) | |
| } else { | |
| if config.maxYearRow != nil { | |
| datePicker.selectRow(config.maxYearRow!, inComponent: 0, animated: false) | |
| } else { | |
| datePicker.selectRow(0, inComponent: 0, animated: false) | |
| } | |
| } | |
| } | |
| } | |
| func dismiss(_ completion: (() -> Void)? = nil) { | |
| isPickerBeingShown = false | |
| move(goUp: false) | |
| UIView.animate( | |
| withDuration: 0, animations: { | |
| self.layoutIfNeeded() | |
| self.overlayButton.alpha = 0 | |
| }, completion: { (_) in | |
| completion?() | |
| self.removeFromSuperview() | |
| self.overlayButton.removeFromSuperview() | |
| } | |
| ) | |
| } | |
| // returns the number of 'columns' to display. | |
| func numberOfComponents(in pickerView: UIPickerView) -> Int { | |
| if config.type == "month" { | |
| return 2 | |
| } | |
| return 1 | |
| } | |
| // returns the # of rows in each component.. | |
| func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { | |
| if config.type == "month" { | |
| return config.options[component].count | |
| } else { | |
| return config.options[1].count | |
| } | |
| } | |
| // returns the title of a row | |
| func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { | |
| if config.type == "month" { | |
| return config.options[component][row] | |
| } else { | |
| return config.options[1][row] | |
| } | |
| } | |
| func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { | |
| let date = Date() | |
| let calendar = Calendar.current | |
| let month = calendar.component(.month, from: date) - 1 | |
| if config.type != nil && config.type == "month" { | |
| if pickerView.selectedRow(inComponent: 1) == config.maxYearRow && | |
| pickerView.selectedRow(inComponent: 0) > month { | |
| pickerView.selectRow(month, inComponent: 0, animated: true) | |
| } | |
| } | |
| } | |
| func goForward() { | |
| if config.type == "month" { | |
| if config.selectedMonthRow! + 1 <= 11 { | |
| config.selectedMonthRow = config.selectedMonthRow! + 1 | |
| } else { | |
| config.selectedMonthRow = 0 | |
| config.selectedYearRow = config.selectedYearRow! + 1 | |
| } | |
| } else { | |
| config.selectedYearRow = config.selectedYearRow! + 1 | |
| } | |
| delegate?.miPicker(self, didSelect: updateSelectedDate()) | |
| } | |
| func goBackward() { | |
| if config.type == "month" { | |
| if config.selectedMonthRow! - 1 >= 0 { | |
| config.selectedMonthRow = config.selectedMonthRow! - 1 | |
| } else if config.selectedMonthRow! - 1 < 0 && config.selectedYearRow!-1 >= 0 { | |
| config.selectedMonthRow = 11 | |
| config.selectedYearRow = config.selectedYearRow! - 1 | |
| } | |
| } else { | |
| config.selectedMonthRow = 0 | |
| config.selectedYearRow = config.selectedYearRow! - 1 | |
| } | |
| delegate?.miPicker(self, didSelect: updateSelectedDate()) | |
| //return [config.selectedMonthRow!, config.selectedYearRow!] | |
| } | |
| func updateSelectedDate() -> Date { | |
| let userCalendar = Calendar.current | |
| var components = DateComponents() | |
| let options = config.options | |
| components.year = Int(options[1][config.selectedYearRow!]) | |
| components.month = config.type == "month" ? config.selectedMonthRow! + 1 : 1 | |
| components.day = 1 | |
| let date = userCalendar.date(from: components)! | |
| selectedDate = date | |
| return selectedDate! | |
| } | |
| func isSelectedDateMaximum() -> Bool { | |
| if config.type == "month" { | |
| let nextDate = Calendar.current.date(byAdding: .month, value: 1, to: selectedDate!) | |
| if nextDate?.compare(Date()) == .orderedAscending { | |
| return false | |
| } else { | |
| return true | |
| } | |
| } else { | |
| let nextDate = Calendar.current.date(byAdding: .year, value: 1, to: selectedDate!) | |
| if nextDate?.compare(Date()) == .orderedAscending { | |
| return false | |
| } else { | |
| return true | |
| } | |
| } | |
| } | |
| func isSelectedDateMinimum() -> Bool { | |
| let comp = DateComponents(year: Constants.minimumYear, month: 1, day: 1) | |
| let date = Calendar.current.date(from: comp) | |
| if config.type == "month" { | |
| let nextDate = Calendar.current.date(byAdding: .month, value: -1, to: selectedDate!) | |
| if nextDate?.compare(date!) == .orderedAscending { | |
| return true | |
| } else { | |
| return false | |
| } | |
| } else { | |
| let nextDate = Calendar.current.date(byAdding: .year, value: -1, to: selectedDate!) | |
| if nextDate?.compare(date!) == .orderedAscending { | |
| return true | |
| } else { | |
| return false | |
| } | |
| } | |
| } | |
| func getSelectedDateString() -> String { | |
| _ = updateSelectedDate() | |
| let dateFormatter = DateFormatter() | |
| if config.type!.elementsEqual("month") { | |
| dateFormatter.dateFormat = "MMM - yyyy" | |
| return dateFormatter.string(from: selectedDate!).uppercased() | |
| } else { | |
| dateFormatter.dateFormat = "yyyy" | |
| return dateFormatter.string(from: selectedDate!) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment