在iOS8 下用Swift 创建自定义的键盘

本文翻译自How to make a custom keyboard in iOS 8 using Swift

我将讲解一些关于键盘扩展的基本知识,然后使用iOS 8 提供的新应用扩展API来创建一个莫斯码键盘。大概需要你花20多分钟来走完所有的步骤。 完整代码

综述

一个自定义的键盘会替换系统的键盘,来提供给用户一个新的文本输入方法,或者输入哪些iOS系统还不支持的语言。一个自定义键盘的基本功能很简单:响应点击,手势或者其它输入事件以及在当前的文本输入对象的文本插入点上提供非属性化的NSString对象的文本。

当用户选择了一个键盘,那么当用户打开一个app时,这个键盘会作为默认的键盘显示。因此这个键盘必须允许用户切换到另一个键盘。

对于每个自定义键盘,有两个开发要素:

信任 : 你的自定义键盘可以访问用户输入的每个字符,所以你和你用户之间的信任非常重要。

下一个键盘按键 : 能够让用户切换另一个键盘这种可见性的功能应该是一个键盘用户界面的一部分;你必须提供这个切换功能。

注意:如果你只需要添加几个按钮到系统的键盘,你应该查看 Custom Views for Data Input

一个自定义键盘不能够做什么

有一些特定的输入对象是你的自定义键盘没资格输入的:安全领域(例如密码输入框), 电话键盘对象(如在通讯录中的电话号码输入框)。

你的自定义键盘不能访问输入视图的层级结构,不能控制光标和选择文本。

另外,自定义键盘无法在顶行以上显示任何东西(如系统键盘,当你在顶行长按一个按键时)。

沙盒

默认情况下,一个键盘是没有网络访问权限的,而且也无法与键盘的容器app分享文件。为了获得这些权限,可以在Info.plist 文件中设置 RequestsOpenAccess 这个布尔类型的键的值为 YES。 做这些会扩展键盘的沙盒,如 Establishing and Maintaining User Trust.中描述的。

如何你这么做来申请开放权限,你的键盘会获得一下功能,每一个都伴随着责任:

  • 访问位置服务和 Address BOOK 数据库,在第一次访问时会要求申请用户权限。

  • 可以与包含键盘的app共享一个容器,例如这样可以允许在包含键盘的app里面提供一个自定义的词库管理界面。

  • 能够发送键盘的点击和其它输入事件到服务端去处理。

  • 访问iCloud,例如确保同一个用户的键盘的设置和你的自动更正词库在所有设备上同步。

  • 通过包含键盘的app访问Game Center 和 应用内购买。

  • 如果你设计你的键盘支持手机设备管理(MDM),那么还可以允许与管理的app一起工作。

确保你阅读了 Designing for User Trust,它描述了在你申请开放权限的情况下,你尊重和保护用户数据的责任。

高层视图

下面的图片显示了在一个运行的键盘中一些重要的对象,并且显示了在一个典型的开发流程中这些对象来源于哪里。在一个最基本的形式中,我们有一个app包含了键盘扩展和一个控制这个键盘和响应用户事件的UIInputViewController对象。

这个自定义的键盘模版包含一个 UIInputViewController的子类,这是你的键盘的主视图控制器。让我们看看它的接口是怎么定义的:

class UIInputViewController : UIViewController, UITextInputDelegate, NSObjectProtocol {

    var inputView: UIInputView!

    var textDocumentProxy: NSObject! { get }

    func dismissKeyboard()
    func advanceToNextInputMode()

    // This will not provide a complete repository of a language's vocabulary.
    // It is solely intended to supplement existing lexicons.
    func requestSupplementaryLexiconWithCompletion(completionHandler: ((UILexicon!) -> Void)!)
}
  • inputView 是这个键盘的视图,与view属性一样

  • dismissKeyboard方法可以被调用来关闭键盘视图

  • advanceToNextInputMode 是用来切换键盘的

  • textDocumentProxy 是你将用来与当前的文本输入进行交互的对象。

例如:

self.textDocumentProxy.insertText("We ❤ Swift") // inserts the string "We ❤ Swift" at the insertion point

self.textDocumentProxy.deleteBackward() // Deletes the character to the left of the insertion point
  • UIInputViewController 实现了UITextInputDelegate协议,当文本或者选择的文本发生变化时,会使用selectionWillChange , selectionDidChange, textWillChangetextDidChange 消息来通知你。

创建一个莫斯码键盘

我们将创建一个简单的键盘,可以输入点和破折号,切换键盘,删除一个字符以及关闭键盘。这个例子只通过代码来创建用户界面。我们也可以使用Nib 文件来创建界面-这个会在教程末尾涉及到。 加载Nibs 文件可能会对性能产生负面影响。

创建一个新的工程

打开Xcode 6, 创建一个新的“Single Page Application” 项目,选择 Swift作为开发语言。

添加一个text field 文本框

