UIDatePicker select Month and Year

Viewed 63291

I need a UIDatePicker for selecting Month and Year only. I checked the class reference documents. Looks like UIDatePicker is a UIView. I imagined UIPickerView may be a sub view and I can hide the component if I can grab it. But no. That was not possible. Do I have to create my own custom picker then? Any ideas?

11 Answers

Rewritten answer of RossP in Swift 4:

import Foundation

@objc class ShortDatePickerView: UIPickerView  {

    enum Component: Int {
        case Month = 0
        case Year = 1
    }

    let LABEL_TAG = 43
    let bigRowCount = 1000
    let numberOfComponentsRequired = 2

    let months = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]
    var years: [String] {
        get {
            var years: [String] = [String]()

            for i in minYear...maxYear {
                years.append("\(i)")
            }

            return years;
        }
    }

    var bigRowMonthsCount: Int {
        get {
            return bigRowCount * months.count
        }
    }

    var bigRowYearsCount: Int {
        get {
            return bigRowCount * years.count
        }
    }

    var monthSelectedTextColor: UIColor?
    var monthTextColor: UIColor?
    var yearSelectedTextColor: UIColor?
    var yearTextColor: UIColor?
    var monthSelectedFont: UIFont?
    var monthFont: UIFont?
    var yearSelectedFont: UIFont?
    var yearFont: UIFont?

    let rowHeight: NSInteger = 44

    /**
     Will be returned in user's current TimeZone settings
    **/
    var date: Date {
        get {
            let month = self.months[selectedRow(inComponent: Component.Month.rawValue) % months.count]
            let year = self.years[selectedRow(inComponent: Component.Year.rawValue) % years.count]
            let formatter = DateFormatter()
            formatter.dateFormat = "MM yyyy"
            return formatter.date(from: "\(month) \(year)")!
        }
    }

    var minYear: Int!
    var maxYear: Int!

    override init(frame: CGRect) {
        super.init(frame: frame)
        loadDefaultParameters()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        loadDefaultParameters()
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        loadDefaultParameters()
    }

    func loadDefaultParameters() {
        minYear = Calendar.current.dateComponents([.year], from: Date()).year
        maxYear = minYear! + 10

        self.delegate = self
        self.dataSource = self

        monthSelectedTextColor = .blue
        monthTextColor = .black

        yearSelectedTextColor = .blue
        yearTextColor = .black

        monthSelectedFont = .boldSystemFont(ofSize: 17)
        monthFont = .boldSystemFont(ofSize: 17)

        yearSelectedFont = .boldSystemFont(ofSize: 17)
        yearFont = .boldSystemFont(ofSize: 17)
    }


    func setup(minYear: NSInteger, andMaxYear maxYear: NSInteger) {
        self.minYear = minYear

        if maxYear > minYear {
            self.maxYear = maxYear
        } else {
            self.maxYear = minYear + 10
        }
    }

    func selectToday() {
        selectRow(todayIndexPath.row, inComponent: Component.Month.rawValue, animated: false)
        selectRow(todayIndexPath.section, inComponent: Component.Year.rawValue, animated: false)
    }


    var todayIndexPath: NSIndexPath {
        get {
            var row = 0.0
            var section = 0.0

            for cellMonth in months {
                if cellMonth == currentMonthName {
                    row = Double(months.index(of: cellMonth)!)
                    row = row + Double(bigRowMonthsCount / 2)
                    break
                }
            }

            for cellYear in years {
                if cellYear == currentYearName {
                    section = Double(years.index(of: cellYear)!)
                    section = section + Double(bigRowYearsCount / 2)
                    break
                }
            }

            return NSIndexPath(row: Int(row), section: Int(section))
        }
    }

    var currentMonthName: String {
        get {
            let formatter = DateFormatter()
            let locale = Locale.init(identifier: "en_US")
            formatter.locale = locale
            formatter.dateFormat = "MM"
            return formatter.string(from: Date())
        }
    }

    var currentYearName: String {
        get {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy"
            return formatter.string(from: Date())
        }
    }


    func selectedColorForComponent(component: NSInteger) -> UIColor {
        if component == Component.Month.rawValue {
            return monthSelectedTextColor!
        }
        return yearSelectedTextColor!
    }

    func colorForComponent(component: NSInteger) -> UIColor {
        if component == Component.Month.rawValue {
            return monthTextColor!
        }
        return yearTextColor!
    }


    func selectedFontForComponent(component: NSInteger) -> UIFont {
        if component == Component.Month.rawValue {
            return monthSelectedFont!
        }
        return yearSelectedFont!
    }

    func fontForComponent(component: NSInteger) -> UIFont {
        if component == Component.Month.rawValue {
            return monthFont!
        }
        return yearFont!
    }


    func titleForRow(row: Int, forComponent component: Int) -> String? {
        if component == Component.Month.rawValue {
            return self.months[row % self.months.count]
        }
        return self.years[row % self.years.count]
    }


    func labelForComponent(component: NSInteger) -> UILabel {
        let frame = CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: CGFloat(rowHeight))
        let label = UILabel(frame: frame)
        label.textAlignment = .center
        label.backgroundColor = .clear
        label.isUserInteractionEnabled = false
        label.tag = LABEL_TAG
        return label
    }
}

