diff --git a/DropDown.podspec b/DropDown.podspec index f38c955..1134d77 100644 --- a/DropDown.podspec +++ b/DropDown.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.author = { "kevin-hirsch" => "kevin.hirsch.be@gmail.com" } s.social_media_url = "http://twitter.com/kevinh6113" - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' s.source = { :git => "https://github.com/AssistoLab/DropDown.git", :tag => "v#{s.version.to_s}" diff --git a/DropDown/helpers/DPDConstants.swift b/DropDown/helpers/DPDConstants.swift index b5caa30..67026af 100644 --- a/DropDown/helpers/DPDConstants.swift +++ b/DropDown/helpers/DPDConstants.swift @@ -19,6 +19,7 @@ internal struct DPDConstant { internal struct ReusableIdentifier { static let DropDownCell = "DropDownCell" + static let MoreDropDownCell = "MoreDropDownCell" } diff --git a/DropDown/resources/MoreDropDownCell.xib b/DropDown/resources/MoreDropDownCell.xib new file mode 100644 index 0000000..e2483a9 --- /dev/null +++ b/DropDown/resources/MoreDropDownCell.xib @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DropDown/src/DropDown.swift b/DropDown/src/DropDown.swift index 25fa2b2..2ab31b7 100644 --- a/DropDown/src/DropDown.swift +++ b/DropDown/src/DropDown.swift @@ -14,75 +14,79 @@ public typealias SelectionClosure = (Index, String) -> Void public typealias MultiSelectionClosure = ([Index], [String]) -> Void public typealias ConfigurationClosure = (Index, String) -> String public typealias CellConfigurationClosure = (Index, String, DropDownCell) -> Void +public typealias MoreCellConfigurationClosure = (MoreDropDownCell) -> Void private typealias ComputeLayoutTuple = (x: CGFloat, y: CGFloat, width: CGFloat, offscreenHeight: CGFloat) /// Can be `UIView` or `UIBarButtonItem`. @objc -public protocol AnchorView: class { +public protocol AnchorView: AnyObject { - var plainView: UIView { get } + var plainView: UIView { get } } extension UIView: AnchorView { - public var plainView: UIView { - return self - } + public var plainView: UIView { + return self + } } extension UIBarButtonItem: AnchorView { - public var plainView: UIView { - return value(forKey: "view") as! UIView - } + public var plainView: UIView { + return value(forKey: "view") as! UIView + } } /// A Material Design drop down in replacement for `UIPickerView`. public final class DropDown: UIView { - //TODO: handle iOS 7 landscape mode - - /// The dismiss mode for a drop down. - public enum DismissMode { + //TODO: handle iOS 7 landscape mode - /// A tap outside the drop down is required to dismiss. - case onTap + /// The dismiss mode for a drop down. + public enum DismissMode { - /// No tap is required to dismiss, it will dimiss when interacting with anything else. - case automatic + /// A tap outside the drop down is required to dismiss. + case onTap - /// Not dismissable by the user. - case manual + /// No tap is required to dismiss, it will dimiss when interacting with anything else. + case automatic - } + /// Not dismissable by the user. + case manual - /// The direction where the drop down will show from the `anchorView`. - public enum Direction { + } - /// The drop down will show below the anchor view when possible, otherwise above if there is more place than below. - case any + /// The direction where the drop down will show from the `anchorView`. + public enum Direction { - /// The drop down will show above the anchor view or will not be showed if not enough space. - case top + /// The drop down will show below the anchor view when possible, otherwise above if there is more place than below. + case any - /// The drop down will show below or will not be showed if not enough space. - case bottom + /// The drop down will show above the anchor view or will not be showed if not enough space. + case top - } + /// The drop down will show below or will not be showed if not enough space. + case bottom - //MARK: - Properties + } - /// The current visible drop down. There can be only one visible drop down at a time. - public static weak var VisibleDropDown: DropDown? + //MARK: - Properties - //MARK: UI - fileprivate let dismissableView = UIView() - fileprivate let tableViewContainer = UIView() - fileprivate let tableView = UITableView() - fileprivate var templateCell: DropDownCell! + /// The current visible drop down. There can be only one visible drop down at a time. + public static weak var VisibleDropDown: DropDown? + + private var shortListEnabled: Bool = false + public var sortListMaxEntries: Int = -1 + + //MARK: UI + fileprivate let dismissableView = UIView() + fileprivate let tableViewContainer = UIView() + fileprivate let tableView = UITableView() + fileprivate var templateCell: DropDownCell! fileprivate lazy var arrowIndication: UIImageView = { UIGraphicsBeginImageContextWithOptions(CGSize(width: 20, height: 10), false, 0) let path = UIBezierPath() @@ -101,39 +105,39 @@ public final class DropDown: UIView { }() - /// The view to which the drop down will displayed onto. - public weak var anchorView: AnchorView? { - didSet { setNeedsUpdateConstraints() } - } + /// The view to which the drop down will displayed onto. + public weak var anchorView: AnchorView? { + didSet { setNeedsUpdateConstraints() } + } - /** - The possible directions where the drop down will be showed. + /** + The possible directions where the drop down will be showed. - See `Direction` enum for more info. - */ - public var direction = Direction.any + See `Direction` enum for more info. + */ + public var direction = Direction.any - /** - The offset point relative to `anchorView` when the drop down is shown above the anchor view. + /** + The offset point relative to `anchorView` when the drop down is shown above the anchor view. - By default, the drop down is showed onto the `anchorView` with the top - left corner for its origin, so an offset equal to (0, 0). - You can change here the default drop down origin. - */ - public var topOffset: CGPoint = .zero { - didSet { setNeedsUpdateConstraints() } - } + By default, the drop down is showed onto the `anchorView` with the top + left corner for its origin, so an offset equal to (0, 0). + You can change here the default drop down origin. + */ + public var topOffset: CGPoint = .zero { + didSet { setNeedsUpdateConstraints() } + } - /** - The offset point relative to `anchorView` when the drop down is shown below the anchor view. + /** + The offset point relative to `anchorView` when the drop down is shown below the anchor view. - By default, the drop down is showed onto the `anchorView` with the top - left corner for its origin, so an offset equal to (0, 0). - You can change here the default drop down origin. - */ - public var bottomOffset: CGPoint = .zero { - didSet { setNeedsUpdateConstraints() } - } + By default, the drop down is showed onto the `anchorView` with the top + left corner for its origin, so an offset equal to (0, 0). + You can change here the default drop down origin. + */ + public var bottomOffset: CGPoint = .zero { + didSet { setNeedsUpdateConstraints() } + } /** The offset from the bottom of the window when the drop down is shown below the anchor view. @@ -143,194 +147,194 @@ public final class DropDown: UIView { didSet { setNeedsUpdateConstraints() } } - /** - The width of the drop down. - - Defaults to `anchorView.bounds.width - offset.x`. - */ - public var width: CGFloat? { - didSet { setNeedsUpdateConstraints() } - } - - /** - arrowIndication.x - - arrowIndication will be add to tableViewContainer when configured - */ - public var arrowIndicationX: CGFloat? { - didSet { - if let arrowIndicationX = arrowIndicationX { - tableViewContainer.addSubview(arrowIndication) - arrowIndication.tintColor = tableViewBackgroundColor - arrowIndication.frame.origin.x = arrowIndicationX - } else { - arrowIndication.removeFromSuperview() - } - } - } - - //MARK: Constraints - fileprivate var heightConstraint: NSLayoutConstraint! - fileprivate var widthConstraint: NSLayoutConstraint! - fileprivate var xConstraint: NSLayoutConstraint! - fileprivate var yConstraint: NSLayoutConstraint! - - //MARK: Appearance - @objc public dynamic var cellHeight = DPDConstant.UI.RowHeight { - willSet { tableView.rowHeight = newValue } - didSet { reloadAllComponents() } - } - - @objc fileprivate dynamic var tableViewBackgroundColor = DPDConstant.UI.BackgroundColor { - willSet { + /** + The width of the drop down. + + Defaults to `anchorView.bounds.width - offset.x`. + */ + public var width: CGFloat? { + didSet { setNeedsUpdateConstraints() } + } + + /** + arrowIndication.x + + arrowIndication will be add to tableViewContainer when configured + */ + public var arrowIndicationX: CGFloat? { + didSet { + if let arrowIndicationX = arrowIndicationX { + tableViewContainer.addSubview(arrowIndication) + arrowIndication.tintColor = tableViewBackgroundColor + arrowIndication.frame.origin.x = arrowIndicationX + } else { + arrowIndication.removeFromSuperview() + } + } + } + + //MARK: Constraints + fileprivate var heightConstraint: NSLayoutConstraint! + fileprivate var widthConstraint: NSLayoutConstraint! + fileprivate var xConstraint: NSLayoutConstraint! + fileprivate var yConstraint: NSLayoutConstraint! + + //MARK: Appearance + @objc public dynamic var cellHeight = DPDConstant.UI.RowHeight { + willSet { tableView.rowHeight = newValue } + didSet { reloadAllComponents() } + } + + @objc fileprivate dynamic var tableViewBackgroundColor = DPDConstant.UI.BackgroundColor { + willSet { tableView.backgroundColor = newValue if arrowIndicationX != nil { arrowIndication.tintColor = newValue } } - } - - public override var backgroundColor: UIColor? { - get { return tableViewBackgroundColor } - set { tableViewBackgroundColor = newValue! } - } - - /** - The color of the dimmed background (behind the drop down, covering the entire screen). - */ - public var dimmedBackgroundColor = UIColor.clear { - willSet { super.backgroundColor = newValue } - } - - /** - The background color of the selected cell in the drop down. - - Changing the background color automatically reloads the drop down. - */ - @objc public dynamic var selectionBackgroundColor = DPDConstant.UI.SelectionBackgroundColor - - /** - The separator color between cells. - - Changing the separator color automatically reloads the drop down. - */ - @objc public dynamic var separatorColor = DPDConstant.UI.SeparatorColor { - willSet { tableView.separatorColor = newValue } - didSet { reloadAllComponents() } - } - - /** - The corner radius of DropDown. - - Changing the corner radius automatically reloads the drop down. - */ - @objc public dynamic var cornerRadius = DPDConstant.UI.CornerRadius { - willSet { - tableViewContainer.layer.cornerRadius = newValue - tableView.layer.cornerRadius = newValue - } - didSet { reloadAllComponents() } - } - - /** - Alias method for `cornerRadius` variable to avoid ambiguity. - */ - @objc public dynamic func setupCornerRadius(_ radius: CGFloat) { - tableViewContainer.layer.cornerRadius = radius - tableView.layer.cornerRadius = radius - reloadAllComponents() - } - - /** - The masked corners of DropDown. - - Changing the masked corners automatically reloads the drop down. - */ - @available(iOS 11.0, *) - @objc public dynamic func setupMaskedCorners(_ cornerMask: CACornerMask) { - tableViewContainer.layer.maskedCorners = cornerMask - tableView.layer.maskedCorners = cornerMask - reloadAllComponents() - } - - /** - The color of the shadow. - - Changing the shadow color automatically reloads the drop down. - */ - @objc public dynamic var shadowColor = DPDConstant.UI.Shadow.Color { - willSet { tableViewContainer.layer.shadowColor = newValue.cgColor } - didSet { reloadAllComponents() } - } - - /** - The offset of the shadow. - - Changing the shadow color automatically reloads the drop down. - */ - @objc public dynamic var shadowOffset = DPDConstant.UI.Shadow.Offset { - willSet { tableViewContainer.layer.shadowOffset = newValue } - didSet { reloadAllComponents() } - } - - /** - The opacity of the shadow. - - Changing the shadow opacity automatically reloads the drop down. - */ - @objc public dynamic var shadowOpacity = DPDConstant.UI.Shadow.Opacity { - willSet { tableViewContainer.layer.shadowOpacity = newValue } - didSet { reloadAllComponents() } - } - - /** - The radius of the shadow. - - Changing the shadow radius automatically reloads the drop down. - */ - @objc public dynamic var shadowRadius = DPDConstant.UI.Shadow.Radius { - willSet { tableViewContainer.layer.shadowRadius = newValue } - didSet { reloadAllComponents() } - } - - /** - The duration of the show/hide animation. - */ - @objc public dynamic var animationduration = DPDConstant.Animation.Duration - - /** - The option of the show animation. Global change. - */ - public static var animationEntranceOptions = DPDConstant.Animation.EntranceOptions - - /** - The option of the hide animation. Global change. - */ - public static var animationExitOptions = DPDConstant.Animation.ExitOptions - - /** - The option of the show animation. Only change the caller. To change all drop down's use the static var. - */ - public var animationEntranceOptions: UIView.AnimationOptions = DropDown.animationEntranceOptions - - /** - The option of the hide animation. Only change the caller. To change all drop down's use the static var. - */ - public var animationExitOptions: UIView.AnimationOptions = DropDown.animationExitOptions - - /** - The downScale transformation of the tableview when the DropDown is appearing - */ - public var downScaleTransform = DPDConstant.Animation.DownScaleTransform { - willSet { tableViewContainer.transform = newValue } - } - - /** - The color of the text for each cells of the drop down. - - Changing the text color automatically reloads the drop down. - */ - @objc public dynamic var textColor = DPDConstant.UI.TextColor { - didSet { reloadAllComponents() } - } + } + + public override var backgroundColor: UIColor? { + get { return tableViewBackgroundColor } + set { tableViewBackgroundColor = newValue! } + } + + /** + The color of the dimmed background (behind the drop down, covering the entire screen). + */ + public var dimmedBackgroundColor = UIColor.clear { + willSet { super.backgroundColor = newValue } + } + + /** + The background color of the selected cell in the drop down. + + Changing the background color automatically reloads the drop down. + */ + @objc public dynamic var selectionBackgroundColor = DPDConstant.UI.SelectionBackgroundColor + + /** + The separator color between cells. + + Changing the separator color automatically reloads the drop down. + */ + @objc public dynamic var separatorColor = DPDConstant.UI.SeparatorColor { + willSet { tableView.separatorColor = newValue } + didSet { reloadAllComponents() } + } + + /** + The corner radius of DropDown. + + Changing the corner radius automatically reloads the drop down. + */ + @objc public dynamic var cornerRadius = DPDConstant.UI.CornerRadius { + willSet { + tableViewContainer.layer.cornerRadius = newValue + tableView.layer.cornerRadius = newValue + } + didSet { reloadAllComponents() } + } + + /** + Alias method for `cornerRadius` variable to avoid ambiguity. + */ + @objc public dynamic func setupCornerRadius(_ radius: CGFloat) { + tableViewContainer.layer.cornerRadius = radius + tableView.layer.cornerRadius = radius + reloadAllComponents() + } + + /** + The masked corners of DropDown. + + Changing the masked corners automatically reloads the drop down. + */ + @available(iOS 11.0, *) + @objc public dynamic func setupMaskedCorners(_ cornerMask: CACornerMask) { + tableViewContainer.layer.maskedCorners = cornerMask + tableView.layer.maskedCorners = cornerMask + reloadAllComponents() + } + + /** + The color of the shadow. + + Changing the shadow color automatically reloads the drop down. + */ + @objc public dynamic var shadowColor = DPDConstant.UI.Shadow.Color { + willSet { tableViewContainer.layer.shadowColor = newValue.cgColor } + didSet { reloadAllComponents() } + } + + /** + The offset of the shadow. + + Changing the shadow color automatically reloads the drop down. + */ + @objc public dynamic var shadowOffset = DPDConstant.UI.Shadow.Offset { + willSet { tableViewContainer.layer.shadowOffset = newValue } + didSet { reloadAllComponents() } + } + + /** + The opacity of the shadow. + + Changing the shadow opacity automatically reloads the drop down. + */ + @objc public dynamic var shadowOpacity = DPDConstant.UI.Shadow.Opacity { + willSet { tableViewContainer.layer.shadowOpacity = newValue } + didSet { reloadAllComponents() } + } + + /** + The radius of the shadow. + + Changing the shadow radius automatically reloads the drop down. + */ + @objc public dynamic var shadowRadius = DPDConstant.UI.Shadow.Radius { + willSet { tableViewContainer.layer.shadowRadius = newValue } + didSet { reloadAllComponents() } + } + + /** + The duration of the show/hide animation. + */ + @objc public dynamic var animationduration = DPDConstant.Animation.Duration + + /** + The option of the show animation. Global change. + */ + public static var animationEntranceOptions = DPDConstant.Animation.EntranceOptions + + /** + The option of the hide animation. Global change. + */ + public static var animationExitOptions = DPDConstant.Animation.ExitOptions + + /** + The option of the show animation. Only change the caller. To change all drop down's use the static var. + */ + public var animationEntranceOptions: UIView.AnimationOptions = DropDown.animationEntranceOptions + + /** + The option of the hide animation. Only change the caller. To change all drop down's use the static var. + */ + public var animationExitOptions: UIView.AnimationOptions = DropDown.animationExitOptions + + /** + The downScale transformation of the tableview when the DropDown is appearing + */ + public var downScaleTransform = DPDConstant.Animation.DownScaleTransform { + willSet { tableViewContainer.transform = newValue } + } + + /** + The color of the text for each cells of the drop down. + + Changing the text color automatically reloads the drop down. + */ + @objc public dynamic var textColor = DPDConstant.UI.TextColor { + didSet { reloadAllComponents() } + } /** The color of the text for selected cells of the drop down. @@ -341,66 +345,81 @@ public final class DropDown: UIView { didSet { reloadAllComponents() } } - /** - The font of the text for each cells of the drop down. - - Changing the text font automatically reloads the drop down. - */ - @objc public dynamic var textFont = DPDConstant.UI.TextFont { - didSet { reloadAllComponents() } - } + /** + The font of the text for each cells of the drop down. + + Changing the text font automatically reloads the drop down. + */ + @objc public dynamic var textFont = DPDConstant.UI.TextFont { + didSet { reloadAllComponents() } + } /** The NIB to use for DropDownCells Changing the cell nib automatically reloads the drop down. */ - public var cellNib = UINib(nibName: "DropDownCell", bundle: Bundle(for: DropDownCell.self)) { - didSet { - tableView.register(cellNib, forCellReuseIdentifier: DPDConstant.ReusableIdentifier.DropDownCell) - templateCell = nil - reloadAllComponents() - } - } - - //MARK: Content - - /** - The data source for the drop down. - - Changing the data source automatically reloads the drop down. - */ - public var dataSource = [String]() { - didSet { + public var cellNib = UINib(nibName: "DropDownCell", bundle: Bundle(for: DropDownCell.self)) { + didSet { + tableView.register(cellNib, forCellReuseIdentifier: DPDConstant.ReusableIdentifier.DropDownCell) + templateCell = nil + reloadAllComponents() + } + } + + public var moreCellNib = UINib(nibName: "MoreDropDownCell", bundle: Bundle(for: MoreDropDownCell.self)) { + didSet { + tableView.register(moreCellNib, forCellReuseIdentifier: DPDConstant.ReusableIdentifier.MoreDropDownCell) + reloadAllComponents() + } + } + + //MARK: Content + + /** + The data source for the drop down. + + Changing the data source automatically reloads the drop down. + */ + public var dataSource = [String]() { + didSet { + + if sortListMaxEntries > 0, + dataSource.count > sortListMaxEntries { + shortListEnabled = true + } else { + shortListEnabled = false + } + deselectRows(at: selectedRowIndices) - reloadAllComponents() - } - } - - /** - The localization keys for the data source for the drop down. - - Changing this value automatically reloads the drop down. - This has uses for setting accibility identifiers on the drop down cells (same ones as the localization keys). - */ - public var localizationKeysDataSource = [String]() { - didSet { - dataSource = localizationKeysDataSource.map { NSLocalizedString($0, comment: "") } - } - } - - /// The indicies that have been selected - fileprivate var selectedRowIndices = Set() - - /** - The format for the cells' text. - - By default, the cell's text takes the plain `dataSource` value. - Changing `cellConfiguration` automatically reloads the drop down. - */ - public var cellConfiguration: ConfigurationClosure? { - didSet { reloadAllComponents() } - } + reloadAllComponents() + } + } + + /** + The localization keys for the data source for the drop down. + + Changing this value automatically reloads the drop down. + This has uses for setting accibility identifiers on the drop down cells (same ones as the localization keys). + */ + public var localizationKeysDataSource = [String]() { + didSet { + dataSource = localizationKeysDataSource.map { NSLocalizedString($0, comment: "") } + } + } + + /// The indicies that have been selected + fileprivate var selectedRowIndices = Set() + + /** + The format for the cells' text. + + By default, the cell's text takes the plain `dataSource` value. + Changing `cellConfiguration` automatically reloads the drop down. + */ + public var cellConfiguration: ConfigurationClosure? { + didSet { reloadAllComponents() } + } /** A advanced formatter for the cells. Allows customization when custom cells are used @@ -410,9 +429,13 @@ public final class DropDown: UIView { public var customCellConfiguration: CellConfigurationClosure? { didSet { reloadAllComponents() } } + + public var customMoreCellConfiguration: MoreCellConfigurationClosure? { + didSet { reloadAllComponents() } + } - /// The action to execute when the user selects a cell. - public var selectionAction: SelectionClosure? + /// The action to execute when the user selects a cell. + public var selectionAction: SelectionClosure? /** The action to execute when the user selects multiple cells. @@ -422,80 +445,80 @@ public final class DropDown: UIView { */ public var multiSelectionAction: MultiSelectionClosure? - /// The action to execute when the drop down will show. - public var willShowAction: Closure? - - /// The action to execute when the user cancels/hides the drop down. - public var cancelAction: Closure? - - /// The dismiss mode of the drop down. Default is `OnTap`. - public var dismissMode = DismissMode.onTap { - willSet { - if newValue == .onTap { - let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissableViewTapped)) - dismissableView.addGestureRecognizer(gestureRecognizer) - } else if let gestureRecognizer = dismissableView.gestureRecognizers?.first { - dismissableView.removeGestureRecognizer(gestureRecognizer) - } - } - } - - fileprivate var minHeight: CGFloat { - return tableView.rowHeight - } - - fileprivate var didSetupConstraints = false - - //MARK: - Init's - - deinit { - stopListeningToNotifications() - } - - /** - Creates a new instance of a drop down. - Don't forget to setup the `dataSource`, - the `anchorView` and the `selectionAction` - at least before calling `show()`. - */ - public convenience init() { - self.init(frame: .zero) - } - - /** - Creates a new instance of a drop down. - - - parameter anchorView: The view to which the drop down will displayed onto. - - parameter selectionAction: The action to execute when the user selects a cell. - - parameter dataSource: The data source for the drop down. - - parameter topOffset: The offset point relative to `anchorView` used when drop down is displayed on above the anchor view. - - parameter bottomOffset: The offset point relative to `anchorView` used when drop down is displayed on below the anchor view. - - parameter cellConfiguration: The format for the cells' text. - - parameter cancelAction: The action to execute when the user cancels/hides the drop down. - - - returns: A new instance of a drop down customized with the above parameters. - */ - public convenience init(anchorView: AnchorView, selectionAction: SelectionClosure? = nil, dataSource: [String] = [], topOffset: CGPoint? = nil, bottomOffset: CGPoint? = nil, cellConfiguration: ConfigurationClosure? = nil, cancelAction: Closure? = nil) { - self.init(frame: .zero) - - self.anchorView = anchorView - self.selectionAction = selectionAction - self.dataSource = dataSource - self.topOffset = topOffset ?? .zero - self.bottomOffset = bottomOffset ?? .zero - self.cellConfiguration = cellConfiguration - self.cancelAction = cancelAction - } - - override public init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } + /// The action to execute when the drop down will show. + public var willShowAction: Closure? + + /// The action to execute when the user cancels/hides the drop down. + public var cancelAction: Closure? + + /// The dismiss mode of the drop down. Default is `OnTap`. + public var dismissMode = DismissMode.onTap { + willSet { + if newValue == .onTap { + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissableViewTapped)) + dismissableView.addGestureRecognizer(gestureRecognizer) + } else if let gestureRecognizer = dismissableView.gestureRecognizers?.first { + dismissableView.removeGestureRecognizer(gestureRecognizer) + } + } + } + + fileprivate var minHeight: CGFloat { + return tableView.rowHeight + } + + fileprivate var didSetupConstraints = false + + //MARK: - Init's + + deinit { + stopListeningToNotifications() + } + + /** + Creates a new instance of a drop down. + Don't forget to setup the `dataSource`, + the `anchorView` and the `selectionAction` + at least before calling `show()`. + */ + public convenience init() { + self.init(frame: .zero) + } + + /** + Creates a new instance of a drop down. + + - parameter anchorView: The view to which the drop down will displayed onto. + - parameter selectionAction: The action to execute when the user selects a cell. + - parameter dataSource: The data source for the drop down. + - parameter topOffset: The offset point relative to `anchorView` used when drop down is displayed on above the anchor view. + - parameter bottomOffset: The offset point relative to `anchorView` used when drop down is displayed on below the anchor view. + - parameter cellConfiguration: The format for the cells' text. + - parameter cancelAction: The action to execute when the user cancels/hides the drop down. + + - returns: A new instance of a drop down customized with the above parameters. + */ + public convenience init(anchorView: AnchorView, selectionAction: SelectionClosure? = nil, dataSource: [String] = [], topOffset: CGPoint? = nil, bottomOffset: CGPoint? = nil, cellConfiguration: ConfigurationClosure? = nil, cancelAction: Closure? = nil) { + self.init(frame: .zero) + + self.anchorView = anchorView + self.selectionAction = selectionAction + self.dataSource = dataSource + self.topOffset = topOffset ?? .zero + self.bottomOffset = bottomOffset ?? .zero + self.cellConfiguration = cellConfiguration + self.cancelAction = cancelAction + } + + override public init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } } @@ -503,44 +526,45 @@ public final class DropDown: UIView { private extension DropDown { - func setup() { - tableView.register(cellNib, forCellReuseIdentifier: DPDConstant.ReusableIdentifier.DropDownCell) + func setup() { + tableView.register(cellNib, forCellReuseIdentifier: DPDConstant.ReusableIdentifier.DropDownCell) + tableView.register(moreCellNib, forCellReuseIdentifier: DPDConstant.ReusableIdentifier.MoreDropDownCell) - DispatchQueue.main.async { - //HACK: If not done in dispatch_async on main queue `setupUI` will have no effect - self.updateConstraintsIfNeeded() - self.setupUI() - } + DispatchQueue.main.async { + //HACK: If not done in dispatch_async on main queue `setupUI` will have no effect + self.updateConstraintsIfNeeded() + self.setupUI() + } - tableView.rowHeight = cellHeight - setHiddentState() - isHidden = true + tableView.rowHeight = cellHeight + setHiddentState() + isHidden = true - dismissMode = .onTap + dismissMode = .onTap - tableView.delegate = self - tableView.dataSource = self - - startListeningToKeyboard() + tableView.delegate = self + tableView.dataSource = self + + startListeningToKeyboard() - accessibilityIdentifier = "drop_down" - } + accessibilityIdentifier = "drop_down" + } - func setupUI() { - super.backgroundColor = dimmedBackgroundColor + func setupUI() { + super.backgroundColor = dimmedBackgroundColor - tableViewContainer.layer.masksToBounds = false - tableViewContainer.layer.cornerRadius = cornerRadius - tableViewContainer.layer.shadowColor = shadowColor.cgColor - tableViewContainer.layer.shadowOffset = shadowOffset - tableViewContainer.layer.shadowOpacity = shadowOpacity - tableViewContainer.layer.shadowRadius = shadowRadius + tableViewContainer.layer.masksToBounds = false + tableViewContainer.layer.cornerRadius = cornerRadius + tableViewContainer.layer.shadowColor = shadowColor.cgColor + tableViewContainer.layer.shadowOffset = shadowOffset + tableViewContainer.layer.shadowOpacity = shadowOpacity + tableViewContainer.layer.shadowRadius = shadowRadius - tableView.backgroundColor = tableViewBackgroundColor - tableView.separatorColor = separatorColor - tableView.layer.cornerRadius = cornerRadius - tableView.layer.masksToBounds = true - } + tableView.backgroundColor = tableViewBackgroundColor + tableView.separatorColor = separatorColor + tableView.layer.cornerRadius = cornerRadius + tableView.layer.masksToBounds = true + } } @@ -548,253 +572,258 @@ private extension DropDown { extension DropDown { - public override func updateConstraints() { - if !didSetupConstraints { - setupConstraints() - } - - didSetupConstraints = true - - let layout = computeLayout() - - if !layout.canBeDisplayed { - super.updateConstraints() - hide() + public override func updateConstraints() { + if !didSetupConstraints { + setupConstraints() + } + + didSetupConstraints = true + + let layout = computeLayout() + + if !layout.canBeDisplayed { + super.updateConstraints() + hide() + + return + } + + if UIView.appearance().semanticContentAttribute == .forceLeftToRight { + xConstraint.constant = layout.x + }else { + xConstraint.constant = (UIWindow.visibleWindow()?.frame.width ?? 0) - (layout.x + layout.width) + } + + yConstraint.constant = layout.y + widthConstraint.constant = layout.width + heightConstraint.constant = layout.visibleHeight + + tableView.isScrollEnabled = layout.offscreenHeight > 0 + + DispatchQueue.main.async { [weak self] in + self?.tableView.flashScrollIndicators() + } + + super.updateConstraints() + } + + fileprivate func setupConstraints() { + translatesAutoresizingMaskIntoConstraints = false + + // Dismissable view + addSubview(dismissableView) + dismissableView.translatesAutoresizingMaskIntoConstraints = false + + addUniversalConstraints(format: "|[dismissableView]|", views: ["dismissableView": dismissableView]) + + + // Table view container + addSubview(tableViewContainer) + tableViewContainer.translatesAutoresizingMaskIntoConstraints = false + + xConstraint = NSLayoutConstraint( + item: tableViewContainer, + attribute: .leading, + relatedBy: .equal, + toItem: self, + attribute: .leading, + multiplier: 1, + constant: 0) + addConstraint(xConstraint) + + yConstraint = NSLayoutConstraint( + item: tableViewContainer, + attribute: .top, + relatedBy: .equal, + toItem: self, + attribute: .top, + multiplier: 1, + constant: 0) + addConstraint(yConstraint) + + widthConstraint = NSLayoutConstraint( + item: tableViewContainer, + attribute: .width, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1, + constant: 0) + tableViewContainer.addConstraint(widthConstraint) + + heightConstraint = NSLayoutConstraint( + item: tableViewContainer, + attribute: .height, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1, + constant: 0) + tableViewContainer.addConstraint(heightConstraint) + + // Table view + tableViewContainer.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + + tableViewContainer.addUniversalConstraints(format: "|[tableView]|", views: ["tableView": tableView]) + } + + public override func layoutSubviews() { + super.layoutSubviews() + + // When orientation changes, layoutSubviews is called + // We update the constraint to update the position + setNeedsUpdateConstraints() + + let shadowPath = UIBezierPath(roundedRect: tableViewContainer.bounds, cornerRadius: cornerRadius) + tableViewContainer.layer.shadowPath = shadowPath.cgPath + } + + fileprivate func computeLayout() -> (x: CGFloat, y: CGFloat, width: CGFloat, offscreenHeight: CGFloat, visibleHeight: CGFloat, canBeDisplayed: Bool, Direction: Direction) { + var layout: ComputeLayoutTuple = (0, 0, 0, 0) + var direction = self.direction + + guard let window = UIWindow.visibleWindow() else { return (0, 0, 0, 0, 0, false, direction) } + + barButtonItemCondition: if let anchorView = anchorView as? UIBarButtonItem { + let isRightBarButtonItem = anchorView.plainView.frame.minX > window.frame.midX + + guard isRightBarButtonItem else { break barButtonItemCondition } + + let width = self.width ?? fittingWidth() + let anchorViewWidth = anchorView.plainView.frame.width + let x = -(width - anchorViewWidth) + + bottomOffset = CGPoint(x: x, y: 0) + } + + if anchorView == nil { + layout = computeLayoutBottomDisplay(window: window) + direction = .any + } else { + switch direction { + case .any: + layout = computeLayoutBottomDisplay(window: window) + direction = .bottom + + if layout.offscreenHeight > 0 { + let topLayout = computeLayoutForTopDisplay(window: window) + + if topLayout.offscreenHeight < layout.offscreenHeight { + layout = topLayout + direction = .top + } + } + case .bottom: + layout = computeLayoutBottomDisplay(window: window) + direction = .bottom + case .top: + layout = computeLayoutForTopDisplay(window: window) + direction = .top + } + } + + constraintWidthToFittingSizeIfNecessary(layout: &layout) + constraintWidthToBoundsIfNecessary(layout: &layout, in: window) + + let visibleHeight = tableHeight - layout.offscreenHeight + let canBeDisplayed = visibleHeight >= minHeight + + return (layout.x, layout.y, layout.width, layout.offscreenHeight, visibleHeight, canBeDisplayed, direction) + } - return - } - - xConstraint.constant = layout.x - yConstraint.constant = layout.y - widthConstraint.constant = layout.width - heightConstraint.constant = layout.visibleHeight - - tableView.isScrollEnabled = layout.offscreenHeight > 0 - - DispatchQueue.main.async { [weak self] in - self?.tableView.flashScrollIndicators() - } - - super.updateConstraints() - } - - fileprivate func setupConstraints() { - translatesAutoresizingMaskIntoConstraints = false - - // Dismissable view - addSubview(dismissableView) - dismissableView.translatesAutoresizingMaskIntoConstraints = false - - addUniversalConstraints(format: "|[dismissableView]|", views: ["dismissableView": dismissableView]) - - - // Table view container - addSubview(tableViewContainer) - tableViewContainer.translatesAutoresizingMaskIntoConstraints = false - - xConstraint = NSLayoutConstraint( - item: tableViewContainer, - attribute: .leading, - relatedBy: .equal, - toItem: self, - attribute: .leading, - multiplier: 1, - constant: 0) - addConstraint(xConstraint) - - yConstraint = NSLayoutConstraint( - item: tableViewContainer, - attribute: .top, - relatedBy: .equal, - toItem: self, - attribute: .top, - multiplier: 1, - constant: 0) - addConstraint(yConstraint) - - widthConstraint = NSLayoutConstraint( - item: tableViewContainer, - attribute: .width, - relatedBy: .equal, - toItem: nil, - attribute: .notAnAttribute, - multiplier: 1, - constant: 0) - tableViewContainer.addConstraint(widthConstraint) - - heightConstraint = NSLayoutConstraint( - item: tableViewContainer, - attribute: .height, - relatedBy: .equal, - toItem: nil, - attribute: .notAnAttribute, - multiplier: 1, - constant: 0) - tableViewContainer.addConstraint(heightConstraint) - - // Table view - tableViewContainer.addSubview(tableView) - tableView.translatesAutoresizingMaskIntoConstraints = false - - tableViewContainer.addUniversalConstraints(format: "|[tableView]|", views: ["tableView": tableView]) - } - - public override func layoutSubviews() { - super.layoutSubviews() - - // When orientation changes, layoutSubviews is called - // We update the constraint to update the position - setNeedsUpdateConstraints() - - let shadowPath = UIBezierPath(roundedRect: tableViewContainer.bounds, cornerRadius: cornerRadius) - tableViewContainer.layer.shadowPath = shadowPath.cgPath - } - - fileprivate func computeLayout() -> (x: CGFloat, y: CGFloat, width: CGFloat, offscreenHeight: CGFloat, visibleHeight: CGFloat, canBeDisplayed: Bool, Direction: Direction) { - var layout: ComputeLayoutTuple = (0, 0, 0, 0) - var direction = self.direction - - guard let window = UIWindow.visibleWindow() else { return (0, 0, 0, 0, 0, false, direction) } - - barButtonItemCondition: if let anchorView = anchorView as? UIBarButtonItem { - let isRightBarButtonItem = anchorView.plainView.frame.minX > window.frame.midX - - guard isRightBarButtonItem else { break barButtonItemCondition } - - let width = self.width ?? fittingWidth() - let anchorViewWidth = anchorView.plainView.frame.width - let x = -(width - anchorViewWidth) - - bottomOffset = CGPoint(x: x, y: 0) - } - - if anchorView == nil { - layout = computeLayoutBottomDisplay(window: window) - direction = .any - } else { - switch direction { - case .any: - layout = computeLayoutBottomDisplay(window: window) - direction = .bottom - - if layout.offscreenHeight > 0 { - let topLayout = computeLayoutForTopDisplay(window: window) - - if topLayout.offscreenHeight < layout.offscreenHeight { - layout = topLayout - direction = .top - } - } - case .bottom: - layout = computeLayoutBottomDisplay(window: window) - direction = .bottom - case .top: - layout = computeLayoutForTopDisplay(window: window) - direction = .top - } - } - - constraintWidthToFittingSizeIfNecessary(layout: &layout) - constraintWidthToBoundsIfNecessary(layout: &layout, in: window) - - let visibleHeight = tableHeight - layout.offscreenHeight - let canBeDisplayed = visibleHeight >= minHeight - - return (layout.x, layout.y, layout.width, layout.offscreenHeight, visibleHeight, canBeDisplayed, direction) - } - - fileprivate func computeLayoutBottomDisplay(window: UIWindow) -> ComputeLayoutTuple { - var offscreenHeight: CGFloat = 0 - - let width = self.width ?? (anchorView?.plainView.bounds.width ?? fittingWidth()) - bottomOffset.x - - let anchorViewX = anchorView?.plainView.windowFrame?.minX ?? window.frame.midX - (width / 2) - let anchorViewY = anchorView?.plainView.windowFrame?.minY ?? window.frame.midY - (tableHeight / 2) - - let x = anchorViewX + bottomOffset.x - let y = anchorViewY + bottomOffset.y - - let maxY = y + tableHeight - let windowMaxY = window.bounds.maxY - DPDConstant.UI.HeightPadding - offsetFromWindowBottom - - let keyboardListener = KeyboardListener.sharedInstance - let keyboardMinY = keyboardListener.keyboardFrame.minY - DPDConstant.UI.HeightPadding - - if keyboardListener.isVisible && maxY > keyboardMinY { - offscreenHeight = abs(maxY - keyboardMinY) - } else if maxY > windowMaxY { - offscreenHeight = abs(maxY - windowMaxY) - } - - return (x, y, width, offscreenHeight) - } - - fileprivate func computeLayoutForTopDisplay(window: UIWindow) -> ComputeLayoutTuple { - var offscreenHeight: CGFloat = 0 - - let anchorViewX = anchorView?.plainView.windowFrame?.minX ?? 0 - let anchorViewMaxY = anchorView?.plainView.windowFrame?.maxY ?? 0 - - let x = anchorViewX + topOffset.x - var y = (anchorViewMaxY + topOffset.y) - tableHeight - - let windowY = window.bounds.minY + DPDConstant.UI.HeightPadding - - if y < windowY { - offscreenHeight = abs(y - windowY) - y = windowY - } - - let width = self.width ?? (anchorView?.plainView.bounds.width ?? fittingWidth()) - topOffset.x - - return (x, y, width, offscreenHeight) - } - - fileprivate func fittingWidth() -> CGFloat { - if templateCell == nil { - templateCell = (cellNib.instantiate(withOwner: nil, options: nil)[0] as! DropDownCell) - } - - var maxWidth: CGFloat = 0 - - for index in 0.. maxWidth { - maxWidth = width - } - } - - return maxWidth - } - - fileprivate func constraintWidthToBoundsIfNecessary(layout: inout ComputeLayoutTuple, in window: UIWindow) { - let windowMaxX = window.bounds.maxX - let maxX = layout.x + layout.width - - if maxX > windowMaxX { - let delta = maxX - windowMaxX - let newOrigin = layout.x - delta - - if newOrigin > 0 { - layout.x = newOrigin - } else { - layout.x = 0 - layout.width += newOrigin // newOrigin is negative, so this operation is a substraction - } - } - } - - fileprivate func constraintWidthToFittingSizeIfNecessary(layout: inout ComputeLayoutTuple) { - guard width == nil else { return } - - if layout.width < fittingWidth() { - layout.width = fittingWidth() - } - } - + fileprivate func computeLayoutBottomDisplay(window: UIWindow) -> ComputeLayoutTuple { + var offscreenHeight: CGFloat = 0 + + let width = self.width ?? (anchorView?.plainView.bounds.width ?? fittingWidth()) - bottomOffset.x + + let anchorViewX = anchorView?.plainView.windowFrame?.minX ?? window.frame.midX - (width / 2) + let anchorViewY = anchorView?.plainView.windowFrame?.minY ?? window.frame.midY - (tableHeight / 2) + + let x = anchorViewX + bottomOffset.x + let y = anchorViewY + bottomOffset.y + + let maxY = y + tableHeight + let windowMaxY = window.bounds.maxY - DPDConstant.UI.HeightPadding - offsetFromWindowBottom + + let keyboardListener = KeyboardListener.sharedInstance + let keyboardMinY = keyboardListener.keyboardFrame.minY - DPDConstant.UI.HeightPadding + + if keyboardListener.isVisible && maxY > keyboardMinY { + offscreenHeight = abs(maxY - keyboardMinY) + } else if maxY > windowMaxY { + offscreenHeight = abs(maxY - windowMaxY) + } + + return (x, y, width, offscreenHeight) + } + + fileprivate func computeLayoutForTopDisplay(window: UIWindow) -> ComputeLayoutTuple { + var offscreenHeight: CGFloat = 0 + + let anchorViewX = anchorView?.plainView.windowFrame?.minX ?? 0 + let anchorViewMaxY = anchorView?.plainView.windowFrame?.maxY ?? 0 + + let x = anchorViewX + topOffset.x + var y = (anchorViewMaxY + topOffset.y) - tableHeight + + let windowY = window.bounds.minY + DPDConstant.UI.HeightPadding + + if y < windowY { + offscreenHeight = abs(y - windowY) + y = windowY + } + + let width = self.width ?? (anchorView?.plainView.bounds.width ?? fittingWidth()) - topOffset.x + + return (x, y, width, offscreenHeight) + } + + fileprivate func fittingWidth() -> CGFloat { + if templateCell == nil { + templateCell = (cellNib.instantiate(withOwner: nil, options: nil)[0] as! DropDownCell) + } + + var maxWidth: CGFloat = 0 + + for index in 0.. maxWidth { + maxWidth = width + } + } + + return maxWidth + } + + fileprivate func constraintWidthToBoundsIfNecessary(layout: inout ComputeLayoutTuple, in window: UIWindow) { + let windowMaxX = window.bounds.maxX + let maxX = layout.x + layout.width + + if maxX > windowMaxX { + let delta = maxX - windowMaxX + let newOrigin = layout.x - delta + + if newOrigin > 0 { + layout.x = newOrigin + } else { + layout.x = 0 + layout.width += newOrigin // newOrigin is negative, so this operation is a substraction + } + } + } + + fileprivate func constraintWidthToFittingSizeIfNecessary(layout: inout ComputeLayoutTuple) { + guard width == nil else { return } + + if layout.width < fittingWidth() { + layout.width = fittingWidth() + } + } + } //MARK: - Actions @@ -818,43 +847,43 @@ extension DropDown { return NSDictionary(dictionary: info) } - - /** - Shows the drop down if enough height. + + /** + Shows the drop down if enough height. - - returns: Wether it succeed and how much height is needed to display all cells at once. - */ - @discardableResult + - returns: Wether it succeed and how much height is needed to display all cells at once. + */ + @discardableResult public func show(onTopOf window: UIWindow? = nil, beforeTransform transform: CGAffineTransform? = nil, anchorPoint: CGPoint? = nil) -> (canBeDisplayed: Bool, offscreenHeight: CGFloat?) { - if self == DropDown.VisibleDropDown && DropDown.VisibleDropDown?.isHidden == false { // added condition - DropDown.VisibleDropDown?.isHidden == false -> to resolve forever hiding dropdown issue when continuous taping on button - Kartik Patel - 2016-12-29 - return (true, 0) - } - - if let visibleDropDown = DropDown.VisibleDropDown { - visibleDropDown.cancel() - } + if self == DropDown.VisibleDropDown && DropDown.VisibleDropDown?.isHidden == false { // added condition - DropDown.VisibleDropDown?.isHidden == false -> to resolve forever hiding dropdown issue when continuous taping on button - Kartik Patel - 2016-12-29 + return (true, 0) + } - willShowAction?() + if let visibleDropDown = DropDown.VisibleDropDown { + visibleDropDown.cancel() + } + + willShowAction?() - DropDown.VisibleDropDown = self + DropDown.VisibleDropDown = self - setNeedsUpdateConstraints() + setNeedsUpdateConstraints() - let visibleWindow = window != nil ? window : UIWindow.visibleWindow() - visibleWindow?.addSubview(self) - visibleWindow?.bringSubviewToFront(self) + let visibleWindow = window != nil ? window : UIWindow.visibleWindow() + visibleWindow?.addSubview(self) + visibleWindow?.bringSubviewToFront(self) - self.translatesAutoresizingMaskIntoConstraints = false - visibleWindow?.addUniversalConstraints(format: "|[dropDown]|", views: ["dropDown": self]) + self.translatesAutoresizingMaskIntoConstraints = false + visibleWindow?.addUniversalConstraints(format: "|[dropDown]|", views: ["dropDown": self]) - let layout = computeLayout() + let layout = computeLayout() - if !layout.canBeDisplayed { - hide() - return (layout.canBeDisplayed, layout.offscreenHeight) - } + if !layout.canBeDisplayed { + hide() + return (layout.canBeDisplayed, layout.offscreenHeight) + } - isHidden = false + isHidden = false if anchorPoint != nil { tableViewContainer.layer.anchorPoint = anchorPoint! @@ -866,80 +895,80 @@ extension DropDown { tableViewContainer.transform = downScaleTransform } - layoutIfNeeded() - - UIView.animate( - withDuration: animationduration, - delay: 0, - options: animationEntranceOptions, - animations: { [weak self] in - self?.setShowedState() - }, - completion: nil) - - accessibilityViewIsModal = true - UIAccessibility.post(notification: .screenChanged, argument: self) - - //deselectRows(at: selectedRowIndices) - selectRows(at: selectedRowIndices) - - return (layout.canBeDisplayed, layout.offscreenHeight) - } - - public override func accessibilityPerformEscape() -> Bool { - switch dismissMode { - case .automatic, .onTap: - cancel() - return true - case .manual: - return false - } - } - - /// Hides the drop down. - public func hide() { - if self == DropDown.VisibleDropDown { - /* - If one drop down is showed and another one is not - but we call `hide()` on the hidden one: - we don't want it to set the `VisibleDropDown` to nil. - */ - DropDown.VisibleDropDown = nil - } - - if isHidden { - return - } - - UIView.animate( - withDuration: animationduration, - delay: 0, - options: animationExitOptions, - animations: { [weak self] in - self?.setHiddentState() - }, - completion: { [weak self] finished in - guard let `self` = self else { return } - - self.isHidden = true - self.removeFromSuperview() - UIAccessibility.post(notification: .screenChanged, argument: nil) - }) - } - - fileprivate func cancel() { - hide() - cancelAction?() - } - - fileprivate func setHiddentState() { - alpha = 0 - } - - fileprivate func setShowedState() { - alpha = 1 - tableViewContainer.transform = CGAffineTransform.identity - } + layoutIfNeeded() + + UIView.animate( + withDuration: animationduration, + delay: 0, + options: animationEntranceOptions, + animations: { [weak self] in + self?.setShowedState() + }, + completion: nil) + + accessibilityViewIsModal = true + UIAccessibility.post(notification: .screenChanged, argument: self) + + //deselectRows(at: selectedRowIndices) + selectRows(at: selectedRowIndices) + + return (layout.canBeDisplayed, layout.offscreenHeight) + } + + public override func accessibilityPerformEscape() -> Bool { + switch dismissMode { + case .automatic, .onTap: + cancel() + return true + case .manual: + return false + } + } + + /// Hides the drop down. + public func hide() { + if self == DropDown.VisibleDropDown { + /* + If one drop down is showed and another one is not + but we call `hide()` on the hidden one: + we don't want it to set the `VisibleDropDown` to nil. + */ + DropDown.VisibleDropDown = nil + } + + if isHidden { + return + } + + UIView.animate( + withDuration: animationduration, + delay: 0, + options: animationExitOptions, + animations: { [weak self] in + self?.setHiddentState() + }, + completion: { [weak self] finished in + guard let `self` = self else { return } + + self.isHidden = true + self.removeFromSuperview() + UIAccessibility.post(notification: .screenChanged, argument: nil) + }) + } + + fileprivate func cancel() { + hide() + cancelAction?() + } + + fileprivate func setHiddentState() { + alpha = 0 + } + + fileprivate func setShowedState() { + alpha = 1 + tableViewContainer.transform = CGAffineTransform.identity + } } @@ -947,32 +976,32 @@ extension DropDown { extension DropDown { - /** - Reloads all the cells. - - It should not be necessary in most cases because each change to - `dataSource`, `textColor`, `textFont`, `selectionBackgroundColor` - and `cellConfiguration` implicitly calls `reloadAllComponents()`. - */ - public func reloadAllComponents() { - DispatchQueue.executeOnMainThread { - self.tableView.reloadData() - self.setNeedsUpdateConstraints() - } - } - - /// (Pre)selects a row at a certain index. - public func selectRow(at index: Index?, scrollPosition: UITableView.ScrollPosition = .none) { - if let index = index { + /** + Reloads all the cells. + + It should not be necessary in most cases because each change to + `dataSource`, `textColor`, `textFont`, `selectionBackgroundColor` + and `cellConfiguration` implicitly calls `reloadAllComponents()`. + */ + public func reloadAllComponents() { + DispatchQueue.executeOnMainThread { + self.tableView.reloadData() + self.setNeedsUpdateConstraints() + } + } + + /// (Pre)selects a row at a certain index. + public func selectRow(at index: Index?, scrollPosition: UITableView.ScrollPosition = .none) { + if let index = index { tableView.selectRow( at: IndexPath(row: index, section: 0), animated: true, scrollPosition: scrollPosition ) selectedRowIndices.insert(index) - } else { - deselectRows(at: selectedRowIndices) + } else { + deselectRows(at: selectedRowIndices) selectedRowIndices.removeAll() - } - } + } + } public func selectRows(at indices: Set?) { indices?.forEach { @@ -985,18 +1014,18 @@ extension DropDown { } } - public func deselectRow(at index: Index?) { - guard let index = index - , index >= 0 - else { return } + public func deselectRow(at index: Index?) { + guard let index = index + , index >= 0 + else { return } // remove from indices if let selectedRowIndex = selectedRowIndices.firstIndex(where: { $0 == index }) { selectedRowIndices.remove(at: selectedRowIndex) } - tableView.deselectRow(at: IndexPath(row: index, section: 0), animated: true) - } + tableView.deselectRow(at: IndexPath(row: index, section: 0), animated: true) + } // de-selects the rows at the indices provided public func deselectRows(at indices: Set?) { @@ -1005,25 +1034,30 @@ extension DropDown { } } - /// Returns the index of the selected row. - public var indexForSelectedRow: Index? { - return (tableView.indexPathForSelectedRow as NSIndexPath?)?.row - } + /// Returns the index of the selected row. + public var indexForSelectedRow: Index? { + return (tableView.indexPathForSelectedRow as NSIndexPath?)?.row + } - /// Returns the selected item. - public var selectedItem: String? { - guard let row = (tableView.indexPathForSelectedRow as NSIndexPath?)?.row else { return nil } + /// Returns the selected item. + public var selectedItem: String? { + guard let row = (tableView.indexPathForSelectedRow as NSIndexPath?)?.row else { return nil } - return dataSource[row] - } + return dataSource[row] + } - /// Returns the height needed to display all cells. - fileprivate var tableHeight: CGFloat { - return tableView.rowHeight * CGFloat(dataSource.count) - } + /// Returns the height needed to display all cells. + fileprivate var tableHeight: CGFloat { + + if shortListEnabled { + return tableView.rowHeight * CGFloat(sortListMaxEntries + 1) + }else { + return tableView.rowHeight * CGFloat(dataSource.count) + } + } //MARK: Objective-C methods for converting the Swift type Index - @objc public func selectRow(_ index: Int, scrollPosition: UITableView.ScrollPosition = .none) { + @objc public func selectRow(_ index: Int, scrollPosition: UITableView.ScrollPosition = .none) { self.selectRow(at:Index(index), scrollPosition: scrollPosition) } @@ -1044,46 +1078,91 @@ extension DropDown { extension DropDown: UITableViewDataSource, UITableViewDelegate { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return dataSource.count - } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: DPDConstant.ReusableIdentifier.DropDownCell, for: indexPath) as! DropDownCell - let index = (indexPath as NSIndexPath).row - - configureCell(cell, at: index) - - return cell - } - - fileprivate func configureCell(_ cell: DropDownCell, at index: Int) { - if index >= 0 && index < localizationKeysDataSource.count { - cell.accessibilityIdentifier = localizationKeysDataSource[index] - } - - cell.optionLabel.textColor = textColor - cell.optionLabel.font = textFont - cell.selectedBackgroundColor = selectionBackgroundColor + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + + if shortListEnabled, + sortListMaxEntries > 0, + dataSource.count > sortListMaxEntries { + return sortListMaxEntries + 1 + } else { + return dataSource.count + } + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let index = (indexPath as NSIndexPath).row + + if shortListEnabled && index >= sortListMaxEntries { + + let cell = tableView.dequeueReusableCell(withIdentifier: DPDConstant.ReusableIdentifier.MoreDropDownCell, for: indexPath) as! MoreDropDownCell + + configureCell(cell, at: index) + + return cell + } else { + + let cell = tableView.dequeueReusableCell(withIdentifier: DPDConstant.ReusableIdentifier.DropDownCell, for: indexPath) as! DropDownCell + + configureCell(cell, at: index) + + return cell + } + } + + fileprivate func configureCell(_ cell: MoreDropDownCell, at index: Int) { + + if index >= 0 && index < localizationKeysDataSource.count { + cell.accessibilityIdentifier = localizationKeysDataSource[index] + } + + cell.optionLabel.textColor = textColor + cell.optionLabel.font = textFont + cell.selectedBackgroundColor = selectionBackgroundColor + cell.highlightTextColor = selectedTextColor + cell.normalTextColor = textColor + + if let cellConfiguration = cellConfiguration { + cell.optionLabel.text = cellConfiguration(index, "dropdown_more") + } else { + cell.optionLabel.text = "dropdown_more" + } + + customMoreCellConfiguration?(cell) + } + + fileprivate func configureCell(_ cell: DropDownCell, at index: Int) { + if index >= 0 && index < localizationKeysDataSource.count { + cell.accessibilityIdentifier = localizationKeysDataSource[index] + } + + cell.optionLabel.textColor = textColor + cell.optionLabel.font = textFont + cell.selectedBackgroundColor = selectionBackgroundColor cell.highlightTextColor = selectedTextColor cell.normalTextColor = textColor - - if let cellConfiguration = cellConfiguration { - cell.optionLabel.text = cellConfiguration(index, dataSource[index]) - } else { - cell.optionLabel.text = dataSource[index] - } - - customCellConfiguration?(index, dataSource[index], cell) - } - - public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + + if let cellConfiguration = cellConfiguration { + cell.optionLabel.text = cellConfiguration(index, dataSource[index]) + } else { + cell.optionLabel.text = dataSource[index] + } + + customCellConfiguration?(index, dataSource[index], cell) + } + + public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.isSelected = selectedRowIndices.first{ $0 == (indexPath as NSIndexPath).row } != nil - } + } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let selectedRowIndex = (indexPath as NSIndexPath).row + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selectedRowIndex = (indexPath as NSIndexPath).row + if shortListEnabled, selectedRowIndex == sortListMaxEntries { + shortListEnabled = false + reloadAllComponents() + return + } // are we in multi-selection mode? if let multiSelectionCallback = multiSelectionAction { @@ -1091,7 +1170,7 @@ extension DropDown: UITableViewDataSource, UITableViewDelegate { if selectedRowIndices.first(where: { $0 == selectedRowIndex}) != nil { deselectRow(at: selectedRowIndex) - let selectedRowIndicesArray = Array(selectedRowIndices) + let selectedRowIndicesArray = Array(selectedRowIndices) let selectedRows = selectedRowIndicesArray.map { dataSource[$0] } multiSelectionCallback(selectedRowIndicesArray, selectedRows) return @@ -1099,8 +1178,8 @@ extension DropDown: UITableViewDataSource, UITableViewDelegate { else { selectedRowIndices.insert(selectedRowIndex) - let selectedRowIndicesArray = Array(selectedRowIndices) - let selectedRows = selectedRowIndicesArray.map { dataSource[$0] } + let selectedRowIndicesArray = Array(selectedRowIndices) + let selectedRows = selectedRowIndicesArray.map { dataSource[$0] } selectionAction?(selectedRowIndex, dataSource[selectedRowIndex]) multiSelectionCallback(selectedRowIndicesArray, selectedRows) @@ -1121,7 +1200,7 @@ extension DropDown: UITableViewDataSource, UITableViewDelegate { hide() - } + } } @@ -1129,21 +1208,21 @@ extension DropDown: UITableViewDataSource, UITableViewDelegate { extension DropDown { - public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let view = super.hitTest(point, with: event) + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) - if dismissMode == .automatic && view === dismissableView { - cancel() - return nil - } else { - return view - } - } + if dismissMode == .automatic && view === dismissableView { + cancel() + return nil + } else { + return view + } + } - @objc - fileprivate func dismissableViewTapped() { - cancel() - } + @objc + fileprivate func dismissableViewTapped() { + cancel() + } } @@ -1151,46 +1230,46 @@ extension DropDown { extension DropDown { - /** - Starts listening to keyboard events. - Allows the drop down to display correctly when keyboard is showed. - */ - @objc public static func startListeningToKeyboard() { - KeyboardListener.sharedInstance.startListeningToKeyboard() - } - - fileprivate func startListeningToKeyboard() { - KeyboardListener.sharedInstance.startListeningToKeyboard() - - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardUpdate), - name: UIResponder.keyboardWillShowNotification, - object: nil) - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardUpdate), - name: UIResponder.keyboardWillHideNotification, - object: nil) - } - - fileprivate func stopListeningToNotifications() { - NotificationCenter.default.removeObserver(self) - } - - @objc - fileprivate func keyboardUpdate() { - self.setNeedsUpdateConstraints() - } + /** + Starts listening to keyboard events. + Allows the drop down to display correctly when keyboard is showed. + */ + @objc public static func startListeningToKeyboard() { + KeyboardListener.sharedInstance.startListeningToKeyboard() + } + + fileprivate func startListeningToKeyboard() { + KeyboardListener.sharedInstance.startListeningToKeyboard() + + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardUpdate), + name: UIResponder.keyboardWillShowNotification, + object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardUpdate), + name: UIResponder.keyboardWillHideNotification, + object: nil) + } + + fileprivate func stopListeningToNotifications() { + NotificationCenter.default.removeObserver(self) + } + + @objc + fileprivate func keyboardUpdate() { + self.setNeedsUpdateConstraints() + } } private extension DispatchQueue { - static func executeOnMainThread(_ closure: @escaping Closure) { - if Thread.isMainThread { - closure() - } else { - main.async(execute: closure) - } - } + static func executeOnMainThread(_ closure: @escaping Closure) { + if Thread.isMainThread { + closure() + } else { + main.async(execute: closure) + } + } } diff --git a/DropDown/src/DropDownCell.swift b/DropDown/src/DropDownCell.swift index 7a90efc..88a4621 100644 --- a/DropDown/src/DropDownCell.swift +++ b/DropDown/src/DropDownCell.swift @@ -13,9 +13,9 @@ open class DropDownCell: UITableViewCell { //UI @IBOutlet open weak var optionLabel: UILabel! - var selectedBackgroundColor: UIColor? - var highlightTextColor: UIColor? - var normalTextColor: UIColor? + open var selectedBackgroundColor: UIColor? + open var highlightTextColor: UIColor? + open var normalTextColor: UIColor? } diff --git a/DropDown/src/MoreDropDownCell.swift b/DropDown/src/MoreDropDownCell.swift new file mode 100644 index 0000000..31eca3e --- /dev/null +++ b/DropDown/src/MoreDropDownCell.swift @@ -0,0 +1,73 @@ +// +// MoreDropDownCell.swift +// DropDown +// +// Created by Ignazio Altomare on 22/05/2020. +// + +import UIKit + +open class MoreDropDownCell: UITableViewCell { + + //UI + @IBOutlet open weak var optionLabel: UILabel! + + open var selectedBackgroundColor: UIColor? + open var highlightTextColor: UIColor? + open var normalTextColor: UIColor? + +} + +//MARK: - UI + +extension MoreDropDownCell { + + override open func awakeFromNib() { + super.awakeFromNib() + + backgroundColor = .clear + } + + override open var isSelected: Bool { + willSet { + setSelected(newValue, animated: false) + } + } + + override open var isHighlighted: Bool { + willSet { + setSelected(newValue, animated: false) + } + } + + override open func setHighlighted(_ highlighted: Bool, animated: Bool) { + setSelected(highlighted, animated: animated) + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + let executeSelection: () -> Void = { [weak self] in + guard let `self` = self else { return } + + if let selectedBackgroundColor = self.selectedBackgroundColor { + if selected { + self.backgroundColor = selectedBackgroundColor + self.optionLabel.textColor = self.highlightTextColor + } else { + self.backgroundColor = .clear + self.optionLabel.textColor = self.normalTextColor + } + } + } + + if animated { + UIView.animate(withDuration: 0.3, animations: { + executeSelection() + }) + } else { + executeSelection() + } + + accessibilityTraits = selected ? .selected : .none + } + +}