KeyboardAssistant 1.3.0

KeyboardAssistant 1.3.0

Levi 维护。



  • levieggert

KeyboardAssistant

版本 1.3.0

Keyboard Assistant 当设备键盘存在时方便视图的重定位。通过观测键盘通知(willShow, didShow, willHide, didHide)和在 UITextField 和 UITextView 对象开始编辑时对其进行响应来实现。

注意:在继续阅读之前,请注意我在这份文件中多次使用了“输入”和“输入项”这两个词。我指的是 InputNavigator 类中的输入项,其类型为 UITextField 和 UITextView。

要求

  • iOS 9.0+
  • Swift 5.0

Cocoapods

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'KeyboardAssistant', '1.3.0'
end

类概述

键盘助手被分解为3个核心类:KeyboardNotificationObserver、InputNavigator和KeyboardAssistant。KeyboardAssistant类是您将与之交互的主要类,位于KeyboardNotificationObserver和InputNavigator之上。在开始使用KeyboardAssistant之前,您需要创建一个InputNavigator并将其注入到KeyboardAssistant中。InputNavigator处理输入元素之间的导航,并负责通知KeyboardAssistant关于焦点变化(当一个UITextField或UITextView开始编辑时)。KeyboardNotificationObserver类负责检查键盘状态变化(willShow、didShow、willHide、didHide、didChangeFrame)并向KeyboardAssistant报告变化。如果您愿意,可以单独使用InputNavigator和KeyboardNotificationObserver。然而,这不是本模块的意图。要重新定位设备键盘上方的输入项,请使用KeyboardAssistant。要了解更多关于KeyboardAssistant的信息,请从下面的如何使用KeyboardAssistant部分开始。

如何使用KeyboardAssistant

使用键盘助手的3种主要方式。

  1. 自动滚动视图中助手:自动定位滚动视图内的输入项。
  2. 手动滚动视图中助手:提供在滚动视图内的动态定位。定位到按钮或下一个输入项。
  3. 手动助手:最灵活。不需要滚动视图。相反,您需要编写定位代码。

自动滚动视图中助手

自动滚动视图中助手会自动为您定位键盘上方的输入项。设置它需要非常少的代码,但是您需要将您的viewcontroller类结构为使用滚动视图。

在继续之前,请阅读更多关于如何结构您的滚动视图的信息。

在本示例中,我们将

  1. 创建一个具有默认控制器的InputNavigator。如果您不熟悉InputNavigator,请在这里了解更多信息
  2. 添加输入项到InputNavigator中。
  3. 创建一个自动滚动视图中助手。

开始创建自动滚动视图中助手的步骤。首先在控制器类中声明一个KeyboardAssistant变量。

import UIKit

class YourViewController: UIViewController {

    private var keyboardAssistant: KeyboardAssistant!
}

接下来,确保您的控制器结构为使用滚动视图进行定位。您需要一个为定位而定位的滚动视图的引用,以及键盘助手将放置在设备键盘顶部的滚动视图的底部约束。

import UIKit

class YourViewController: UIViewController {

    private var keyboardAssistant: KeyboardAssistant!

    @IBOutlet weak private var scrollView: UIScrollView!
    @IBOutlet weak private var scrollViewBottomConstraint: NSLayoutConstraint!
}

viewDidLoad()方法是您完成KeyboardAssistant设置的步骤。首先创建一个用于使用的InputNavigator并添加一些用于导航的输入。

import UIKit

class YourViewController: UIViewController {

    private var keyboardAssistant: KeyboardAssistant!

    @IBOutlet weak private var scrollView: UIScrollView!
    @IBOutlet weak private var contentView: UIView!
    @IBOutlet weak private var lbTitle: UILabel!
    @IBOutlet weak private var txtFirstName: UITextField!
    @IBOutlet weak private var txtLastName: UITextField!
    @IBOutlet weak private var txtEmail: UITextField!
    @IBOutlet weak private var txtPassword: UITextField!
    @IBOutlet weak private var btRegisterAccount: UIButton!

