hackiftekhar/IQKeyboardManager

View on GitHub
IQKeyboardManagerSwift/ReturnKeyHandler/IQKeyboardReturnKeyHandler.swift

Summary

Maintainability
C
1 day
Test Coverage
//
//  IQKeyboardReturnKeyHandler.swift
//  https://github.com/hackiftekhar/IQKeyboardManager
//  Copyright (c) 2013-24 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

/**
Manages the return key to work like next/done in a view hierarchy.
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public final class IQKeyboardReturnKeyHandler: NSObject {

    // 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 model in textFieldInfoCache {
                if let view: UIView = model.textFieldView {
                    updateReturnKeyTypeOnTextField(view)
                }
            }
        }
    }

    // MARK: Initialization/De-initialization

    @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 model in textFieldInfoCache {
//            model.restore()
//        }

        textFieldInfoCache.removeAll()
    }

    // MARK: Private variables
    private var textFieldInfoCache: [IQTextFieldViewInfoModel] = []

    // MARK: Private Functions
    internal func textFieldViewCachedInfo(_ textField: UIView) -> IQTextFieldViewInfoModel? {

        for model in textFieldInfoCache {

            if let view: UIView = model.textFieldView {
                if view == textField {
                    return model
                }
            }
        }

        return nil
    }

    internal 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 allowedClasse in IQKeyboardManager.shared.toolbarPreviousNextAllowedClasses {

            superConsideredView = view.iq.superviewOf(type: allowedClasse)

            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: UIView = superConsideredView {     //   (Enhancement ID: #22)
            textFields = unwrappedTableView.iq.deepResponderViews()
        } else {  // Otherwise fetching all the siblings

            textFields = view.iq.responderSiblings()

            // Sorting textFields according to behavior
            switch IQKeyboardManager.shared.toolbarConfiguration.manageBehavior {
                // If needs to sort it by tag
            case .byTag:        textFields = textFields.sortedByTag()
                // If needs to sort it by Position
            case .byPosition:   textFields = textFields.sortedByPosition()
            default:    break
            }
        }

        if let lastView: UIView = textFields.last {

            if let textField: UITextField = 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: UITextView = 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 instance. Assign textFieldView delegate to self, change it's returnKeyType.

    @param view UITextField/UITextView object to register.
    */
    @objc public func addTextFieldView(_ view: UIView) {

        if let textField: UITextField = view as? UITextField {
            let model = IQTextFieldViewInfoModel(textField: textField)
            textFieldInfoCache.append(model)
            textField.delegate = self

        } else if let textView: UITextView = view as? UITextView {
            let model = IQTextFieldViewInfoModel(textView: textView)
            textFieldInfoCache.append(model)
            textView.delegate = self
        }
    }

    /**
    Should pass UITextField/UITextView instance. Restore it's textFieldView delegate and it's returnKeyType.
    
    @param view UITextField/UITextView object to unregister.
    */
    @objc public func removeTextFieldView(_ view: UIView) {

        if let model: IQTextFieldViewInfoModel = textFieldViewCachedInfo(view) {
            model.restore()

            if let index: Int = 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: [UIView] = view.iq.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: [UIView] = view.iq.deepResponderViews()

        for textField in textFields {

            removeTextFieldView(textField)
        }
    }

    @discardableResult
    internal 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 allowedClass in IQKeyboardManager.shared.toolbarPreviousNextAllowedClasses {

            superConsideredView = view.iq.superviewOf(type: allowedClass)

            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: UIView = superConsideredView {     //   (Enhancement ID: #22)
            textFields = unwrappedTableView.iq.deepResponderViews()
        } else {  // Otherwise fetching all the siblings

            textFields = view.iq.responderSiblings()

            // Sorting textFields according to behavior
            switch IQKeyboardManager.shared.toolbarConfiguration.manageBehavior {
                // If needs to sort it by tag
            case .byTag:        textFields = textFields.sortedByTag()
                // If needs to sort it by Position
            case .byPosition:   textFields = textFields.sortedByPosition()
            default:
                break
            }
        }

        //  Getting index of current textField.
        if let index: Int = textFields.firstIndex(of: view) {
            //  If it is not last textField. then it's next object becomeFirstResponder.
            if index < (textFields.count - 1) {

                let nextTextField: UIView = textFields[index+1]
                nextTextField.becomeFirstResponder()
                return false
            } else {

                view.resignFirstResponder()
                return true
            }
        } else {
            return true
        }
    }
}