打开 Main.storyboard ,然后从 Component Library 中拖动一个文本框。我们将在后面使用这个来测试我们的键盘。

把这个文本框居中,添加必要的约束。

暗示: 如果你在 viewDidLoad 中调用 textField.becomeFirstResponder() 那么当你打开这个app时键盘就会打开。

添加键盘扩展

在navigator中选择项目文件,点击 + 号添加一个新target。

选择 Application Extension ,使用 Custom Keyboard 模版, 命名为MorseCodeKeyboard

这样就会创建一个新的组,名叫 MorseCodeKeyboard,里面包含了两个文件 KeyboardViewController.swiftInfo.plist

清理

打开 KeyboardViewController.swift 文件。这个模版键盘有一个已经创建好的按钮,用来进行切换键盘的。把这些代码从 viewDidLoad 中移到一个新的方法 addNextKeyboardButton 中。

func addNextKeyboardButton() {
    self.nextKeyboardButton = UIButton.buttonWithType(.System) as UIButton

    ...

    var nextKeyboardButtonBottomConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1.0, constant: -10.0)
    self.view.addConstraints([nextKeyboardButtonLeftSideConstraint, nextKeyboardButtonBottomConstraint])
}

创建一个 addKeyboardButtons 方法,然后在 viewDidLoad 中调用它。这样会有助于组织代码。现在我们只是有了几个按钮,但是在实际的项目中会有更多的按钮。 在 addKeyboardButtons 中调用 addNextKeyboardButton

class KeyboardViewController: UIInputViewController {

    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        addKeyboardButtons()
    }

    func addKeyboardButtons() {
        addNextKeyboardButton()
    }

    ...

}

现在添加点按钮。 创建一个类型为 UIButton! 为的 dotButton 属性。

class KeyboardViewController: UIInputViewController {

    var nextKeyboardButton: UIButton!
    var dotButton: UIButton!

    ...
}

添加一个 addDot 方法。 以一个系统类型的按钮来初始化这个 dotButton 属性。给 TouchUpInside 事件添加一个回调。 设置一个更大的字体和添加一个圆角。 添加约束来把这个按钮放在离水平中心位置左边 50个点,垂直居中的位置。 代码与 nextKeyboardButton 的类似。

func addDot() {
    // initialize the button
    dotButton = UIButton.buttonWithType(.System) as UIButton
    dotButton.setTitle(".", forState: .Normal)
    dotButton.sizeToFit()
    dotButton.setTranslatesAutoresizingMaskIntoConstraints(false)

    // adding a callback
    dotButton.addTarget(self, action: "didTapDot", forControlEvents: .TouchUpInside)

    // make the font bigger
    dotButton.titleLabel.font = UIFont.systemFontOfSize(32)

    // add rounded corners
    dotButton.backgroundColor = UIColor(white: 0.9, alpha: 1)
    dotButton.layer.cornerRadius = 5

    view.addSubview(dotButton)

    // makes the vertical centers equa;
    var dotCenterYConstraint = NSLayoutConstraint(item: dotButton, attribute: .CenterY, relatedBy: .Equal, toItem: view, attribute: .CenterY, multiplier: 1.0, constant: 0)

    // set the button 50 points to the left (-) of the horizontal center
    var dotCenterXConstraint = NSLayoutConstraint(item: dotButton, attribute: .CenterX, relatedBy: .Equal, toItem: view, attribute: .CenterX, multiplier: 1.0, constant: -50)

    view.addConstraints([dotCenterXConstraint, dotCenterYConstraint])
}

使用 textDocumentProxy实现 dotButton 的回调。

func didTapDot() {
    var proxy = textDocumentProxy as UITextDocumentProxy

    proxy.insertText(".")
}

addKeyboardButtons 中调用 addDot

func addKeyboardButtons() {
    addDot()

    addNextKeyboardButton()
}

对于 dashdelete, hideKeyboard 按钮,过程类似。

破折号

代码类似于 dotButton,为了把它对称地放在水平中心位置,只需要改变水平约束的常量即可:

func addDash() {
    ...

    // set the button 50 points to the left (-) of the horizontal center
    var dotCenterXConstraint = NSLayoutConstraint(item: dotButton, attribute: .CenterX, relatedBy: .Equal, toItem: view, attribute: .CenterX, multiplier: 1.0, constant: -50)

    view.addConstraints([dashCenterXConstraint, dashCenterYConstraint])
}

func didTapDash() {
    var proxy = textDocumentProxy as UITextDocumentProxy

    proxy.insertText("_")
}

删除按钮

删除按钮会使用 deleteBackward 方法从 textDocumentProxy 中删除一个字符。 这个布局约束与 nextKeyboardButton 对称( .Left -> .Right, .Bottom-> .Top)。

