返回

SwiftUI 键盘遮挡 TextField 处理策略:IQKeyboardManager 与自定义

IOS

SwiftUI 中键盘遮挡 TextField 的处理策略

在使用 SwiftUI 构建用户界面时,常常会遇到一个问题:键盘弹出后会遮挡住部分 TextField,导致用户无法看到正在输入的内容。 这个问题会严重影响用户体验,需找到有效的处理方式。 接下来分析此问题的几种常见解决方式,帮助开发者灵活选择。

一、 使用 Keyboard Avoidance

1. 问题原因

SwiftUI 中,当键盘弹出时,视图的布局并不会自动调整。 TextField 的位置是固定的,当键盘高度超过 TextField 底部时,就会造成遮挡。

2. 解决思路

基本思路是在键盘弹出时,动态调整视图的偏移量,使得当前获取焦点的 TextField 始终处于可见区域。

3. 第三方库 IQKeyboardManager

针对类似问题,IQKeyboardManager 提供了一种便利的解决方式。

  • 原理:此库会监听键盘通知,并自动调整 ScrollViewcontentInset 或整个 UIViewtransform

4. 如何集成和使用 IQKeyboardManager

操作步骤:

  1. 添加 IQKeyboardManager Swift 包依赖。 通过 Swift Package Manager 添加。

    # 拷贝代码在工程内通过 Swift Package Manager 添加即可:
    https://github.com/hackiftekhar/IQKeyboardManager.git
    

    需要注意包管理工具有时缓存问题会报错找不到,建议使用最新版 Xcode 重启 Xcode 尝试。

  2. AppDelegateSceneDelegate 中进行配置。

代码示例:

import IQKeyboardManagerSwift

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        IQKeyboardManager.shared.enable = true
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true // 开启点击外部收起键盘功能

        return true
    }
}
  1. 简单配置后,对于很多简单布局都能直接处理遮挡问题了,默认使用比较便捷安全。

二、自定义键盘通知处理

对于更复杂的布局,手动处理键盘通知提供更多灵活性。 这种方式控制精确度高,能满足绝大多数特定要求。

1. 原理说明

  • 通过监听 UIResponder.keyboardWillShowNotificationUIResponder.keyboardWillHideNotification 来获取键盘状态和高度信息。
  • 在键盘弹出时,根据当前 TextField 的位置和键盘高度,计算出需要调整的偏移量。
  • 将视图向上偏移以显示 TextField,通常可以通过修改 padding 或使用 offset 实现。
  • 在键盘收起时,恢复视图的偏移量。

2. 代码实现及步骤说明

操作步骤:

  1. 添加 GeometryReader 以获取 TextField 在全局坐标系中的位置。

  2. 添加两个状态变量:keyboardHeight 记录键盘高度,currentViewOffset 记录视图偏移量。

  3. 创建方法处理键盘的弹出和隐藏事件。通过 NotificationCenter 注册键盘通知。

  4. 使用 onReceive 监听键盘弹出通知,计算 TextField 的底部 Y 坐标与键盘顶部 Y 坐标的差值,据此调整视图偏移。

  5. 同样使用 onReceive 监听键盘隐藏通知,恢复视图偏移为 0。

  6. 利用 offset 修饰符控制整个视图的 Y 轴偏移。

代码示例:

import SwiftUI
import Combine

