---
# System prepended metadata

title: IQKwyboard 研究
tags: [Keyboard, Swift]

---

# IQKwyboard 研究

[Toc]

## 前言
IQKeyboard是一款iOS平台上的第三方鍵盤庫，提供了一些方便的功能來處理鍵盤的顯示和隱藏。它可以幫助開發者更輕鬆地管理鍵盤的彈出和收起，處理鍵盤遮擋輸入框的問題，以及自定義鍵盤的外觀和行為。IQKeyboard還提供了一些其他功能，如自動調整UIScrollView的contentInset來適應鍵盤的高度，以及自定義輸入框的placeholder顏色等。通過使用IQKeyboard，開發者可以更加方便地處理鍵盤相關的操作，提高用戶體驗。

## 官方實作架構

![Flow Diagram](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/IQKeyboardManagerFlowDiagram.jpg)

## 程式運行架構梳理

### 初始化
依照 Github 描述，使用此套件只需在 App 初始化時運行以下方法即可：
``` swift
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
    IQKeyboardManager.shared.enable = true
    return true
}
```

但令人好奇的是，這是如何實現的呢？

其實此時 IQKeyboardManager 實體進行了一系列參數初始化與事件註冊行為：

``` swift
@objc public var enable: Bool = false {

    didSet {
        // If not enable, enable it.
        if enable, !oldValue {
            // If keyboard is currently showing.
            if activeConfiguration.keyboardInfo.keyboardShowing {
                adjustPosition()
            } else {
                restorePosition()
            }
            showLog("Enabled")
        } else if !enable, oldValue {   // If not disable, disable it.
            restorePosition()
            showLog("Disabled")
        }
    }
}
```

依據此流程，正常情況下會先判斷第 7 行的鍵盤是否正在顯示的邏輯，依序逐一確認其屬性與初始化參數

這邊首先會碰觸到的物件是 `activeConfiguration`，型態是 `IQActiveConfiguration`

``` swift
@objc public final class IQKeyboardManager: NSObject {
    internal var activeConfiguration: IQActiveConfiguration = .init()
}
```
#### IQActiveConfiguration 持有參數與定位

**持有變數**
``` swift
// 註冊並監聽與 Keyboard 相關 Notification 通知
private let keyboardListener: IQKeyboardListener = IQKeyboardListener()
// 註冊並監聽與 Textfield 相關 Notification 通知
private let textFieldViewListener: IQTextFieldViewListener = IQTextFieldViewListener()
// 持有外部的監聽閉包（closure)，並於異動時逐一推送
private var changeObservers: [AnyHashable: ConfigurationCompletion] = [:]
// 持有當前 kayboard 狀態，分為 hide, show, change(show->show)
private var lastEvent: Event = .hide
// 持有並儲存當前互動中的 UIViewController 與相關數據
var rootControllerConfiguration: IQRootControllerConfiguration?
```

以上為 `IQActiveConfiguration` 所持有的數據，由此可知其定位為最底層的事件呼叫者，大部分時候其不會被替換且只有一個實體，由 IQKayboardManager 所持有。

**事件監聽**

看完其持有的實體，接著確認它是如何監聽系統的事件。

在 `IQActiveConfiguration` 初始化當下即對系統事件進行了監聽，這邊以 keyboard 事件為例：


#### IQKeyboardListener 事件監聽
`IQKeyboardListener` 監聽的鍵盤從顯示、變化到隱藏的所有系統通知，並在切換時向外推送

``` swift
private(set) var keyboardInfo: IQKeyboardInfo {
    didSet {
        if keyboardInfo != oldValue {
            sendEvent()
        }
    }
}

public init() {
    keyboardInfo = IQKeyboardInfo(notification: nil, name: .didHide)
    //  Registering for keyboard notification.
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(_:)),
                                           name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidShow(_:)),
                                           name: UIResponder.keyboardDidShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)),
                                           name: UIResponder.keyboardWillHideNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidHide(_:)),
                                           name: UIResponder.keyboardDidHideNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillChangeFrame(_:)),
                                           name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidChangeFrame(_:)),
                                           name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
}

typealias SizeCompletion = (_ name: IQKeyboardInfo.Name, _ size: CGSize) -> Void

func registerSizeChange(identifier: AnyHashable, changeHandler: @escaping SizeCompletion) {
    sizeObservers[identifier] = changeHandler
}

private func sendEvent() {

    let size: CGSize = keyboardInfo.frame.size

    for block in sizeObservers.values {
        block(keyboardInfo.name, size)
    }
}
```

其中上列的 `IQKeyboardInfo.Name` 參數如下：

``` swift
public struct IQKeyboardInfo: Equatable {
    nonisolated public static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.frame.equalTo(rhs.frame)
    }

    @objc public enum Name: Int {
        case willShow
        case didShow
        case willHide
        case didHide
        case willChangeFrame
        case didChangeFrame
    }
    ...
}
```

已經知道 `IQKeyboardListener` 負責監聽並且傳出所有事件，`IQActiveConfiguration` 就可以在初始化時對其進行監聽，其實作如下：

