【IQKeyboardManager】解决键盘遮挡的终极方案——1行代码,零配置

3 阅读6分钟

【IQKeyboardManager】解决键盘遮挡的终极方案——1行代码,零配置

iOS三方库精读 · 第 18 期


一、一句话介绍

IQKeyboardManager 是一个用于 iOS 的键盘遮挡解决方案,它通过 Method Swizzling 自动监听键盘事件,在键盘弹出时自动滚动页面让输入框始终可见,并提供 Previous / Next / Done 工具栏,整个过程无需在任何 ViewController 中写一行代码。

属性信息
⭐ GitHub Stars16.5k+
最新稳定版IQKeyboardManagerSwift 7.1.x (Swift)
LicenseMIT
支持平台iOS 13+
语言Objective-C(原版)+ Swift 封装

二、为什么选择它

原生痛点

在 iOS 中处理键盘遮挡,原生方案至少需要:

// ❌ 手动方案(基础版本,还未处理所有 edge case)
class FormVC: UIViewController {
    var keyboardHeight: CGFloat = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        // 1. 注册键盘通知
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification, object: nil)
        // 2. 点击收键盘
        let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        view.addGestureRecognizer(tap)
    }

    @objc func keyboardWillShow(_ notification: Notification) {
        // 3. 计算键盘高度
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        keyboardHeight = keyboardFrame.height
        // 4. 找到当前激活的 textField
        // 5. 计算 textField 是否被遮挡
        // 6. 调整 scrollView.contentInset 或 view.frame.origin.y
        // ... 还有横竖屏、iPad、外接键盘等 edge case
    }

    @objc func keyboardWillHide(_ notification: Notification) {
        // 7. 恢复布局
    }

    @objc func dismissKeyboard() { view.endEditing(true) }

    deinit { NotificationCenter.default.removeObserver(self) }
}

这段代码每个有输入框的 VC 都要写一遍,且还没处理 ScrollView / UITableView / 多个输入框 Tab 切换等场景。

IQKeyboardManager 只需:

// AppDelegate.swift
IQKeyboardManager.shared.isEnabled = true
// 全局生效,所有 VC 自动受管理

三、核心功能速览

基础层(新手必读)

Swift 集成
// SPM: https://github.com/hackiftekhar/IQKeyboardManager.git
// Product: IQKeyboardManagerSwift
# CocoaPods
pod 'IQKeyboardManagerSwift'  # Swift 版
pod 'IQKeyboardManager'       # OC 版(原版)
AppDelegate 配置(一次设置)
// Swift
import IQKeyboardManagerSwift

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    IQKeyboardManager.shared.isEnabled = true
    return true
}
// Objective-C
#import <IQKeyboardManager/IQKeyboardManager.h>

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)options {
    [IQKeyboardManager sharedManager].enable = YES;
    return YES;
}

进阶层(最佳实践)

常用配置项
let iq = IQKeyboardManager.shared
iq.isEnabled = true

// ---- 键盘距离 ----
iq.keyboardDistanceFromTextField = 15  // 键盘顶部与输入框底部的最小间距(默认10)

// ---- 收键盘 ----
iq.resignOnTouchOutside = true         // 点击空白处收键盘

// ---- ToolBar 配置 ----
iq.enableAutoToolbar = true            // 是否自动添加 Toolbar(默认true)
iq.toolbarConfiguration.manageBehaviourBy = .subviews  // 按视图层级排序
// 或
iq.toolbarConfiguration.manageBehaviourBy = .tag       // 按 tag 值排序(推荐表单)

iq.toolbarConfiguration.toolbarTintColor = .systemBlue  // Toolbar 按钮色
iq.toolbarConfiguration.placeholderConfiguration.showPlaceholder = true  // 显示 placeholder

// ---- 禁用某个 VC ----
iq.disabledDistanceHandlingClasses.append(ChatViewController.self)
iq.disabledToolbarClasses.append(ChatViewController.self)
iq.disabledTouchResignedClasses.append(ChatViewController.self)
UITextField 的 tag 排序