struct ContentView: View {
    @State private var textfieldText: String = ""
    @State private var keyboardHeight: CGFloat = 0
    @State private var currentViewOffset: CGFloat = 0
    @FocusState private var focusedField: Int?
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack {
                    TextField("TextField1", text: $textfieldText)
                        .focused($focusedField, equals: 1)
                    TextField("TextField2", text: $textfieldText)
                        .focused($focusedField, equals: 2)
                    TextField("TextField3", text: $textfieldText)
                        .focused($focusedField, equals: 3)
                    TextField("TextField4", text: $textfieldText)
                        .focused($focusedField, equals: 4)
                    TextField("TextField5", text: $textfieldText)
                        .focused($focusedField, equals: 5)
                    TextField("TextField6", text: $textfieldText)
                        .focused($focusedField, equals: 6)
                    TextField("TextField7", text: $textfieldText)
                        .focused($focusedField, equals: 7)
                }
                .padding()
                .offset(y: currentViewOffset)
                .animation(.spring(), value: currentViewOffset) // 添加动画效果
                .onReceive(Publishers.keyboardHeight) { height in
                    handleKeyboardNotification(with: height, geometry: geometry)
                }
            }
            .onTapGesture {
                 focusedField = nil // 收起键盘
             }
        }
    }

    private func handleKeyboardNotification(with height: CGFloat, geometry: GeometryProxy) {
        // 只有键盘高度变化时更新键盘高度
        if height != 0 {
            keyboardHeight = height
            adjustViewOffsetIfNeeded(geometry: geometry)
            return
        }
        
        // 键盘收起时,恢复视图位置
        currentViewOffset = 0
    }
    
    // 响应 TextField 聚焦事件
    private func adjustViewOffsetIfNeeded(geometry: GeometryProxy) {
        guard let focusedField = focusedField, keyboardHeight > 0 else { return }

        // 获取当前聚焦的 TextField 的 frame
        let textFieldFrame = geometry.frame(in: .global)
        
        // 计算 TextField 底部到屏幕顶部的距离
        let textFieldBottom = textFieldFrame.maxY
        // 计算键盘顶部在屏幕中的位置
        let keyboardTop = geometry.size.height - keyboardHeight

        // 如果 TextField 被键盘遮挡,则调整视图
        if textFieldBottom > keyboardTop {
            currentViewOffset = keyboardTop - textFieldBottom - 20
        }
    }
}

// 创建一个 Publisher 来发布键盘高度
extension Publishers {
    static var keyboardHeight: AnyPublisher<CGFloat, Never> {
        let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
            .map { $0.keyboardHeight }
        
        let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
            .map { _ in 0 }
        
        return MergeMany(willShow, willHide)
            .eraseToAnyPublisher()
    }
}

// 获取键盘高度的辅助函数
extension Notification {
    var keyboardHeight: CGFloat {
        return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
    }
}

代码说明:

  • 监听多个TextField的聚焦事件需要用到 @FocusState 。
  • 需要一个额外的 keyboardHeight 的 Published 发布器。
  • 计算视图偏移量 currentViewOffset 时,为确保 TextField 不紧贴键盘顶部,额外增加了一个常量偏移。

3. 安全建议

  • 仔细处理动画效果,确保用户界面的流畅性。 不恰当的动画可能导致卡顿,影响用户体验。

  • 需在不同的设备和键盘类型上进行充分的测试,包括有无预测文本栏和自定义键盘的情况,因为它们的高度各不相同。

三、List的使用

针对这个特殊用例,还有一种更加简洁的方式:使用 List。 SwiftUI 的 List 具备自动避让键盘的功能。

1. 代码实现

将多个 TextField 放入 List 中。

代码示例:

struct ContentView: View {
    @State var textfieldText: String = ""

    var body: some View {
        List {
            TextField("TextField1", text: $textfieldText)
            TextField("TextField2", text: $textfieldText)
            TextField("TextField3", text: $textfieldText)
            TextField("TextField4", text: $textfieldText)
            TextField("TextField5", text: $textfieldText)
            TextField("TextField6", text: $textfieldText)
            TextField("TextField7", text: $textfieldText)
        }
    }
}

2. 优势说明

利用 List 能快速解决此问题。 此方式代码量少,且不依赖于第三方库。 List 内部已处理键盘相关的通知。 当键盘弹出时,List 会自动滚动以显示当前获取焦点的 TextField。 键盘隐藏后,列表会恢复到原来的位置。
此方案简单且适用性广。 遇到多个 TextField 需防止被键盘遮挡问题时,此方案无疑值得尝试。