#### IQActiveConfiguration 事件註冊

``` swift
extension IQActiveConfiguration {
    // 向 IQKeyboardListener 註冊，監聽異動事件
    private func addKeyboardListener() {
        keyboardListener.registerSizeChange(identifier: "IQActiveConfiguration", changeHandler: { [self] name, _ in
         // 若 keyboard 顯示中，通知當前 rootVC 更新狀態
            if let info = textFieldViewInfo, keyboardInfo.keyboardShowing {
                if let rootControllerConfiguration = rootControllerConfiguration {
                    let beginIsPortrait: Bool = rootControllerConfiguration.beginOrientation.isPortrait
                    let currentIsPortrait: Bool = rootControllerConfiguration.currentOrientation.isPortrait
                    if beginIsPortrait != currentIsPortrait {
                        updateRootController(info: info)
                    }
                } else {
                    updateRootController(info: info)
                }
            }
            // 依據 lastEvent 狀態，將新的 lastEvent 狀態推送所有註冊事件的實體
            self.sendEvent()

            // 如果鍵盤完全隱藏，通知 rootVC 還原初始狀態
            if name == .didHide {
                updateRootController(info: nil)
            }
        })
    }
}
```

#### IQTextFieldViewListener 事件監聽

`IQTextFieldViewListener` 也是如法炮製，這邊進列出它所監聽的事件：
``` swift
public init() {
    //  Registering for keyboard notification.
    NotificationCenter.default.addObserver(self, selector: #selector(self.didBeginEditing(_:)),
                                           name: UITextField.textDidBeginEditingNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.didEndEditing(_:)),
                                           name: UITextField.textDidEndEditingNotification, object: nil)

    NotificationCenter.default.addObserver(self, selector: #selector(self.didBeginEditing(_:)),
                                           name: UITextView.textDidBeginEditingNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.didEndEditing(_:)),
                                           name: UITextView.textDidEndEditingNotification, object: nil)
}
```

#### IQKeyboardInfo frame 參數

最後回頭確認 `activeConfiguration.keyboardInfo.keyboardShowing` 這段程式中 `keyboardShowing` 的判斷依據與數據來源：

``` swift
public struct IQKeyboardInfo: Equatable {
    public var keyboardShowing: Bool {
        frame.height > 0
    }
    
    // 當收到 keyboard 事件時生成，解析通知物件並取得鍵盤大小
    public init(notification: Notification?, name: Name) {
        ...
        if let info: [AnyHashable: Any] = notification?.userInfo {
            ...
            //  Getting UIKeyboardSize.
            if var kbFrame: CGRect = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
                ...
                frame = kbFrame
            } else {
                frame = CGRect(x: 0, y: screenBounds.height, width: screenBounds.width, height: 0)
            }
        } else {
            ...
            frame = CGRect(x: 0, y: screenBounds.height, width: screenBounds.width, height: 0)
        }
        ...
    }
}
```

### 事件觸發順序

無論是 textField 開始編輯或 keyboard 顯示、隱藏事件觸發，最終都會觸發 `updateRootController(info: info)` 方法。


>其中 info 的型態為 `IQTextFieldViewInfo`，其持有 textField(抹除為 UIView)與事件類型（開始、結束編輯）。
>
>`IQTextFieldViewInfo` 閉需通過 `Notification` 進行初始化。

`updateRootController(info: info)` 做了以下幾件行為：

1. 嘗試通過 `IQTextFieldViewInfo` 中的 `UIView` 獲取其 `UIViewController`（依靠 Responder chain）
2. 如果獲取不到，且當前已持有編輯中的 `UIViewController`，通知變化並要求恢復狀態
3. 如果獲取成功，生成負責紀錄狀態的 `IQRootControllerConfiguration`
4. 如果 `window` 變動、手機方向變動時，替換 `IQRootControllerConfiguration`
5. 如果 `UIViewController` 實體沒變，通知其變化發生
6. 更新並持有 `IQRootControllerConfiguration`

這邊需關注的是，`IQRootControllerConfiguration`生成時紀錄的參數：
```swift
internal struct IQRootControllerConfiguration {
    init(rootController: UIViewController) {
        self.rootController = rootController
        
        // 原始 view origin, safeAreaInsets 供恢復之用
        beginOrigin = rootController.view.frame.origin
        beginSafeAreaInsets = rootController.view.safeAreaInsets

        // 取得當前 UIViewController 顯示方向
        let interfaceOrientation: UIInterfaceOrientation
        if let scene = rootController.view.window?.windowScene {
            interfaceOrientation = scene.interfaceOrientation
        } else {
            interfaceOrientation = .unknown
        }

        beginOrientation = interfaceOrientation
    }
}
```

至此所有閉要物件與事件都已初始化完畢，接下來問題便是：
>1. 什麼時機下需要調整`rootViewController.view.origin`？
>
>2. 要如何計算出要調整的 offset?

### 計算偏移量

詳見 `adjustPosition()`，to be continue.

---
Referentce: [Github](https://github.com/hackiftekhar/IQKeyboardManager)