func addDelete() {
    deleteButton = UIButton.buttonWithType(.System) as UIButton
    deleteButton.setTitle(" Delete ", forState: .Normal)
    deleteButton.sizeToFit()
    deleteButton.setTranslatesAutoresizingMaskIntoConstraints(false)
    deleteButton.addTarget(self, action: "didTapDelete", forControlEvents: .TouchUpInside)

    deleteButton.backgroundColor = UIColor(white: 0.9, alpha: 1)
    deleteButton.layer.cornerRadius = 5

    view.addSubview(deleteButton)

    var rightSideConstraint = NSLayoutConstraint(item: deleteButton, attribute: .Right, relatedBy: .Equal, toItem: view, attribute: .Right, multiplier: 1.0, constant: -10.0)

    var topConstraint = NSLayoutConstraint(item: deleteButton, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1.0, constant: +10.0)

    view.addConstraints([rightSideConstraint, topConstraint])
}

func didTapDelete() {
    var proxy = textDocumentProxy as UITextDocumentProxy

    proxy.deleteBackward()
}

隐藏键盘

hideKeyboardButton 会在点击时,调用 dismissKeyboard 来隐藏键盘:

func addHideKeyboardButton() {
    hideKeyboardButton = UIButton.buttonWithType(.System) as UIButton

    hideKeyboardButton.setTitle("Hide Keyboard", forState: .Normal)
    hideKeyboardButton.sizeToFit()
    hideKeyboardButton.setTranslatesAutoresizingMaskIntoConstraints(false)

    hideKeyboardButton.addTarget(self, action: "dismissKeyboard", forControlEvents: .TouchUpInside)

    view.addSubview(hideKeyboardButton)

    var rightSideConstraint = NSLayoutConstraint(item: hideKeyboardButton, attribute: .Right, relatedBy: .Equal, toItem: view, attribute: .Right, multiplier: 1.0, constant: -10.0)

    var bottomConstraint = NSLayoutConstraint(item: hideKeyboardButton, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1.0, constant: -10.0)

    view.addConstraints([rightSideConstraint, bottomConstraint])
}

使用 Nib 文件

为了不用手写这些布局约束,你可以创建一个界面文件,然后直接在上面添加约束。

创建一个界面文件

右击 MorseCodeKeyboard组,然后选择 New File.

选择 User Interface 和 View 模版。 命名为 CustomKeyboardInterface

选择 File’s Owner ,改变类名为 KeyboardViewController

在视图中添加一个按钮,设置标题为 We ❤ Swift 。 界面类似下面这样:

加载界面

init(nibName, bundle) 构造器中加载 CustomKeyboard nib 文件

class KeyboardViewController: UIInputViewController {

    ...

    var customInterface: UIView!

    init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        var nib = UINib(nibName: "CustomKeyBoardInterface", bundle: nil)
        let objects = nib.instantiateWithOwner(self, options: nil)
        customInterface = objects[0] as UIView
    }

    ...

}

添加到 inputView

viewDidLoad 方法中,添加自定义的界面到inputView中。

class KeyboardViewController: UIInputViewController {

    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(customInterface)

        ...
    }

    ...
}

给按钮添加一个回调

class KeyboardViewController: UIInputViewController {

    ...

    @IBAction func didTapWeheartSwift() {
        var proxy = textDocumentProxy as UITextDocumentProxy

        proxy.insertText("We ❤ Swift")
    }

    ...
}

连接按钮的事件到这个回调上

右键这个按钮,然后点击 touchUpInside 并拖动到 didTapWeHeartSwift 这个 IBAction中

最后,代码应该是这样的

在你的设备上安装这个容器app

在你的设备上运行这个app后,如下来添加你的自定义键盘:

选择键盘。

选择添加一个新键盘。找到我们的 MorseCode键盘:

现在重新运行我们的应用,尽情享受我们的新键盘吧。

最近的文章

Xcode:一个改变你生活的奇怪调试技巧

本文翻译自Xcode: One Weird Debugging Trick That Will Save Your Life 参考Advanced Debugging in Xcode and Swift希望你可以从题目看出我对于这个题目有多开心。不管怎样,让我们回到定期计划编程中…在过去的几天里,我一直在早餐时看这个高级调试和地址消毒剂。里面有个超级酷的调试技巧我希望能够写下来并记住它,从而能够使用它。奔溃写这篇博文的一个有趣的地方是,我随机打开一个已有的测试工程,并让它在下面一个...…

iOS继续阅读
更早的文章

iOS 8下简单,可交互式的通知

本文翻译自:Simple, interactive notifications in iOS 8在iOS 8 中新添加了一个API用来创建可交互式的通知。这些API允许你向在应用外的用户提供额外的功能。我发现网络上缺少清晰的例子,所以我认为我应该写一篇文章来向你展示如何简单地实现这个功能。下面是一个例子。让我们开始吧,在iOS 8中有三个新的类是必须的: UIUserNotificationSettings, UIUserNotificationCategory, UIUserNoti...…

iOS继续阅读