    @IBOutlet weak private var scrollViewBottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
    
        super.viewDidLoad()

        let navigator: InputNavigator = InputNavigator.createWithDefaultController()
        navigator.addInputItems(inputItems: [self.txtFirstName, 
        self.txtLastName, 
        self.txtEmail, 
        self.txtPassword])
    }
}

然后使用新创建的InputNavigator创建KeyboardAssistant。自动滚动视图中助手有6个参数。

  1. inputNavigator:定义使用的导航和要导航和重新定位的输入项。
  2. positionScrollView:将被重新定位的滚动视图。
  3. 位置约束:存在两种位置约束方式。(viewTopToTopOfScreen 和 viewBottomToTopOfKeyboard),用于定义如何定位输入项目。可以是将视图的顶部放置在屏幕顶部,或视图的底部放置在键盘顶部。
  4. 位置偏移:这是一个在通过位置约束定位视图后应用的偏移量。因此,如果您使用 position constraint viewTopToTopOfScreen,则视图的顶部将被定位到设备屏幕的顶部,然后应用位置偏移。
  5. 底部约束:这是 scrollview 的底部约束。它被设置为键盘顶部。这允许用户滚动通过所有子视图而不会受到键盘的干扰。
  6. 底部约束布局视图:这是更改底部约束位置所必需的。在大多数情况下,这将是控制器视图属性,因为它是 UIScrollView 底部约束的父视图。
import UIKit

class YourViewController: UIViewController {

    private var keyboardAssistant: KeyboardAssistant!

    @IBOutlet weak private var scrollView: UIScrollView!
    @IBOutlet weak private var contentView: UIView!
    @IBOutlet weak private var lbTitle: UILabel!
    @IBOutlet weak private var txtFirstName: UITextField!
    @IBOutlet weak private var txtLastName: UITextField!
    @IBOutlet weak private var txtEmail: UITextField!
    @IBOutlet weak private var txtPassword: UITextField!
    @IBOutlet weak private var btRegisterAccount: UIButton!

    @IBOutlet weak private var scrollViewBottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
    
        super.viewDidLoad()

        let navigator: InputNavigator = InputNavigator.createWithDefaultController()
        navigator.addInputItems(inputItems: [
        self.txtFirstName, 
        self.txtLastName, 
        self.txtEmail, 
        self.txtPassword])

        self.keyboardAssistant = KeyboardAssistant.createAutoScrollView(
        inputNavigator: navigator, 
        positionScrollView: self.scrollView, 
        positionConstraint: .viewBottomToTopOfKeyboard, 
        positionOffset: 30, 
        bottomConstraint: self.scrollViewBottomConstraint, 
        bottomConstraintLayoutView: self.view)
    }
}

最后,您需要管理注册通知的生命周期。这通过 KeyboardAssistant 的开始和停止方法来完成。

override func viewWillAppear(_ animated: Bool) {

    super.viewWillAppear(animated)

    self.keyboardAssistant.start()
}

override func viewWillDisappear(_ animated: Bool) {

    super.viewWillAppear(animated)

    self.keyboardAssistant.stop()
}

这就全部了。当其中一个输入项处于活动状态时,它会自动为您定位。

手动滑动视图辅助工具

手动辅助工具可用于手动定位滑动视图。

请看下面的截图。这是我们想要达到的效果。当一个输入项被聚焦时,我们想要定位到下一个输入项。如果我们位于最后一个输入项上,则定位到注册按钮。

alt text

在下面的代码中,我们将

  1. 创建一个包含键盘导航和默认控制器导航的 InputNavigator。
  2. 添加可供导航的输入项。
  3. 创建一个手动辅助工具。
  4. 管理键盘辅助工具通知,开始和停止。
import UIKit

class YourViewController: UIViewController {

    private var keyboardAssistant: KeyboardAssistant!

