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.
A DatePicker of Month and Year (only). The user has 2 options: 1 - Show month and year or 2 - show only year
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