You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

705 lines
24 KiB

//
// IQDropDownTextField.swift
// Copyright (c) 2020-21 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
// swiftlint:disable type_body_length
// swiftlint:disable file_length
open class IQDropDownTextField: UITextField {
public static let optionalItemIndex: Int = -1
open lazy var pickerView: UIPickerView = {
let view = UIPickerView()
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.showsSelectionIndicator = true
view.delegate = self
view.dataSource = self
return view
}()
open lazy var timePicker: UIDatePicker = {
let view = UIDatePicker()
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.datePickerMode = .time
if #available(iOS 13.4, *) {
view.preferredDatePickerStyle = .wheels
}
view.addTarget(self, action: #selector(timeChanged(_:)), for: .valueChanged)
return view
}()
open lazy var dateTimePicker: UIDatePicker = {
let view = UIDatePicker()
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.datePickerMode = .dateAndTime
if #available(iOS 13.4, *) {
view.preferredDatePickerStyle = .wheels
}
view.addTarget(self, action: #selector(dateTimeChanged(_:)), for: .valueChanged)
return view
}()
open lazy var datePicker: UIDatePicker = {
let view = UIDatePicker()
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.datePickerMode = .date
if #available(iOS 13.4, *) {
view.preferredDatePickerStyle = .wheels
}
view.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged)
return view
}()
private lazy var dismissToolbar: UIToolbar = {
let view = UIToolbar()
view.isTranslucent = true
view.sizeToFit()
let buttonflexible: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
target: nil, action: nil)
let buttonDone: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self,
action: #selector(resignFirstResponder))
view.items = [buttonflexible, buttonDone]
return view
}()
open var showDismissToolbar: Bool {
get {
return (inputAccessoryView == dismissToolbar)
}
set {
inputAccessoryView = (newValue ? dismissToolbar : nil)
}
}
internal let privateMenuButton: UIButton = UIButton()
// Sets a custom font for the IQDropdownTextField items. Default is boldSystemFontOfSize:18.0.
open var dropDownFont: UIFont?
// Sets a custom color for the IQDropdownTextField items. Default is blackColor.
open var dropDownTextColor: UIColor?
// Width and height to adopt for each section.
// If you don't want to specify a row width then use 0 to calculate row width automatically.
open var widthsForComponents: [CGFloat]?
open var heightsForComponents: [CGFloat]?
open var dateFormatter: DateFormatter = DateFormatter() {
didSet {
datePicker.locale = dateFormatter.locale
}
}
open var timeFormatter: DateFormatter = DateFormatter() {
didSet {
timePicker.locale = timeFormatter.locale
}
}
open var dateTimeFormatter: DateFormatter = DateFormatter() {
didSet {
dateTimePicker.locale = dateTimeFormatter.locale
}
}
weak open var dropDownDelegate: IQDropDownTextFieldDelegate?
weak open override var delegate: UITextFieldDelegate? {
didSet {
dropDownDelegate = delegate as? IQDropDownTextFieldDelegate
}
}
open var dataSource: IQDropDownTextFieldDataSource?
open var dropDownMode: IQDropDownMode = .list {
didSet {
switch dropDownMode {
case .list, .multiList:
inputView = pickerView
setSelectedRows(rows: selectedRows, animated: true)
case .date:
inputView = datePicker
if !isOptionalDropDown {
date = datePicker.date
}
case .time:
inputView = timePicker
if !isOptionalDropDown {
date = timePicker.date
}
case .dateTime:
inputView = dateTimePicker
if !isOptionalDropDown {
date = dateTimePicker.date
}
case .textField:
inputView = nil
}
if #available(iOS 15.0, *) {
reconfigureMenu()
}
}
}
private var privateOptionalItemText: String?
@IBInspectable open var optionalItemText: String? {
get {
if let privateOptionalItemText = privateOptionalItemText, !privateOptionalItemText.isEmpty {
return privateOptionalItemText
} else {
return NSLocalizedString("Select", comment: "")
}
}
set {
privateOptionalItemText = newValue
privateUpdateOptionsList()
}
}
private var privateOptionalItemTexts: [String?] = []
open var optionalItemTexts: [String?] {
get {
return privateOptionalItemTexts
}
set {
privateOptionalItemTexts = newValue
privateUpdateOptionsList()
}
}
@IBInspectable open var isOptionalDropDown: Bool {
get { return privateIsOptionalDropDowns.first ?? true }
set {
isOptionalDropDowns = [newValue]
}
}
private var privateIsOptionalDropDowns: [Bool] = []
open var isOptionalDropDowns: [Bool] {
get { return privateIsOptionalDropDowns }
set {
if !hasSetInitialIsOptional || privateIsOptionalDropDowns != newValue {
let previousSelectedRows: [Int] = selectedRows
privateIsOptionalDropDowns = newValue
hasSetInitialIsOptional = true
if dropDownMode == .list || dropDownMode == .multiList {
pickerView.reloadAllComponents()
selectedRows = previousSelectedRows
}
}
}
}
open var multilistSelectionFormatHandler: ((_ selectedItems: [String?], _ selectedIndexes: [Int]) -> String)? {
didSet {
if let handler = multilistSelectionFormatHandler {
super.text = handler(selectedItems, selectedRows)
} else {
super.text = selectedItems.compactMap({ $0 }).joined(separator: ", ")
}
}
}
open var selectionFormatHandler: ((_ selectedItem: String?, _ selectedIndex: Int) -> String)? {
didSet {
if let handler = selectionFormatHandler {
super.text = handler(selectedItem, selectedRow)
} else {
super.text = selectedItems.compactMap({ $0 }).joined(separator: ", ")
}
}
}
@available(*, deprecated, message: "use 'selectedItem' instead")
open override var text: String? {
didSet {
}
}
@available(*, deprecated, message: "use 'selectedItem' instead")
open override var attributedText: NSAttributedString? {
didSet {
}
}
open var itemList: [String] {
get {
multiItemList.first ?? []
}
set {
multiItemList = [newValue]
}
}
open var itemListView: [UIView?] {
get {
multiItemListView.first ?? []
}
set {
multiItemListView = [newValue]
}
}
open var multiItemList: [[String]] = [] {
didSet {
//Refreshing pickerView
isOptionalDropDowns = privateIsOptionalDropDowns
let selectedRows = selectedRows
self.selectedRows = selectedRows
}
}
open var multiItemListView: [[UIView?]] = [] {
didSet {
//Refreshing pickerView
isOptionalDropDowns = privateIsOptionalDropDowns
let selectedRows = selectedRows
self.selectedRows = selectedRows
}
}
open override var adjustsFontSizeToFitWidth: Bool {
didSet {
privateUpdateOptionsList()
}
}
private var hasSetInitialIsOptional: Bool = false
func dealloc() {
pickerView.delegate = nil
pickerView.dataSource = nil
self.delegate = nil
dataSource = nil
privateOptionalItemText = nil
}
// MARK: - Initialization
func initialize() {
contentVerticalAlignment = .center
contentHorizontalAlignment = .center
// These will update the UI and other components,
// all the validation added if awakeFromNib for textField is called after custom UIView awakeFromNib call
do {
let mode = dropDownMode
dropDownMode = mode
isOptionalDropDown = hasSetInitialIsOptional ? isOptionalDropDown : true
}
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
timeFormatter.dateStyle = .none
timeFormatter.timeStyle = .short
dateTimeFormatter.dateStyle = .medium
dateTimeFormatter.timeStyle = .short
if #available(iOS 15.0, *) {
initializeMenu()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
}
open override func awakeFromNib() {
super.awakeFromNib()
initialize()
}
// MARK: - UIView overrides
@discardableResult
open override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder()
for (index, selectdRow) in privatePickerSelectedRows where 0 <= selectdRow {
pickerView.selectRow(selectdRow, inComponent: index, animated: false)
}
return result
}
// MARK: - UITextField overrides
open override func caretRect(for position: UITextPosition) -> CGRect {
if dropDownMode == .textField {
return super.caretRect(for: position)
} else {
return .zero
}
}
// MARK: - Selected Row
open var selectedRow: Int {
get {
var pickerViewSelectedRow: Int = selectedRows.first /*It may return -1*/ ?? 0
pickerViewSelectedRow = max(pickerViewSelectedRow, 0)
return pickerViewSelectedRow - (isOptionalDropDown ? 1 : 0)
}
set {
selectedRows = [newValue]
}
}
// Key represents section index and value represents selection
internal var privatePickerSelectedRows: [Int: Int] = [:]
open var selectedRows: [Int] {
get {
var selection: [Int] = []
for index in multiItemList.indices {
let isOptionalDropDown: Bool
if index < isOptionalDropDowns.count {
isOptionalDropDown = isOptionalDropDowns[index]
} else if let last = isOptionalDropDowns.last {
isOptionalDropDown = last
} else {
isOptionalDropDown = true
}
var pickerViewSelectedRow: Int = privatePickerSelectedRows[index] ?? -1 /*It may return -1*/
pickerViewSelectedRow = max(pickerViewSelectedRow, 0)
let finalSelection = pickerViewSelectedRow - (isOptionalDropDown ? 1 : 0)
selection.append(finalSelection)
}
return selection
}
set {
setSelectedRows(rows: newValue, animated: false)
}
}
open func selectedRow(inSection section: Int) -> Int {
privatePickerSelectedRows[section] ?? Self.optionalItemIndex
}
// open func rowSize(forComponent component: Int) -> CGSize
open func setSelectedRow(row: Int, animated: Bool) {
setSelectedRows(rows: [row], animated: animated)
}
open func setSelectedRow(row: Int, inSection section: Int, animated: Bool) {
var selectedRows = selectedRows
selectedRows[section] = row
setSelectedRows(rows: selectedRows, animated: animated)
}
open func setSelectedRows(rows: [Int], animated: Bool) {
var finalResults: [String?] = []
for (index, row) in rows.enumerated() {
let itemList: [String]
if index < multiItemList.count {
itemList = multiItemList[index]
} else {
itemList = []
}
let isOptionalDropDown: Bool
if index < isOptionalDropDowns.count {
isOptionalDropDown = isOptionalDropDowns[index]
} else if let last = isOptionalDropDowns.last {
isOptionalDropDown = last
} else {
isOptionalDropDown = true
}
if row == Self.optionalItemIndex {
if !isOptionalDropDown, !itemList.isEmpty {
finalResults.append(itemList[0])
} else {
finalResults.append(nil)
}
} else {
if row < itemList.count {
finalResults.append(itemList[row])
} else {
finalResults.append(nil)
}
}
let pickerViewRow: Int = row + (isOptionalDropDown ? 1 : 0)
privatePickerSelectedRows[index] = pickerViewRow
if index < pickerView.numberOfComponents {
pickerView.selectRow(pickerViewRow, inComponent: index, animated: animated)
}
}
if let multilistSelectionFormatHandler = multilistSelectionFormatHandler {
super.text = multilistSelectionFormatHandler(finalResults, rows)
} else if let selectionFormatHandler = selectionFormatHandler,
let selectedItem = finalResults.first,
let selectedRow = rows.first {
super.text = selectionFormatHandler(selectedItem, selectedRow)
} else {
super.text = finalResults.compactMap({ $0 }).joined(separator: ", ")
}
}
// MARK: - Setters
// `setDropDownMode:` has moved as a setter.
// `setItemList:` has moved as a setter.
open var selectedItem: String? {
get {
return selectedItems.first ?? nil
}
set {
switch dropDownMode {
case .multiList:
if let newValue = newValue {
selectedItems = [newValue]
} else {
selectedItems = multiItemList.map({ _ in nil }) // Resetting every section
}
case .list, .date, .time, .dateTime, .textField:
selectedItems = [newValue]
}
}
}
open var selectedItems: [String?] {
get {
switch dropDownMode {
case .list, .multiList:
var finalSelection: [String?] = []
for (index, selectedRow) in selectedRows.enumerated() {
if 0 <= selectedRow, index < multiItemList.count {
finalSelection.append(multiItemList[index][selectedRow])
} else {
finalSelection.append(nil)
}
}
return finalSelection
case .date:
return (super.text?.isEmpty ?? true) ? [nil] : [dateFormatter.string(from: datePicker.date)]
case .time:
return (super.text?.isEmpty ?? true) ? [nil] : [timeFormatter.string(from: timePicker.date)]
case .dateTime:
return (super.text?.isEmpty ?? true) ? [nil] : [dateTimeFormatter.string(from: dateTimePicker.date)]
case .textField:
return [super.text]
}
}
set {
privateSetSelectedItems(selectedItems: newValue, animated: false, shouldNotifyDelegate: false)
}
}
open func setSelectedItem(selectedItem: String?, animated: Bool) {
privateSetSelectedItems(selectedItems: [selectedItem], animated: animated, shouldNotifyDelegate: false)
}
open func setSelectedItems(selectedItems: [String?], animated: Bool) {
privateSetSelectedItems(selectedItems: selectedItems, animated: animated, shouldNotifyDelegate: false)
}
open func privateUpdateOptionsList() {
switch dropDownMode {
case .date:
if !isOptionalDropDown {
date = datePicker.date
}
case .time:
if !isOptionalDropDown {
date = timePicker.date
}
case .dateTime:
if !isOptionalDropDown {
date = dateTimePicker.date
}
case .list, .multiList:
pickerView.reloadAllComponents()
case .textField:
break
}
}
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(cut(_:)) || action == #selector(paste(_:)) {
return false
} else {
return super.canPerformAction(action, withSender: sender)
}
}
}
internal extension IQDropDownTextField {
// swiftlint:disable cyclomatic_complexity
// swiftlint:disable function_body_length
func privateSetSelectedItems(selectedItems: [String?],
animated: Bool, shouldNotifyDelegate: Bool) {
switch dropDownMode {
case .list, .multiList:
var finalIndexes: [Int] = []
var finalSelection: [String?] = []
for (index, selectedItem) in selectedItems.enumerated() {
if let selectedItem = selectedItem,
index < multiItemList.count,
let index = multiItemList[index].firstIndex(of: selectedItem) {
finalIndexes.append(index)
finalSelection.append(selectedItem)
} else {
let isOptionalDropDown: Bool
if index < isOptionalDropDowns.count {
isOptionalDropDown = isOptionalDropDowns[index]
} else if let last = isOptionalDropDowns.last {
isOptionalDropDown = last
} else {
isOptionalDropDown = true
}
let selectedIndex = isOptionalDropDown ? Self.optionalItemIndex : 0
finalIndexes.append(selectedIndex)
finalSelection.append(nil)
}
}
setSelectedRows(rows: finalIndexes, animated: animated)
if shouldNotifyDelegate {
if dropDownMode == .multiList {
dropDownDelegate?.textField(textField: self, didSelectItems: finalSelection)
} else if let selectedItem = finalSelection.first {
dropDownDelegate?.textField(textField: self, didSelectItem: selectedItem)
}
}
case .date:
if let selectedItem = selectedItems.first,
let selectedItem = selectedItem,
let date = dateFormatter.date(from: selectedItem) {
super.text = selectedItem
datePicker.setDate(date, animated: animated)
if shouldNotifyDelegate {
dropDownDelegate?.textField(textField: self, didSelectDate: date)
}
} else if isOptionalDropDown,
let selectedItem = selectedItems.first,
(selectedItem?.isEmpty ?? true) {
super.text = ""
datePicker.setDate(Date(), animated: animated)
if shouldNotifyDelegate {
dropDownDelegate?.textField(textField: self, didSelectDate: nil)
}
}
case .time:
if let selectedItem = selectedItems.first,
let selectedItem = selectedItem,
let time = timeFormatter.date(from: selectedItem) {
let day: Date = Date(timeIntervalSinceReferenceDate: 0)
let componentsForDay: Set<Calendar.Component> = [.era, .year, .month, .day]
let componentsForTime: Set<Calendar.Component> = [.hour, .minute, .second]
var componentsDay: DateComponents = Calendar.current.dateComponents(componentsForDay, from: day)
let componentsTime: DateComponents = Calendar.current.dateComponents(componentsForTime, from: time)
componentsDay.hour = componentsTime.hour
componentsDay.minute = componentsTime.minute
componentsDay.second = componentsTime.second
if let date = Calendar.current.date(from: componentsDay) {
super.text = selectedItem
timePicker.setDate(date, animated: animated)
if shouldNotifyDelegate {
dropDownDelegate?.textField(textField: self, didSelectDate: date)
}
}
} else if isOptionalDropDown,
let selectedItem = selectedItems.first,
(selectedItem?.isEmpty ?? true) {
super.text = ""
timePicker.setDate(Date(), animated: animated)
if shouldNotifyDelegate {
dropDownDelegate?.textField(textField: self, didSelectDate: nil)
}
}
case .dateTime:
if let selectedItem = selectedItems.first,
let selectedItem = selectedItem,
let date: Date = dateTimeFormatter.date(from: selectedItem) {
super.text = selectedItem
dateTimePicker.setDate(date, animated: animated)
if shouldNotifyDelegate {
dropDownDelegate?.textField(textField: self, didSelectDate: date)
}
} else if isOptionalDropDown,
let selectedItem = selectedItems.first,
(selectedItem?.isEmpty ?? true) {
super.text = ""
dateTimePicker.setDate(Date(), animated: animated)
if shouldNotifyDelegate {
dropDownDelegate?.textField(textField: self, didSelectDate: nil)
}
}
case .textField:
super.text = selectedItems.compactMap({ $0 }).joined(separator: ", ")
}
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable function_body_length
}