    @IBOutlet weak private var scrollView: UIScrollView!
    @IBOutlet weak private var contentView: UIView!
    @IBOutlet weak private var lbTitle: UILabel!
    @IBOutlet weak private var txtFirstName: UITextField!
    @IBOutlet weak private var txtLastName: UITextField!
    @IBOutlet weak private var txtEmail: UITextField!
    @IBOutlet weak private var txtPassword: UITextField!
    @IBOutlet weak private var btRegisterAccount: UIButton!

    @IBOutlet weak private var scrollViewBottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
    
        super.viewDidLoad()

        let navigator: InputNavigator = InputNavigator.createWithKeyboardNavigationAndDefaultController(shouldSetTextFieldDelegates: true)
        navigator.addInputItems(inputItems: [
        self.txtFirstName, 
        self.txtLastName, 
        self.txtEmail, 
        self.txtPassword])
        
        self.keyboardAssistant = KeyboardAssistant.createManual(
        inputNavigator: navigator,
        delegate: self,
        bottomConstraint: self.scrollViewBottomConstraint,
        bottomConstraintLayoutView: self.view)
    }

    override func viewWillAppear(_ animated: Bool) {
    
        super.viewWillAppear(animated)

        self.keyboardAssistant.start()
    }

    override func viewWillDisappear(_ animated: Bool) {
    
        super.viewWillAppear(animated)

        self.keyboardAssistant.stop()
    }
}

我们使用 KeyboardAssistantDelegate: keyboardAssistantManuallyReposition() 执行所有手动定位。如果有 nextInputItem,则定位到它。如果没有,那么我们位于末尾,所以定位到注册按钮。

我们将 shouldLoop: false 传递给 getNextInputItem。如果传递 true,则在位于最后一个输入项时返回第一个输入项。

// MARK: - KeyboardAssistantDelegate

extension YourViewController: KeyboardAssistantDelegate {

    func keyboardAssistantManuallyReposition(keyboardAssistant: KeyboardAssistant, toInputItem: UIView, keyboardHeight: Double) {
    
        let constraint: KeyboardAssistant.RepositionConstraint = .viewBottomToTopOfKeyboard
        let offset: CGFloat = 20

        if let nextInputItem = keyboardAssistant.navigator.getNextInputItem(inputItem: toInputItem, shouldLoop: false) {
            keyboardAssistant.reposition(scrollView: self.scrollView, toInputItem: nextInputItem, constraint: constraint, offset: offset)
        }
        else {
            keyboardAssistant.reposition(scrollView: self.scrollView, toInputItem: self.btRegisterAccount, constraint: constraint, offset: offset)
        }
    }
}

这达到了相同的效果,但是是一种硬编码的方法。

// MARK: - KeyboardAssistantDelegate

extension YourViewController: KeyboardAssistantDelegate {

    func keyboardAssistantManuallyReposition(keyboardAssistant: KeyboardAssistant, toInputItem: UIView, keyboardHeight: Double) {
    
        let constraint: KeyboardAssistant.RepositionConstraint = .viewBottomToTopOfKeyboard
        let offset: CGFloat = 20

        if toInputItem == self.txtFirstName {
            keyboardAssistant.reposition(scrollView: self.scrollView, toInputItem: self.txtLastName, constraint: constraint, offset: offset)
        }
        else if toInputItem == self.txtLastName {
            keyboardAssistant.reposition(scrollView: self.scrollView, toInputItem: self.txtEmail, constraint: constraint, offset: offset)
        }
        else if toInputItem == self.txtEmail {
            keyboardAssistant.reposition(scrollView: self.scrollView, toInputItem: self.txtPassword, constraint: constraint, offset: offset)
        }
        else if toInputItem == self.txtPassword {
            keyboardAssistant.reposition(scrollView: self.scrollView, toInputItem: self.btRegisterAccount, constraint: constraint, offset: offset)
        }
    }
}

手动辅助工具

当您想要进行自己的定位时,创建手动辅助工具。

在下面的代码中,我们将

  1. 创建一个包含键盘导航和默认控制器导航的 InputNavigator。
  2. 添加可供导航的输入项。
  3. 创建一个手动辅助工具。
  4. 管理键盘辅助工具通知,开始和停止。
  5. 添加 KeyboardAssistantDelegate: keyboardAssistantManuallyReposition() 来执行手动定位。