extension ShortDatePickerView: UIPickerViewDelegate, UIPickerViewDataSource {

    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return numberOfComponentsRequired
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if (component == Component.Month.rawValue) {
            return bigRowMonthsCount
        } else {
            return bigRowYearsCount
        }
    }

    func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
        return self.bounds.size.width / CGFloat(numberOfComponentsRequired)
    }

    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        var selected = false

        if component == Component.Month.rawValue {
            let monthName = self.months[(row % self.months.count)]
            if monthName == currentMonthName {
                selected = true
            }
        } else {
            let yearName = self.years[(row % self.years.count)]
            if yearName == currentYearName {
                selected = true
            }
        }

        var returnView: UILabel
        if view?.tag == LABEL_TAG {
            returnView = view as! UILabel
        } else {
            returnView = labelForComponent(component: component)
        }

        returnView.font = selected ? selectedFontForComponent(component: component) : fontForComponent(component: component)
        returnView.textColor = selected ? selectedColorForComponent(component: component) : colorForComponent(component: component)

        returnView.text = titleForRow(row: row, forComponent: component)

        return returnView
    }

    func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        return CGFloat(rowHeight)
    }

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2
    }
}

I have a lazy workaround-- I made a 10x10 black png. I then used that as a mask with a scaletofill imageview sized to cover the dates column perfectly. I put the alpha at 0.75 to barely show that column. It is clear to the user and looks decent.

I'm using a simple UIPickerView with 2 components (months and years).

And calling the UIPickerViewDelegate with every row selection:

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    //Today year and month
    NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
    [dateFormat setDateFormat:@"yyyy"];
    int year = [[dateFormat stringFromDate:[NSDate date]] intValue];
    [dateFormat setDateFormat:@"MM"];
    int month = [[dateFormat stringFromDate:[NSDate date]] intValue] - 1;

    //picker year and month
    int selectedYear = [[self.yearsArray objectAtIndex:[pickerView selectedRowInComponent:1]] intValue];
    int selectedMonth = [pickerView selectedRowInComponent:0];

    // if month in the past, change the month
    if (year == selectedYear && selectedMonth < month)
    {
        [pickerView selectRow:month inComponent:0 animated:YES];
    }
}

You can use the open source library named

AKMonthYearPickerView

import AKMonthYearPickerView

AKMonthYearPickerView.sharedInstance.show(vc: viewController, doneHandler: doneHandler, completetionalHandler: completetionalHandler)

https://github.com/ali-cs/AKMonthYearPickerView

to install it using pods,

pod 'AKMonthYearPickerView' 

to customize previous years limit and bar color

AKMonthYearPickerView.sharedInstance.barTintColor =   UIColor.blue

AKMonthYearPickerView.sharedInstance.previousYear = 4

enter image description here

Related