最完整的iOS虚拟键盘开发指南:从模仿到定制Tasty Imitation Keyboard
读完你能得到
- 掌握iOS自定义键盘(Custom Keyboard Extension)的完整开发流程
- 理解Tasty Imitation Keyboard的架构设计与核心实现
- 学习Swift语言在键盘开发中的最佳实践
- 解决键盘布局适配、按键交互等关键技术难题
- 实现自动大写、双击空格输入句号等高级功能
为什么需要自定义键盘?
在iOS开发中,系统键盘虽然功能完善,但在特定场景下仍无法满足个性化需求。无论是企业级应用需要集成安全输入、教育类App需要特殊符号输入,还是创意应用需要独特的交互方式,自定义键盘都成为不可或缺的解决方案。Tasty Imitation Keyboard作为一个开源的仿原生键盘项目,为我们提供了绝佳的学习范例。
项目架构概览
Tasty Imitation Keyboard采用MVC架构模式,将数据模型、界面展示和业务逻辑清晰分离:
核心组件说明
| 组件 | 作用 | 关键类 |
|---|---|---|
| 控制器 | 处理用户交互和生命周期 | KeyboardViewController |
| 布局 | 管理键盘按键位置和尺寸 | KeyboardLayout |
| 模型 | 定义键盘结构和按键属性 | KeyboardModel, Key |
| 视图 | 绘制按键和处理触摸 | KeyboardKey, ForwardingView |
| 设置 | 管理用户偏好设置 | DefaultSettings |
环境搭建与项目配置
开发环境要求
- Xcode 10.0+
- iOS 8.0+ SDK
- Swift 4.0+
项目克隆与构建
git clone https://link.gitcode.com/i/a8455ba8c7dfccaf6974b9ebf062b879.git
cd tasty-imitation-keyboard
open TastyImitationKeyboard.xcodeproj
关键配置步骤
- 启用键盘扩展:在Xcode项目设置中,确保
Keyboard目标已正确配置为扩展 - 设置Info.plist:添加必要的权限和支持的语言
- 配置主应用:设置HostingApp作为键盘的容器应用
核心功能实现详解
1. 键盘布局系统
Tasty Imitation Keyboard的布局系统是其最复杂也最核心的部分,通过动态计算确保在不同设备上的完美适配:
// KeyboardLayout.swift
func generateKeyFrames(_ model: Keyboard, bounds: CGRect, page pageToLayout: Int) -> [Key:CGRect]? {
if bounds.height == 0 || bounds.width == 0 {
return nil
}
let isPortrait = bounds.width < bounds.height
let keyboardWidth = layoutConstants.keyboardShrunkSize(bounds.width)
let sideEdges = isPortrait ? layoutConstants.sideEdgesPortrait(bounds.width) : layoutConstants.sideEdgesLandscape
let topEdge = isPortrait ? layoutConstants.topEdgePortrait(bounds.width) : layoutConstants.topEdgeLandscape
// 计算可用区域
let availableWidth = keyboardWidth - 2 * sideEdges
let availableHeight = bounds.height - topEdge
// 计算行高和行间距
let rowCount = model.pages[pageToLayout].rows.count
let rowGap = isPortrait ? layoutConstants.rowGapPortrait(bounds.width) : layoutConstants.rowGapLandscape
let totalRowGap = rowGap * CGFloat(rowCount - 1)
let rowHeight = (availableHeight - totalRowGap) / CGFloat(rowCount)
// 为每个按键生成Frame
var keyFrames = [Key:CGRect]()
var currentY = topEdge
for (rowIndex, row) in model.pages[pageToLayout].rows.enumerated() {
// 计算每行按键宽度
let keyCount = row.count
let keyGap = isPortrait ? layoutConstants.keyGapPortrait(bounds.width, rowCharacterCount: keyCount) : layoutConstants.keyGapLandscape(bounds.width, rowCharacterCount: keyCount)
let totalKeyGap = keyGap * CGFloat(keyCount - 1)
let keyWidth = (availableWidth - totalKeyGap) / CGFloat(keyCount)
var currentX = sideEdges
for key in row {
let keyFrame = CGRect(x: currentX, y: currentY, width: keyWidth, height: rowHeight)
keyFrames[key] = keyFrame
currentX += keyWidth + keyGap
}
currentY += rowHeight + rowGap
}
return keyFrames
}
2. 按键交互处理
键盘的核心交互逻辑在KeyboardViewController中实现,包括按键按下、抬起、长按等事件:
// KeyboardViewController.swift
func keyPressedHelper(_ sender: KeyboardKey) {
if let model = self.layout?.keyForView(sender) {
self.keyPressed(model)
// 空格键或回车键后自动切换回字母键盘
if model.type == Key.KeyType.space || model.type == Key.KeyType.return {
self.currentMode = 0
}
// 处理单引号后自动切换回字母键盘
else if model.lowercaseOutput == "'" {
self.currentMode = 0
}
// 双击空格输入句号功能
self.handleAutoPeriod(model)
}
// 更新大小写状态
self.updateCapsIfNeeded()
}
func handleAutoPeriod(_ key: Key) {
if !UserDefaults.standard.bool(forKey: kPeriodShortcut) {
return
}
if self.autoPeriodState == .firstSpace {
if key.type != Key.KeyType.space {
self.autoPeriodState = .noSpace
return
}
// 检查前序文本是否符合条件
let charactersAreInCorrectState = { () -> Bool in
guard let previousContext = self.textDocumentProxy.documentContextBeforeInput else {
return false
}
if previousContext.count < 3 {
return false
}
var index = previousContext.endIndex
index = previousContext.index(before: index)
if previousContext[index] != " " { return false }
index = previousContext.index(before: index)
if previousContext[index] != " " { return false }
index = previousContext.index(before: index)
let char = previousContext[index]
return !self.characterIsWhitespace(char) && !self.characterIsPunctuation(char)
}()
if charactersAreInCorrectState {
// 删除两个空格并输入句号和一个空格
self.textDocumentProxy.deleteBackward()
self.textDocumentProxy.deleteBackward()
self.textDocumentProxy.insertText(". ")
}
self.autoPeriodState = .noSpace
} else {
if key.type == Key.KeyType.space {
self.autoPeriodState = .firstSpace
}
}
}
3. 自动大写功能实现
自动大写是提升用户体验的重要功能,实现逻辑如下:
// KeyboardViewController.swift
func shouldAutoCapitalize() -> Bool {
if !UserDefaults.standard.bool(forKey: kAutoCapitalization) {
return false
}
let proxy = textDocumentProxy
let autocapitalizationType = proxy.autocapitalizationType
switch autocapitalizationType {
case .words, .sentences, .allCharacters:
break
default:
return false
}
// 获取输入上下文
let previousContext = proxy.documentContextBeforeInput ?? ""
// 句子开头自动大写
if autocapitalizationType == .sentences {
let terminators = [".", "!", "?"]
let hasTerminator = terminators.contains { previousContext.hasSuffix($0) }
if hasTerminator || previousContext.isEmpty {
return true
}
}
// 单词开头自动大写
if autocapitalizationType == .words {
let whitespaceCharacters = CharacterSet.whitespacesAndNewlines
let lastCharacter = previousContext.unicodeScalars.last
if previousContext.isEmpty || (lastCharacter != nil && whitespaceCharacters.contains(lastCharacter!)) {
return true
}
}
// 全部大写
if autocapitalizationType == .allCharacters {
return true
}
return false
}
4. 多页面切换机制
Tasty Imitation Keyboard实现了字母、数字和符号三个页面的切换功能:
// KeyboardViewController.swift
func setMode(_ mode: Int) {
self.forwardingView.resetTrackedViews()
self.shiftStartingState = nil
self.shiftWasMultitapped = false
let uppercase = self.shiftState.uppercase()
let characterUppercase = (UserDefaults.standard.bool(forKey: kSmallLowercase) ? uppercase : true)
self.layout?.layoutKeys(mode, uppercase: uppercase, characterUppercase: characterUppercase, shiftState: self.shiftState)
self.setupKeys()
}
// 模式切换按键处理
func modeChangeTapped(_ sender: KeyboardKey) {
if let toMode = self.layout?.viewToModel[sender]?.toMode {
self.currentMode = toMode
}
}
高级功能实现
1. 键盘主题切换
支持明/暗两种主题模式,根据系统设置自动切换:
// KeyboardLayout.swift
func updateAppearances(_ appearanceIsDark: Bool) {
self.layout?.solidColorMode = self.solidColorMode()
self.layout?.darkMode = appearanceIsDark
self.layout?.updateKeyAppearance()
self.bannerView?.darkMode = appearanceIsDark
self.settingsView?.darkMode = appearanceIsDark
}
// 判断当前是否为暗黑模式
func darkMode() -> Bool {
let proxy = self.textDocumentProxy
return proxy.keyboardAppearance == UIKeyboardAppearance.dark
}
2. 用户设置界面
实现了包含多项设置的界面,允许用户自定义键盘行为:
// DefaultSettings.swift
class DefaultSettings: UIView {
var autoCapitalizationSwitch: UISwitch!
var periodShortcutSwitch: UISwitch!
var keyboardClicksSwitch: UISwitch!
var smallLowercaseSwitch: UISwitch!
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
loadSettings()
}
func setupViews() {
// 创建并布局各个设置项...
autoCapitalizationSwitch.addTarget(self, action: #selector(toggleSetting(_:)), for: .valueChanged)
periodShortcutSwitch.addTarget(self, action: #selector(toggleSetting(_:)), for: .valueChanged)
keyboardClicksSwitch.addTarget(self, action: #selector(toggleSetting(_:)), for: .valueChanged)
smallLowercaseSwitch.addTarget(self, action: #selector(toggleSetting(_:)), for: .valueChanged)
}
func loadSettings() {
let defaults = UserDefaults.standard
autoCapitalizationSwitch.isOn = defaults.bool(forKey: kAutoCapitalization)
periodShortcutSwitch.isOn = defaults.bool(forKey: kPeriodShortcut)
keyboardClicksSwitch.isOn = defaults.bool(forKey: kKeyboardClicks)
smallLowercaseSwitch.isOn = defaults.bool(forKey: kSmallLowercase)
}
@objc func toggleSetting(_ sender: UISwitch) {
let defaults = UserDefaults.standard
switch sender {
case autoCapitalizationSwitch:
defaults.set(sender.isOn, forKey: kAutoCapitalization)
case periodShortcutSwitch:
defaults.set(sender.isOn, forKey: kPeriodShortcut)
case keyboardClicksSwitch:
defaults.set(sender.isOn, forKey: kKeyboardClicks)
case smallLowercaseSwitch:
defaults.set(sender.isOn, forKey: kSmallLowercase)
updateKeyCaps(self.shiftState.uppercase())
default:
break
}
}
}
常见问题与解决方案
1. 键盘高度适配问题
问题:不同设备和 orientations 下键盘高度不一致
解决方案:动态计算键盘高度,考虑设备类型和屏幕方向
// KeyboardViewController.swift
func height(forOrientation orientation: UIInterfaceOrientation, withTopBanner: Bool) -> CGFloat {
let isPad = UIDevice.current.userInterfaceIdiom == .pad
let actualScreenWidth = (UIScreen.main.nativeBounds.size.width / UIScreen.main.nativeScale)
let canonicalPortraitHeight: CGFloat
let canonicalLandscapeHeight: CGFloat
if isPad {
canonicalPortraitHeight = 264
canonicalLandscapeHeight = 352
} else {
// 根据屏幕宽度适配不同设备
canonicalPortraitHeight = orientation.isPortrait && actualScreenWidth >= 400 ? 226 : 216
canonicalLandscapeHeight = 162
}
let topBannerHeight = withTopBanner ? metric("topBanner") : 0
return orientation.isPortrait ? canonicalPortraitHeight + topBannerHeight : canonicalLandscapeHeight + topBannerHeight
}
2. 按键响应区域问题
问题:按键边缘区域点击无响应
解决方案:优化按键布局计算,确保按键之间无缝衔接
// 调整按键间距计算方式
class func keyGapPortrait(_ width: CGFloat, rowCharacterCount: Int) -> CGFloat {
let compressed = (rowCharacterCount >= self.keyCompressedThreshhold)
if compressed {
return width >= self.keyGapPortraitUncompressThreshhold ? keyGapPortraitNormal : keyGapPortraitSmall
} else {
return keyGapPortraitNormal
}
}
测试与调试技巧
1. 调试键盘扩展
自定义键盘作为扩展,调试方式与普通应用有所不同:
- 在Xcode中选择
Keyboard目标 - 点击"Edit Scheme",设置"Executable"为HostingApp
- 在设备上运行,会自动启动HostingApp
- 在任意输入框点击,激活自定义键盘
- 设置断点进行调试
2. 测试不同设备适配
使用Xcode的模拟器测试不同设备和方向:
# 列出所有可用模拟器
xcrun simctl list devices
# 启动特定模拟器
open -a Simulator --args -CurrentDeviceUDID <device-udid>
发布与部署
1. 项目配置检查清单
- 确保所有资源文件都已正确添加到目标
- 检查Info.plist中的权限设置
- 验证键盘扩展的NSExtension属性
- 确保支持的iOS版本正确设置
2. App Store上架注意事项
- 自定义键盘必须提供关闭选项
- 确保不收集用户输入数据,或明确告知用户并获得许可
- 键盘扩展不能包含广告
- 必须支持所有iOS设备屏幕尺寸
扩展与定制方向
Tasty Imitation Keyboard作为一个基础框架,可以从以下几个方向进行扩展:
1. 添加表情符号键盘
func addEmojiKeyboard(to keyboard: Keyboard) {
let emojiPage = 3
// 添加常用表情符号
let emojis = ["😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "😇"]
for emoji in emojis {
let key = Key(.specialCharacter)
key.uppercaseOutput = emoji
key.lowercaseOutput = emoji
key.uppercaseKeyCap = emoji
key.lowercaseKeyCap = emoji
keyboard.add(key: key, row: 0, page: emojiPage)
}
// 添加切换到表情键盘的按钮
let emojiModeButton = Key(.modeChange)
emojiModeButton.uppercaseKeyCap = "😊"
emojiModeButton.toMode = emojiPage
keyboard.add(key: emojiModeButton, row: 3, page: 0)
// 添加返回字母键盘的按钮
let letterModeButton = Key(.modeChange)
letterModeButton.uppercaseKeyCap = "ABC"
letterModeButton.toMode = 0
keyboard.add(key: letterModeButton, row: 3, page: emojiPage)
}
2. 实现手势输入
可以添加滑动输入功能,提升输入效率:
// 添加滑动手势识别
func setupSwipeGestures() {
let swipeRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
swipeRecognizer.cancelsTouchesInView = false
self.forwardingView.addGestureRecognizer(swipeRecognizer)
}
@objc func handleSwipe(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
// 记录滑动起点和当前按键
let location = recognizer.location(in: self.forwardingView)
startKey = keyAtLocation(location)
currentKey = startKey
case .changed:
// 更新滑动位置和高亮按键
let location = recognizer.location(in: self.forwardingView)
let newKey = keyAtLocation(location)
if newKey != currentKey {
currentKey?.isHighlighted = false
newKey?.isHighlighted = true
currentKey = newKey
}
case .ended, .cancelled:
// 输入当前按键并重置状态
if let key = currentKey {
keyPressedHelper(key)
}
currentKey?.isHighlighted = false
startKey = nil
currentKey = nil
default:
break
}
}
总结与展望
Tasty Imitation Keyboard作为一个开源的自定义键盘项目,展示了iOS自定义键盘开发的核心技术和最佳实践。通过学习该项目,我们掌握了从键盘布局、按键交互到用户设置的完整实现流程。
未来可以进一步探索的方向:
- 集成AI预测输入功能
- 添加手写输入支持
- 实现云同步用户配置
- 支持多语言输入
希望本指南能帮助你快速入门iOS自定义键盘开发,并创造出功能强大、体验优秀的键盘应用!
参考资料
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