import UIKit

class YourViewController: UIViewController {

    private var keyboardAssistant: KeyboardAssistant!

    @IBOutlet weak private var txtFirstName: UITextField!
    @IBOutlet weak private var txtLastName: UITextField!
    @IBOutlet weak private var txtEmail: UITextField!
    @IBOutlet weak private var txtPassword: UITextField!
    @IBOutlet weak private var btRegisterAccount: UIButton!

    override func viewDidLoad() {
    
        super.viewDidLoad()

        let navigator: InputNavigator = InputNavigator.createWithKeyboardNavigationAndDefaultController(shouldSetTextFieldDelegates: true)
        navigator.addInputItems(inputItems: [
        self.txtFirstName, 
        self.txtLastName, 
        self.txtEmail, 
        self.txtPassword])
        
        self.keyboardAssistant = KeyboardAssistant.createManual(inputNavigator: navigator, delegate: self)
    }

    override func viewWillAppear(_ animated: Bool) {
    
        super.viewWillAppear(animated)

        self.keyboardAssistant.start()
    }

    override func viewWillDisappear(_ animated: Bool) {
    
        super.viewWillAppear(animated)

        self.keyboardAssistant.stop()
    }
}

// MARK: - KeyboardAssistantDelegate

extension YourViewController: KeyboardAssistantDelegate {

    func keyboardAssistantManuallyReposition(keyboardAssistant: KeyboardAssistant, toInputItem: UIView, keyboardHeight: Double) {
    
        // Do your manual positioning here.
    }
}

如何使用输入导航器

InputNavigator 有它自己的部分,因为这个类实际上有很多可以配置的方式,所以配置方式有很多。

在深入研究代码之前,最好先简要概述一下这个类的职责。这个类的主要目的是处理和提供输入项(UITextField / UITextView)之间的导航。InputNavigator 非常灵活,您可以自行选择使用内置导航选项或提供自己的选项。有两个内置选项,键盘返回键和DefaultNavigationView。这两个选项可以同时使用、分别使用或者都不使用。您可以提供自己的自定义视图用于导航,并将其附加到输入项的inputAccessoryView上,甚至可以和键盘返回键一起使用。您有大量的选项可供选择。

让我们从内置选项开始,并在此基础上进行展开。

使用默认控制器创建

alt text

我们从默认控制器开始。DefaultNavigationView 是 KeyboardAssistant 模块附带的一个自定义视图类,有一个自己的 .xib 文件用于创建 UI。它有 3 个主要按钮:btPrev、btNext 和 btDone。prev 和 next 按钮用于导航输入项,done 按钮将关闭键盘,通过解除活动输入项。要使用默认控制器创建导航器,请使用以下示例中的静态方法。

override func viewDidLoad() {

    super.viewDidLoad()
    
    let navigator: InputNavigator = InputNavigator.createWithDefaultController()
}

编辑默认控制器是很容易的。

override func viewDidLoad() {

    super.viewDidLoad()

    let navigator: InputNavigator = InputNavigator.createWithDefaultController()
    
    // change all button colors
    navigator.defaultController?.setButtonColors(color: .red)
    
    // you can also configure the default controller in anyway you like.
    if let defaultController = navigator.defaultController {
    
        // remove the top shadow or change the top shadow in anyway you want
        defaultController.layer.shadowOpacity = 0
        
        // edit individual buttons
        defaultController.btPrev.backgroundColor = .lightGray
        defaultController.btNext.backgroundColor = .lightGray
        defaultController.btDone.backgroundColor = .lightGray
        defaultController.setBtPrevColor(color: .white)
        defaultController.setBtNextColor(color: .white)
        defaultController.setBtDoneColor(color: .black)
    }
}

使用键盘导航创建

alt text