当表单有多个输入框时,设置 tag 值控制 Previous/Next 的跳转顺序:

nameField.tag     = 1
emailField.tag    = 2
phoneField.tag    = 3
passwordField.tag = 4
// IQKeyboardManager 会按 tag 升序排列 Toolbar 的 Previous/Next 跳转
iq.toolbarConfiguration.manageBehaviourBy = .tag
对特定 TextField 单独配置
// 某个 TextField 不希望 IQ 管理
searchBar.keyboardDistanceFromTextField = CGFloat.greatestFiniteMagnitude

// 某个 TextView 自定义 Done 按钮文字
bioTextView.keyboardToolbar.doneBarButton.title = "关闭键盘"
临时禁用(特定页面)
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    IQKeyboardManager.shared.isEnabled = false  // 当前页面禁用
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    IQKeyboardManager.shared.isEnabled = true   // 离开后恢复
}

深入层(源码视角)

Method Swizzling 的工作机制

IQKeyboardManager 通过 Swizzling 注入了以下系统方法:

  • UIViewController.viewWillAppear: / viewWillDisappear: → 追踪当前 VC
  • UITextField.becomeFirstResponder / resignFirstResponder → 追踪激活的 textField

当键盘弹出时:

  1. 通过 UIApplication 遍历 view 层级找到当前 firstResponder
  2. 将 firstResponder 的 frame 转换到 window 坐标系
  3. 与键盘 frame 对比,计算需要上移的距离
  4. 通过修改 UIScrollView.contentOffsetUIViewController.view.frame.origin.y 实现避让
为什么不是所有场景都适用

IQKeyboardManager 的自动处理对 99% 的标准表单场景完美适用,但在以下场景需要手动处理:

  • 聊天气泡列表:需要精确控制 UICollectionView 的 contentOffset,IQ 的计算会冲突
  • 全屏文本编辑器:inputAccessoryView 固定在键盘上方,不需要额外偏移
  • 自定义底部 Sheet:键盘弹出时 Sheet 整体上移,IQ 会双重处理

四、实战演示

场景:多字段注册表单(最典型应用)

import IQKeyboardManagerSwift

// AppDelegate(全局一次)
IQKeyboardManager.shared.isEnabled = true
IQKeyboardManager.shared.resignOnTouchOutside = true
IQKeyboardManager.shared.toolbarConfiguration.manageBehaviourBy = .tag

// RegisterFormVC(无需任何键盘处理代码!)
class RegisterFormVC: UIViewController {

    private let scrollView = UIScrollView()

    // 各字段自动得到 Previous/Next/Done 工具栏
    private let usernameField = { $0.tag = 1; return $0 }(UITextField())
    private let emailField    = { $0.tag = 2; return $0 }(UITextField())
    private let passwordField = { $0.tag = 3; $0.isSecureTextEntry = true; return $0 }(UITextField())
    private let phoneField    = { $0.tag = 4; $0.keyboardType = .phonePad; return $0 }(UITextField())

    // 完全无需写任何键盘避让代码!
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
        // 不需要注册任何 Notification
        // 不需要实现 UITextFieldDelegate 的 shouldReturn
        // 不需要添加 TapGestureRecognizer
    }
}

五、源码亮点

进阶层:单例管理器模式

IQKeyboardManager 是一个全局单例,通过统一的单例入口管理所有状态。这带来了极简的使用体验,但也意味着:

  • 配置是全局的,需要注意在不同 VC 间的状态隔离
  • 禁用某个 VC 时必须在 viewWillAppear/viewWillDisappear 配对处理

深入层:为什么不用 UIScrollView.keyboardDismissMode

苹果在 iOS 7 引入了 UIScrollView.keyboardDismissMode = .onDrag,但它只处理"收键盘",不处理"避让键盘遮挡"。IQKeyboardManager 解决的是更完整的键盘管理问题。

iOS 16+ 苹果引入了 .scrollDismissesKeyboard modifier(SwiftUI),但 UIKit 仍需手动处理避让。


六、踩坑记录

