创建Application Extension
-
Xcode -> File -> New ->Target -> Custom Keyboard Extension.
-
默认提供了一个继承自UIInputViewController的类.和对应的infoPlist文件.
InfoPlist各字段含义.
3.1IsASCIICapable
:默认为NO,表示自定义键盘是否可以将ASCII字符串插入到文档中.
3.2PrefersRightToLeft
:默认为NO,表示自定义键盘是否为从右向左的语言.
3.3.PrimaryLanguage
:语言,默认美式英语.语言对照地址.
3.4.RequestsOpenAccess
:是否开启完全访问,默认为NO.
完全访问开启后可拥有的权限:(1).访问位置服务,地址簿数据库和相机胶卷.(2).能够通过网络发送击键,其他输入事件和数据以进行服务器端处理(3).能够使用UIPasteboard类.(4)能够播放音效,包括使用playInputClick方法的键盘点击.(5).可以访问Game Center和In-App Purchase.(6).如果您设计键盘以支持移动设备管理(MDM),则可以使用受管理的应用程序.
综上,可见完全访问是个很重要的功能.但这是需要用户自行开启的.没有完全访问的情况下,上述4内的功能都无法使用.
响应输入和删除
系统提供了一个 self.textDocumentProxy
属性,实际上一个代理对象,继承自UIKeyInput -> UITextInputTraits -> UITextInputTraits(箭头表示逐层向上的递进顺序).
- 首先,
[self.textDocumentProxy insertText:@"hello "];
或[self.textDocumentProxy insertText:@"\n"];
来执行文本的插入或换行. -
[self.textDocumentProxy deleteBackward];
来执行文本的删除.
原理:代理数据传输,只是和我们平时使用不同的是,我们成了代理方,系统的键盘文本框所属控制器成了代理的遵守方.将相当于你在自己写的代理方法中执行[self.degate xxx];
UITextDocumentProxy协议
经过验证,只有documentContextBeforeInput
和documentInputMode
属性可获取到值(上次的值),其他的未知
.
而adjustTextPositionByCharacterOffset:
方法则用来控制光标的位置.例如[self.textDocumentProxy adjustTextPositionByCharacterOffset: 1];
,它会一直向左前进一个.
注:删除操作默认是单个单次,如果想要全部删除.则需要使用分词器CFStringTokenizer Reference
- 每个自定义键盘(独立于其RequestsOpenAccess键的值)都可以通过UILexicon类访问基本的自动纠正词典。 利用这个课程以及您自己设计的词汇,在用户输入文本时提供建议和自动更正。 UILexicon对象包含来自各种来源的单词,其中包括:
- 来自用户的地址簿数据库的未配对的名字和姓氏.
- 在设置>常规>键盘>快捷方式列表中定义的文本快捷方式.
- 一个常用词典.
两个重点
用户信任和键盘模式切换.
- 如果你构建的键盘没有开放访问权限,则系统会确保键击无法发送回你或其他任何地方。 如果你的目标是提供正常的键盘功能,请使用非网络键盘。 其受限制的沙盒。
注意:
(1).不能将用户的输入信息发送到自己的服务器.
(2).不能存储用户的输入信息.
(3).不能进行用户行为分析.
(4).电话本数据限制.
(5).定位信息限制.
以上内容,不限制使用,但使用后易被下架. - 开放式访问键盘及其包含的应用程序可以将击键数据发送到你的服务器。 如果你使用此功能,不要将接收到的击键或语音数据存储到向用户提供文本所需的时间之外,或者
提供你向用户解释的功能
.
以上内容也是开放权限需提供解释,否则易被下架. -
advanceToNextInputMode
切换键盘模式.
扩展与宿主的数据共享
扩展与宿主间的数据共享是通过App Groups来实现的.
-
首先需要开发者账号创建证书.
1.1 填写信息.
1.2 Identify内要开通AppGroups功能.
1.3 Xcode内配置.
1.3.1 在宿主内配置.
1.3.2 扩展内配置.步骤同上,从配置证书开始.(扩展是另外一个Target,另一个进程,所以不能简单的额进行数据共享).不同的是BundileID创建过程中,需要勾选已经创建的AppGroups.之后在keyboard内也勾选相同的group.
1.4 使用.(封装了一个处理类,均是异步操作).
#import
typedef void(^compeletitonHandle)(BOOL success);
@interface SEKCacheValue : NSObject
/**单例对象*/
+ (instancetype)shared;
#pragma mark -----------
/**userDefaults存值*/
- (void)setUserDefaultsValue:(id)value forKey:(NSString *)key;
/**userDefaults读值*/
- (id)valueForUserDefaultWithKey:(NSString *)key;
#pragma mark ----------------
/**获取路径*/
- (NSURL *)fileURLWithCompComponent:(NSString *)component;
- (void)writeString:(NSString *)string toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle;
- (void)writeData:(NSData *)data toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle;
- (void)writeArray:(NSArray *)array toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle;
- (void)writeDictionary:(NSDictionary *)dictionary toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle;
- (void)readStringAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSString * string))handle;
- (void)readDataAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSData *data))handle;
- (void)readArrayAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSArray *array))handle;
- (void)readDictionaryAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSDictionary *dictionary))handle;
@end
#import "SEKCacheValue.h"
static NSString * const SEKAppGroupName = @"group.com.wkc.test.Keyboard"; //grounp名称
@interface SEKCacheValue()
@property (nonatomic, strong) NSUserDefaults * userDefault;
@property (nonatomic, strong) NSURL * groupURL;
@end
static SEKCacheValue * _instance = nil;
@implementation SEKCacheValue
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!_instance) _instance = [super allocWithZone:zone];
});
return _instance;
}
+ (instancetype)shared {
return [[self alloc] init];
}
- (NSUserDefaults *)userDefault {
if (!_userDefault) {
_userDefault = [[NSUserDefaults alloc] initWithSuiteName:SEKAppGroupName];
}
return _userDefault;
}
- (NSURL *)groupURL {
if (!_groupURL) {
_groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SEKAppGroupName];
}
return _groupURL;
}
- (void)setUserDefaultsValue:(id)value forKey:(NSString *)key {
[self.userDefault setValue:value forKey:key];
}
- (id)valueForUserDefaultWithKey:(NSString *)key {
return [self.userDefault valueForKey:key];
}
- (NSURL *)fileURLWithCompComponent:(NSString *)component {
return [self.groupURL URLByAppendingPathComponent:component];
}
- (void)writeString:(NSString *)string toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL success = [string writeToURL:URL atomically:YES encoding:NSUTF8StringEncoding error:nil];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(success);
});
});
}
- (void)writeData:(NSData *)data toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL success = [data writeToURL:URL atomically:YES];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(success);
});
});
}
- (void)writeArray:(NSArray *)array toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL success = [array writeToURL:URL atomically:YES];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(success);
});
});
}
- (void)writeDictionary:(NSDictionary *)dictionary toFileURL:(NSURL *)URL compeletionHandle:(compeletitonHandle)handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL success = [dictionary writeToURL:URL atomically:YES];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(success);
});
});
}
- (void)readStringAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSString * string))handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *string = [NSString stringWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:nil];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(string);
});
});
}
- (void)readDataAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSData *data))handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:URL];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(data);
});
});
}
- (void)readArrayAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSArray *array))handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *array = [NSArray arrayWithContentsOfURL:URL];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(array);
});
});
}
- (void)readDictionaryAtFileURL:(NSURL *)URL compeletionHandle:(void(^)(NSDictionary *dictionary))handle {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:URL];
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) handle(dictionary);
});
});
}
@end
开发注意
- 第一次启动时,需在设置>常规>键盘>添加键盘,添加自己的输入法.
- 扩展的BundileID要在宿主的BudileID的前提下,进行后缀添加.例如宿主的是
com.a.a
,那么扩展的需是com.a.a.xxx
. - 无法使用UIMenuController,进行菜单操作.
好了,剩下的就是具体的键盘界面开发了.至于性能调整,就需要自己在开发中实现了.