下一个内置导航选项是键盘导航,该导航使用键盘的返回键。在编辑 UITextField 时,returnKeyType 会被设置为 next 或 done,这取决于 UITextField 在 inputItems 列表中的索引位置。如果被编辑的 UITextField 在列表的末尾,它的 returnKeyType 被设置为 done,否则设置为 next。当点击 next 时,InputNavigator 会移动到下一个 inputItem;当点击 done 时,当前 inputItem 将解除活动状态,并隐藏键盘。

注意:不会在 UITextView 对象上设置 returnKeyType。这是故意的,因为返回键可用于向.TextView 添加新行。如果您需要从.TextView 导航,请考虑使用默认控制器或您自己的自定义导航。

重要!在创建具有键盘导航的InputNavigator时,存在一个布尔标志shouldSetTextFieldDelegates。如果传递true,InputNavigator将设置UITextField的代理属性以响应textFieldShouldReturn代理方法。如果您需要在控制器中使用UITextFieldDelegate,请在此处传递false,并确保调用InputNavigator的textFieldShouldReturn方法来传递导航。您可以在下面的代码示例中看到这个示例。

这是如何创建具有键盘导航的InputNavigator的方式。在这里传递true,将设置所有UITextField的代理为InputNavigator。

override func viewDidLoad() {

    super.viewDidLoad()
    
    let navigator: InputNavigator = InputNavigator.createWithKeyboardNavigation(shouldSetTextFieldDelegates: true)
}

如果您的控制器类需要使用UITextFieldDelegate,则将标志设置为false,并确保在InputNavigator上调用textFieldShouldReturn。

class YourViewController: UIViewController {

    private var keyboardAssistant: KeyboardAssistant!
    
    override func viewDidLoad() {
    
        super.viewDidLoad()

        let navigator: InputNavigator = InputNavigator.createWithKeyboardNavigation(shouldSetTextFieldDelegates: false)
    }
}

// MARK: - UITextFieldDelegate

extension YourViewController: UITextFieldDelegate {

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    
        _ = self.keyboardAssistant.navigator.textFieldShouldReturn(textField)

        return true
    }
}

使用自定义控制器创建

自定义控制器用于创建自定义导航视图。它将替换默认控制器。要创建自定义控制器,创建自己的UIView类并实现InputNavigatorAccessoryController协议。按钮Delegate属性用于InputNavigator响应用户操作btPrev、btNext和btDone。您必须在自定义视图类中设置这些操作并确保调用代理方法。

在您的自定义视图类中实现InputNavigatorAccessoryController协议。

import UIKit

public class YourCustomController: UIView, InputNavigatorAccessoryController {

    public var controllerView: UIView { return self }
    @IBOutlet public weak var btPrev: UIButton!
    @IBOutlet public weak var btNext: UIButton!
    @IBOutlet public weak var btDone: UIButton!
    public weak var buttonDelegate: InputNavigatorAccessoryControllerDelegate?
}
    

为btPrev、btNext和btDone添加操作,并调用适当的buttonDelegate方法来通知InputNavigator。

import UIKit

public class YourCustomController: UIView, InputNavigatorAccessoryController {

    // MARK: - Actions
    
    @IBAction func handlePrev(button: UIButton) {
        if let buttonDelegate = self.buttonDelegate {
            buttonDelegate.inputNavigatorAccessoryControllerPreviousButtonTapped(accessoryController: self)
        }
    }
    
    @IBAction func handleNext(button: UIButton) {
        if let buttonDelegate = self.buttonDelegate {
            buttonDelegate.inputNavigatorAccessoryControllerNextButtonTapped(accessoryController: self)
        }
    }
    
    @IBAction func handleDone(button: UIButton) {        
        if let buttonDelegate = self.buttonDelegate {
            buttonDelegate.inputNavigatorAccessoryControllerDoneButtonTapped(accessoryController: self)
        }
    }
}
    

使用自定义附加视图创建

let navigator: InputNavigator = InputNavigator.createWithCustomAccessoryView(accessoryView: yourAccessoryView)

使用键盘导航和默认控制器创建

let navigator: InputNavigator = InputNavigator.createWithKeyboardNavigationAndDefaultController(shouldSetTextFieldDelegates: true)