问题 1:键盘弹出后整个 view 偏移过大

  • 原因keyboardDistanceFromTextField 设置值过大,或 ScrollView 的 contentInset 被重复计算
  • 解决:调小 keyboardDistanceFromTextField;检查是否同时存在手动 notification 处理

问题 2:SwiftUI 的 TextField 不受 IQKeyboardManager 管理

  • 原因:IQKeyboardManager 针对 UIKit 的 UITextField/UITextView,SwiftUI 的 TextField 底层实现不同
  • 解决:SwiftUI 用 .scrollDismissesKeyboard(.interactively)(iOS 16+)或手动处理

问题 3:某个 VC 禁用后忘记恢复,导致其他页面也失效

  • 原因:在 viewWillAppear 禁用,但在 viewWillDisappear 忘记恢复
  • 解决:配对处理,或使用 disabledDistanceHandlingClasses 一次性配置

问题 4:Toolbar 上的 Done 按钮没有收起键盘

  • 原因:某些 VC 的输入框在自定义 container 里,IQ 无法识别层级
  • 解决:手动设置 textField.keyboardToolbar.doneBarButton.target = textField; doneBarButton.action = #selector(UIResponder.resignFirstResponder)

问题 5:iPad 上 popover 中的 TextField 行为异常

  • 原因:iPad 的键盘行为与 iPhone 不同(浮动键盘)
  • 解决:对 iPad popover 场景禁用 IQ,手动处理

问题 6:与 UISearchController 冲突

  • 原因:UISearchController 的 searchBar 不走标准的 UIViewController 生命周期
  • 解决:在 disabledDistanceHandlingClasses 中排除相关 VC,或通过 enabledDistanceHandlingClasses 精确指定受管理的 VC

七、延伸思考

iOS 16+ 的原生键盘管理

苹果在 iOS 16 引入了更多原生键盘支持:

// SwiftUI - iOS 16+
ScrollView {
    // ...
}
.scrollDismissesKeyboard(.interactively)  // 拖拽时收键盘

// UIKit - iOS 16+
textView.keyboardDismissMode = .interactiveWithAccessory

但对于 UIKit 的"避让遮挡"问题,iOS 17 之前仍无原生解决方案,IQKeyboardManager 仍然是最佳选择。

推荐使用场景

  • ✅ 包含多个输入框的表单页面(注册、设置、地址填写)
  • ✅ 需要快速开发,不想写键盘处理代码
  • ✅ OC 和 Swift 混合项目
  • ✅ 需要 Previous/Next 快速切换输入框

不推荐场景

  • ❌ 聊天界面(需要精确控制键盘交互)
  • ❌ 全屏文本编辑器(inputAccessoryView 方案更合适)
  • ❌ 纯 SwiftUI 项目(使用 SwiftUI 原生方案)

八、参考资源


九、本期互动

小作业

在一个包含 ScrollView 的注册表单页(6个字段:用户名/邮箱/密码/确认密码/手机/验证码)中,对比两种实现:① 手动监听 UIKeyboardWillShowNotification 处理避让,② 使用 IQKeyboardManager 1行配置。统计两种方式的代码行数,并在评论区分享你手动方案中遇到的 edge case。

思考题

IQKeyboardManager 用 Method Swizzling 注入系统方法来实现"0侵入"的键盘管理,这是一个权衡:使用方便但增加了黑盒复杂度。苹果在 iOS 16 引入了 UIKeyboardLayoutGuide 作为原生解决方案。你认为三方库的"方便性"和"原生 API 的可控性"之间的边界在哪里?何时该用三方库,何时该坚持原生?

读者征集

下一期我们将深入 GRDB.swift(Swift 中最好的 SQLite 封装)。你在项目中选择过 GRDB / SQLite.swift / Realm / CoreData 吗?做数据库选型时你最看重哪个维度(性能/API 简洁度/类型安全/OC 兼容性)?欢迎评论区分享。


📅 本系列每周五晚更新 ✅ 第14期:Moya · ✅ 第15期:SwiftyJSON · ✅ 第16期:R.swift · ✅ 第17期:PromiseKit · ➡️ 第18期:IQKeyboardManager · ○ 第19期:GRDB.swift