Skip to content

Instantly share code, notes, and snippets.

@nathanmrtns
Created August 9, 2018 20:44
Show Gist options
  • Save nathanmrtns/7d74c7c55f9f90d7c480d7e6a354d804 to your computer and use it in GitHub Desktop.
Save nathanmrtns/7d74c7c55f9f90d7c480d7e6a354d804 to your computer and use it in GitHub Desktop.

Revisions

  1. nathanmrtns created this gist Aug 9, 2018.
    380 changes: 380 additions & 0 deletions MiPicker.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,380 @@
    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!)
    }
    }
    }