使用键盘导航和自定义控制器创建

let navigator: InputNavigator = InputNavigator.createWithKeyboardNavigation(shouldSetTextFieldDelegates: true, andController: yourCustomController)

使用键盘导航和自定义配件视图创建

let navigator: InputNavigator = InputNavigator.createWithKeyboardNavigation(shouldSetTextFieldDelegates: true, andCustomAccessoryView: yourAccessoryView)

添加输入项

将输入项添加到 InputNavigator 有多种方式。

  • 显式:手动将输入项添加到 InputNavigator。
  • 非显式:传递视图控制器引用。
显式添加输入项

显式添加输入项时,InputNavigator 将按照添加的顺序导航这些项。

要显式添加输入项,请在您的 InputNavigator 实例上调用以下方法

addInputItem(inputItem: UIView)
addInputItems(inputItems: [UIView])
从视图控制器添加输入项

从视图控制器引用添加输入项时,InputNavigator 将递归遍历视图控制器视图层次结构,并将所有 UITextField 和.TextView 对象添加到 inputItems 数组中。然后,根据每个输入项的起点(x,y)对 inputItems 数组进行排序,以便按从上到下的顺序进行导航。

此方法需要两个参数:ViewController 和 InputItemType。InputItemType 是一个枚举,用于过滤要添加哪些输入项。

  • InputItem.textField:仅添加在视图控制器中找到的文本字段。
  • InputItem.textView:仅添加在视图控制器中找到的文本视图。
  • InputItem.bothTextFieldAndTextView:添加在视图控制器中找到的文本字段和文本视图。

请在您的 InputNavigator 实例上调用以下方法

addInputItems(from: UIViewController, itemType: InputItemType)

组织您的滚视图

对于键盘定位,我更喜欢使用滚视图方法。这里有几个主要原因。

  1. 这使得它更加用户友好,因为在键盘打开的情况下,用户可以滚动浏览输入内容。
  2. 管理起来比TableView要简单得多。TableView虽然很棒,但在收集用户输入时管理起来可能会变得麻烦。这是因为在你滚动TableView时,单元格会被回收。这增加了收集输入和导航输入的额外管理。
  3. 结果是我通常需要在大多数ViewController中使用ScrollView来处理较小的设备大小。

在开始构建ScrollView之前,请确保在Interface Builder中勾选了“使用安全区域布局指南”选项。检查方法如下:选择您的.storyboard文件并选择文档选项卡。

alt text

以下是您需要构建的ViewController视图层级结构。UIView [root / UIViewController.view] > UIScrollView [scrollView] > UIView [contentView]。

UIScrollView应该设置所有边缘约束到安全区域。UIView [contentView]应该设置所有边缘约束到UIScrollView,并设置与UIScrollView相同的宽度。

就是这样。然后,您所有的自定义UI都放在UIView [contentView]中。

注意:此设置使用自动布局来确定ScrollView的内容大小。这意味着,UIView [contentView]内部的子视图都需要提供顶部和底部约束,以便满足contentView的高度。它还需要为您的一些子视图设置高度约束。除非它们的高度由它们的子视图决定。如果您不熟悉这个概念,请阅读更多关于自动布局的信息。

下方的截图是这个结构的示例。右边的约束显示了如何设置UIScrollView和UIView [contentView]的约束。

alt text

最后,请确保将ScrollView的底部约束连接到输出。这个约束位于键盘顶部,允许整个视图滚动而不受键盘的干扰。

alt text

示例

请确保检查KeyboardAssistant-Example项目。目前有3个示例,分别使用长ScrollView、嵌套子视图的长ScrollView和模拟注册屏幕的短ScrollView。每个示例都遵循FilteredKeyboardAssistant在运行时配置KeyboardAssistant,这可以让测试更加容易。如果您有任何希望添加的示例,请告诉我。

路线图

KeyboardAssistant目前处于早期发行阶段。我非常希望得到大家的意见和反馈。我还在考虑增加更多定位选项,例如直观的UIView定位和对UITableView的支持。