// // IQKeyboardReturnKeyHandler.swift // https://github.com/hackiftekhar/IQKeyboardManager // Copyright (c) 2013-16 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 Foundation import UIKit private class IQTextFieldViewInfoModal: NSObject { fileprivate weak var textFieldDelegate: UITextFieldDelegate? fileprivate weak var textViewDelegate: UITextViewDelegate? fileprivate weak var textFieldView: UIView? fileprivate var originalReturnKeyType = UIReturnKeyType.default init(textFieldView: UIView?, textFieldDelegate: UITextFieldDelegate?, textViewDelegate: UITextViewDelegate?, originalReturnKeyType: UIReturnKeyType = .default) { self.textFieldView = textFieldView self.textFieldDelegate = textFieldDelegate self.textViewDelegate = textViewDelegate self.originalReturnKeyType = originalReturnKeyType } } /** Manages the return key to work like next/done in a view hierarchy. */ public class IQKeyboardReturnKeyHandler: NSObject, UITextFieldDelegate, UITextViewDelegate { ///--------------- // MARK: Settings ///--------------- /** Delegate of textField/textView. */ @objc public weak var delegate: (UITextFieldDelegate & UITextViewDelegate)? /** Set the last textfield return key type. Default is UIReturnKeyDefault. */ @objc public var lastTextFieldReturnKeyType: UIReturnKeyType = UIReturnKeyType.default { didSet { for modal in textFieldInfoCache { if let view = modal.textFieldView { updateReturnKeyTypeOnTextField(view) } } } } ///-------------------------------------- // MARK: Initialization/Deinitialization ///-------------------------------------- @objc public override init() { super.init() } /** Add all the textFields available in UIViewController's view. */ @objc public init(controller: UIViewController) { super.init() addResponderFromView(controller.view) } deinit { for modal in textFieldInfoCache { if let textField = modal.textFieldView as? UITextField { textField.returnKeyType = modal.originalReturnKeyType textField.delegate = modal.textFieldDelegate } else if let textView = modal.textFieldView as? UITextView { textView.returnKeyType = modal.originalReturnKeyType textView.delegate = modal.textViewDelegate } } textFieldInfoCache.removeAll() } ///------------------------ // MARK: Private variables ///------------------------ private var textFieldInfoCache = [IQTextFieldViewInfoModal]() ///------------------------ // MARK: Private Functions ///------------------------ private func textFieldViewCachedInfo(_ textField: UIView) -> IQTextFieldViewInfoModal? { for modal in textFieldInfoCache { if let view = modal.textFieldView { if view == textField { return modal } } } return nil } private func updateReturnKeyTypeOnTextField(_ view: UIView) { var superConsideredView: UIView? //If find any consider responderView in it's upper hierarchy then will get deepResponderView. (Bug ID: #347) for disabledClass in IQKeyboardManager.shared.toolbarPreviousNextAllowedClasses { superConsideredView = view.superviewOfClassType(disabledClass) if superConsideredView != nil { break } } var textFields = [UIView]() //If there is a tableView in view's hierarchy, then fetching all it's subview that responds. if let unwrappedTableView = superConsideredView { // (Enhancement ID: #22) textFields = unwrappedTableView.deepResponderViews() } else { //Otherwise fetching all the siblings textFields = view.responderSiblings() //Sorting textFields according to behaviour switch IQKeyboardManager.shared.toolbarManageBehaviour { //If needs to sort it by tag case .byTag: textFields = textFields.sortedArrayByTag() //If needs to sort it by Position case .byPosition: textFields = textFields.sortedArrayByPosition() default: break } } if let lastView = textFields.last { if let textField = view as? UITextField { //If it's the last textField in responder view, else next textField.returnKeyType = (view == lastView) ? lastTextFieldReturnKeyType: UIReturnKeyType.next } else if let textView = view as? UITextView { //If it's the last textField in responder view, else next textView.returnKeyType = (view == lastView) ? lastTextFieldReturnKeyType: UIReturnKeyType.next } } } ///---------------------------------------------- // MARK: Registering/Unregistering textFieldView ///---------------------------------------------- /** Should pass UITextField/UITextView intance. Assign textFieldView delegate to self, change it's returnKeyType. @param view UITextField/UITextView object to register. */ @objc public func addTextFieldView(_ view: UIView) { let modal = IQTextFieldViewInfoModal(textFieldView: view, textFieldDelegate: nil, textViewDelegate: nil) if let textField = view as? UITextField { modal.originalReturnKeyType = textField.returnKeyType modal.textFieldDelegate = textField.delegate textField.delegate = self } else if let textView = view as? UITextView { modal.originalReturnKeyType = textView.returnKeyType modal.textViewDelegate = textView.delegate textView.delegate = self } textFieldInfoCache.append(modal) } /** Should pass UITextField/UITextView intance. Restore it's textFieldView delegate and it's returnKeyType. @param view UITextField/UITextView object to unregister. */ @objc public func removeTextFieldView(_ view: UIView) { if let modal = textFieldViewCachedInfo(view) { if let textField = view as? UITextField { textField.returnKeyType = modal.originalReturnKeyType textField.delegate = modal.textFieldDelegate } else if let textView = view as? UITextView { textView.returnKeyType = modal.originalReturnKeyType textView.delegate = modal.textViewDelegate } if let index = textFieldInfoCache.firstIndex(where: { $0.textFieldView == view}) { textFieldInfoCache.remove(at: index) } } } /** Add all the UITextField/UITextView responderView's. @param view UIView object to register all it's responder subviews. */ @objc public func addResponderFromView(_ view: UIView) { let textFields = view.deepResponderViews() for textField in textFields { addTextFieldView(textField) } } /** Remove all the UITextField/UITextView responderView's. @param view UIView object to unregister all it's responder subviews. */ @objc public func removeResponderFromView(_ view: UIView) { let textFields = view.deepResponderViews() for textField in textFields { removeTextFieldView(textField) } } @discardableResult private func goToNextResponderOrResign(_ view: UIView) -> Bool { var superConsideredView: UIView? //If find any consider responderView in it's upper hierarchy then will get deepResponderView. (Bug ID: #347) for disabledClass in IQKeyboardManager.shared.toolbarPreviousNextAllowedClasses { superConsideredView = view.superviewOfClassType(disabledClass) if superConsideredView != nil { break } } var textFields = [UIView]() //If there is a tableView in view's hierarchy, then fetching all it's subview that responds. if let unwrappedTableView = superConsideredView { // (Enhancement ID: #22) textFields = unwrappedTableView.deepResponderViews() } else { //Otherwise fetching all the siblings textFields = view.responderSiblings() //Sorting textFields according to behaviour switch IQKeyboardManager.shared.toolbarManageBehaviour { //If needs to sort it by tag case .byTag: textFields = textFields.sortedArrayByTag() //If needs to sort it by Position case .byPosition: textFields = textFields.sortedArrayByPosition() default: break } } //Getting index of current textField. if let index = textFields.firstIndex(of: view) { //If it is not last textField. then it's next object becomeFirstResponder. if index < (textFields.count - 1) { let nextTextField = textFields[index+1] nextTextField.becomeFirstResponder() return false } else { view.resignFirstResponder() return true } } else { return true } } ///--------------------------------------- // MARK: UITextField/UITextView delegates ///--------------------------------------- @objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textField)?.textFieldDelegate { if unwrapDelegate.responds(to: #selector(UITextFieldDelegate.textFieldShouldBeginEditing(_:))) { return unwrapDelegate.textFieldShouldBeginEditing?(textField) == true } } } return true } @objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textField)?.textFieldDelegate { if unwrapDelegate.responds(to: #selector(UITextFieldDelegate.textFieldShouldEndEditing(_:))) { return unwrapDelegate.textFieldShouldEndEditing?(textField) == true } } } return true } @objc public func textFieldDidBeginEditing(_ textField: UITextField) { updateReturnKeyTypeOnTextField(textField) var aDelegate: UITextFieldDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textField) { aDelegate = modal.textFieldDelegate } } aDelegate?.textFieldDidBeginEditing?(textField) } @objc public func textFieldDidEndEditing(_ textField: UITextField) { var aDelegate: UITextFieldDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textField) { aDelegate = modal.textFieldDelegate } } aDelegate?.textFieldDidEndEditing?(textField) } #if swift(>=4.2) @available(iOS 10.0, *) @objc public func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) { var aDelegate: UITextFieldDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textField) { aDelegate = modal.textFieldDelegate } } aDelegate?.textFieldDidEndEditing?(textField, reason: reason) } #else @available(iOS 10.0, *) @objc public func textFieldDidEndEditing(_ textField: UITextField, reason: UITextFieldDidEndEditingReason) { var aDelegate: UITextFieldDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textField) { aDelegate = modal.textFieldDelegate } } aDelegate?.textFieldDidEndEditing?(textField, reason: reason) } #endif @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textField)?.textFieldDelegate { if unwrapDelegate.responds(to: #selector(UITextFieldDelegate.textField(_:shouldChangeCharactersIn:replacementString:))) { return unwrapDelegate.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == true } } } return true } @objc public func textFieldShouldClear(_ textField: UITextField) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textField)?.textFieldDelegate { if unwrapDelegate.responds(to: #selector(UITextFieldDelegate.textFieldShouldClear(_:))) { return unwrapDelegate.textFieldShouldClear?(textField) == true } } } return true } @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { var shouldReturn = true if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textField)?.textFieldDelegate { if unwrapDelegate.responds(to: #selector(UITextFieldDelegate.textFieldShouldReturn(_:))) { shouldReturn = unwrapDelegate.textFieldShouldReturn?(textField) == true } } } if shouldReturn == true { goToNextResponderOrResign(textField) return true } else { return goToNextResponderOrResign(textField) } } @objc public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textView)?.textViewDelegate { if unwrapDelegate.responds(to: #selector(UITextViewDelegate.textViewShouldBeginEditing(_:))) { return unwrapDelegate.textViewShouldBeginEditing?(textView) == true } } } return true } @objc public func textViewShouldEndEditing(_ textView: UITextView) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textView)?.textViewDelegate { if unwrapDelegate.responds(to: #selector(UITextViewDelegate.textViewShouldEndEditing(_:))) { return unwrapDelegate.textViewShouldEndEditing?(textView) == true } } } return true } @objc public func textViewDidBeginEditing(_ textView: UITextView) { updateReturnKeyTypeOnTextField(textView) var aDelegate: UITextViewDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textView) { aDelegate = modal.textViewDelegate } } aDelegate?.textViewDidBeginEditing?(textView) } @objc public func textViewDidEndEditing(_ textView: UITextView) { var aDelegate: UITextViewDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textView) { aDelegate = modal.textViewDelegate } } aDelegate?.textViewDidEndEditing?(textView) } @objc public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { var shouldReturn = true if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(textView)?.textViewDelegate { if unwrapDelegate.responds(to: #selector(UITextViewDelegate.textView(_:shouldChangeTextIn:replacementText:))) { shouldReturn = (unwrapDelegate.textView?(textView, shouldChangeTextIn: range, replacementText: text)) == true } } } if shouldReturn == true && text == "\n" { shouldReturn = goToNextResponderOrResign(textView) } return shouldReturn } @objc public func textViewDidChange(_ textView: UITextView) { var aDelegate: UITextViewDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textView) { aDelegate = modal.textViewDelegate } } aDelegate?.textViewDidChange?(textView) } @objc public func textViewDidChangeSelection(_ textView: UITextView) { var aDelegate: UITextViewDelegate? = delegate if aDelegate == nil { if let modal = textFieldViewCachedInfo(textView) { aDelegate = modal.textViewDelegate } } aDelegate?.textViewDidChangeSelection?(textView) } @available(iOS 10.0, *) @objc public func textView(_ aTextView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(aTextView)?.textViewDelegate { if unwrapDelegate.responds(to: #selector(textView as (UITextView, URL, NSRange, UITextItemInteraction) -> Bool)) { return unwrapDelegate.textView?(aTextView, shouldInteractWith: URL, in: characterRange, interaction: interaction) == true } } } return true } @available(iOS 10.0, *) @objc public func textView(_ aTextView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(aTextView)?.textViewDelegate { if unwrapDelegate.responds(to: #selector(textView as (UITextView, NSTextAttachment, NSRange, UITextItemInteraction) -> Bool)) { return unwrapDelegate.textView?(aTextView, shouldInteractWith: textAttachment, in: characterRange, interaction: interaction) == true } } } return true } @available(iOS, deprecated: 10.0) @objc public func textView(_ aTextView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(aTextView)?.textViewDelegate { if unwrapDelegate.responds(to: #selector(textView as (UITextView, URL, NSRange) -> Bool)) { return unwrapDelegate.textView?(aTextView, shouldInteractWith: URL, in: characterRange) == true } } } return true } @available(iOS, deprecated: 10.0) @objc public func textView(_ aTextView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool { if delegate == nil { if let unwrapDelegate = textFieldViewCachedInfo(aTextView)?.textViewDelegate { if unwrapDelegate.responds(to: #selector(textView as (UITextView, NSTextAttachment, NSRange) -> Bool)) { return unwrapDelegate.textView?(aTextView, shouldInteractWith: textAttachment, in: characterRange) == true } } } return true } }