以下以开发一个范例插件DemoPlugin为例,为您介绍如何进行iOS插件开发
在本文档中,会用 斜体字 表示那些不是必须,但我们强烈推荐您这样做的操作。
EUExDemoPlugin
(注1),点击Next编辑EUExDemoPlugin这个target的Build Settings如下(注2):
Product Name
对应的值修改为 uexDemoPlugin
(注3)Per-configuration Build Products Path
修改为$SRCROOT/uexDemoPlugin
(注4)编辑EUExDemoPlugin这个target的Build Phases,找到Copy Files
这个phase,清空其Subpath设置,移除下面列表中的.h文件
注1: 此处静态库工程的命名规则为 EUEx + 插件名称
,之后出现的EUExDemoPlugin
亦是如此。
注2: 修改target的BuildSettings的方法如下图所示,选中工程主体-选择指定的target-选择BuildSettings-选中all,然后在右上角搜索框中搜索相应的键,双击编辑对应的值
注3: 此处Product Name 的命名规则为 uex + 插件名称
,之后出现的uexDemoPlugin
亦是如此。
注4: 此设置使得工程编译得到的.a文件会生成在工程目录下的uexDemoPlugin
文件夹中,方便后续的插件打包
调试插件的工程模板
,复制一份到本地目录EUExDemoPluin
的targetlibeuxDemoPlugin.a
(注2)注1: 编辑Build Phases方法如下图所示:选中工程主体-选择target-选择Build Phases - 展开相应的Phase - 点击下方的按钮进行相应的操作
注2: 编辑完成后应该如下图所示:
见下图,红框标注部分都是在插件开发调试中可能会用到的部分。
好了,到此,前期的准备工作就已经完成了,可以正式开始插件开发了!
所有的开发和调试工作,都可以直接在刚刚建立的插件调试工程中进行!
在AppCan插件开发包中,打开AppCan插件依赖库
文件夹,找到AppCanKit.framework
,并引入插件工程
新建插件入口类EUExPlugin。如果你的插件静态库工程名就是EUExDemoPlugin,那么这个类应该已经自动生成了,此步可跳过。
<AppCanKit/AppCanKit.h>
并使得EUExDemoPlugin类继承自EUExBase
*在此类中实现生命周期方法initWithWebViewEngine:
和clean
- (instancetype)initWithWebViewEngine:(id<AppCanWebViewEngineObject>)engine{
self = [super initWithWebViewEngine:engine];
if (self) {
//初始化工作
}
return self;
}
- (void)clean{
//NSLog(@"网页即将被销毁");
}
插件中类的命名规则
EUEx
开头的类名;EUExBase简介
EUExBase
是AppCan插件入口的基类,所有的插件入口类都必须继承自此类。EUExBase
拥有1个实例变量和3个实例方法webViewEngine
是一个弱引用,指向了AppCan的网页引擎。任何对网页的操作都会通过此对象进行。initWithWebViewEngine:
是默认的初始化方法。clean
会在网页被关闭前调用absPath:
用于转换AppCan协议路径(res://
,wgt://
等)至绝对路径。本小节示范了如何让网页JS去调用一个原生的方法helloWorld,实现 JavaScript --> OC 的操作
helloWorld:
- (void)helloWorld:(NSMutableArray *)inArguments{
//打印 hello world!
NSLog(@"hello world!");
插件入口类中实现供网页调用的方法的注意事项
1.注意所有供网页调用的方法必须带有一个类型为NSMutableArray类型的参数
2.引擎默认是在主线程异步调用插件方法,因此需要长时间耗时的阻塞操作,最好放在非主线程中进行,以免造成App卡死。
<?xml version="1.0" encoding="utf-8" ?>
<uexplugins>
<plugin name="uexDemoPlugin">
<method name="helloWorld"></method>
</plugin>
</uexplugins>
plugin.xml中注册插件方法的基本规则
1).每一个插件唯一对应了一个<plugin>
节点,节点中必须声明此插件的名字 用name
字段表示
2).在插件<plugin>
节点内,每个 <method>
节点对应了一个暴露给网页的插件方法,方法名字用name
字段表示
uexDemoPlugin.helloWorld();
html部分
<input type="button" value="helloWorld" onclick="helloWorld();"/>
JavaScript部分
var helloWorld = function(){
uexDemoPlugin.helloWorld();
}
好了 让我们运行工程,点击按钮看一下效果吧!
本小节示范了如何从网页传值给原生环境
sendValue:
- (void)sendValue:(NSMutableArray *)inArguments{
//打印传入的参数个数
NSLog(@"arguments count : %@",@(inArguments.count));
//打印每个参数的描述,和参数所在的类的描述
for (NSInteger i = 0; i < inArguments.count; i++) {
id obj = inArguments[i];
NSLog(@"value : %@ , class : %@ ",[obj description],[[obj class] description]);
}
}
<?xml version="1.0" encoding="utf-8" ?>
<uexplugins>
<plugin name="uexDemoPlugin">
<method name="helloWorld"></method>
<method name="sendValue"></method>
</plugin>
</uexplugins>
uexDemoPlugin.sendValue("aaa",12,true,["x","y"],{key:"value"});
结果如下
JaveScript-->OC传值的转换规则
由上述例子可以看到,JSValue按照如下规则转换成了NSObject
JSValue | NSObject |
---|---|
String | NSString |
Number | NSNumber |
Boolean | NSNumber |
Array | NSArray |
Object | NSDictionary |
null,undefined | NSNull |
Function | ACJSFunctionRef(注) |
注: 此项转换工作是由AppCan引擎完成;
NSString *identifier = inArguments[0];
//如果前端传值"1",则代码正常运作
//如果前端传值1,则此段代码会导致unrecognized selector错误
//因为此时identifier是一个NSNumber,并没有length方法
BOOL isIDValid = identidier.length > 0;
AppCanKit
中提供的宏ACArgsUnpack
来解析参数sendArguments:
,并在config.xml中添加相应的方法。- (void)sendArguments:(NSMutableArray *)inArguments{
//ACArgsUnpack的详细用法请参考AppCanKit内的注释
ACArgsUnpack(NSString *arg1,NSNumber *arg2,NSArray *arg3,NSDictionary *arg4) = inArguments;
for(id arg in @[arg1,arg2,arg3,arg4]){
ACLogDebug(@"value : %@ , class : %@ ",[arg description],[[arg class] description]);
}
}
uexDemoPlugin.sendArguments(123,"45",JSON.stringify([6,7]),JSON.stringify({key: "value"}))
结果如下
可以ACArgsUnpack
宏正确的将参数转换成了需要的类型
此小节示范了如何在插件中执行网页中的JS方法,实现OC --> JavaScript 的操作
执行网页中的JS脚本进行回调的方法实现
//封装一个JS方法用于查看回调结果
var showDetails = function(){
var count = arguments.length;
var result = "";
for (var i = 0;i < count;i ++){
var obj = arguments[i];
result = result + ("参数"+ i + " 类型:" +typeof(obj) + " 值:" + obj + "\n");
}
alert(result);
}
//注册回调函数必须要在window.uexOnload 或者AppCan.ready(如果你已经引入了appcan.js)中进行
//插件调试时,建议使用window.uexOnload,避免AppCan JSSDK可能的干扰
window.uexOnload = function(){
uexDemoPlugin.cbDoCallback = function(jsonStr){
//回调的参数是JSON字符串,需要解析成Object
var json = JSON.parse(jsonStr);
//查看回调结果
showDetails(jsonStr,json,json.key);
}
};
doCallback:
,并在config.xml中添加相应的方法。。- (void)doCallback:(NSMutableArray *)inArguments{
NSDictionary *dict = @{
@"key":@"value"
};
//ac_JSONFragment 方法,可以将NSDictionary转换成JSON字符串
[self.webViewEngine callbackWithFunctionKeyPath:@"uexDemoPlugin.cbDoCallback"
arguments:ACArgsPack(dict.ac_JSONFragment)
completion:^(JSValue * _Nullable returnValue) {
if (returnValue) {
ACLogDebug(@"回调成功!");
}
}];
}
调用接口后,alert窗口结果如下
方法可以直接同步返回值给网页。
注意若使用同步回调,需避免在返回值前执行耗时的操作,以免影响用户体验。
doSyncCallback:
,并在plugin.xml中声明//同步方法的返回值,必须是JSValue对应的NSObject子类
- (NSDictionary *)doSyncCallback:(NSMutableArray *)inArguments{
return @{
@"key1":@"value1",
@"key2":@(NO),
@"key3":@{
@"subKey":@"subValue"
}
};
}
//将获取的返回值赋值给obj
var obj = uexDemoPlugin.doSyncCallback();
//查看obj的结构
showDetails(obj,obj.key1,obj.key2,obj.key3.subKey);
控制台显示的结果如下
OC-->JavaScript同步返回值的转换规则
NSObject | JSValue |
---|---|
NSString | String |
@ YES,@ NO | Boolean |
其他NSNumber | Number |
NSArray | Array |
NSDictionary | Object |
nil,NSNull | null |
引擎中提供了对JSFunction的JavaScriptCore封装,方便开发者直接调用由JS传入的回调函数
在EUExDemoPlugin类中实现一个方法doFunctionCallback:
,并在config.xml中添加相应的方法
- (void)doFunctionCallback:(NSMutableArray *)inArguments{
//当要处理来自JS的回调函数时,直接用ACArgsUnpack宏对参数进行解包
//解包类型指定为ACJSFunctionRef
ACArgsUnpack(ACJSFunctionRef *callback) = inArguments;
//ACJSFunctionRef对象会保证JS中的回调函数不被GC回收,直至ACJSFunctionRef对象本身被销毁,或者由于其他原因(比如网页被关闭)导致JS上下文被销毁
//执行JS中的回调函数
[callback executeWithArguments:nil completionHandler:^(JSValue * _Nullable returnValue) {
//completionHandler会在JS回调函数执行完之后被调用,returnValue为JS回调函数的返回值
ACLogDebug(@"回调成功!");
}];
}
网页中执行下列JS,可以查看回调结果
uexDemoPlugin.doFunctionCallback(function(){
alert("function callback!");
});
本小节介绍了插件如何在网页上添加原生的View
添加view的限制
原生View总是会在网页顶端,即网页中所有dom元素上方
EUExDemoPlugin
类中实现方法addView:
removeView
并在plugin.xml中声明aView
来管理被添加的View@property (nonatomic,strong)UIView *aView;
- (void)addView:(NSMutableArray *)inArguments{
if (self.aView) {
//如果已经添加了view 直接返回
return;
}
ACArgsUnpack(NSDictionary *info) = inArguments;
NSNumber *isScrollableNum = numberArg(info[@"isScrollable"]);
if (!isScrollableNum) {
//如果参数信息不包含isScrollable这个键 直接返回
return;
}
BOOL isScroll = [isScrollableNum boolValue];
//新建一个view,并将其背景设置为红色
UIView *view = [[UIView alloc]initWithFrame:CGRectMake(10, 400, 300, 200)];
view.backgroundColor = [UIColor redColor];
if (isScroll) {
[[self.webViewEngine webScrollView] addSubview:view];
}else{
[[self.webViewEngine webView] addSubview:view];
}
//插件对象持有此view,方便对其进行移除操作
self.aView = view;
}
- (void)removeView:(NSMutableArray *)inArguments{
if (self.aView) {
[self.aView removeFromSuperview];
self.aView = nil;
}
}
<div>
isScrollable
<input id="checkbox1" type="checkbox" name="isScrollable"/>
</div>
<input type="button" value="addView" onclick="addView();"/><br>
<input type="button" value="removeView" onclick="removeView();"/><br>
<!--还需要添加一些占位文本使得网页超过一屏,从而可以滑动-->
var addView = function(){
//获取单选框的网页对象
var checkbox1 = document.getElementById("checkbox1");
//以单选框是否被勾选作为isScrollable的值,是个boolean
var json = {
isScrollable:checkbox1.checked
}
uexDemoPlugin.addView(JSON.stringify(json));
}
var removeView = function(){
uexDemoPlugin.removeView();
}
本小节介绍了插件如何在网页上展示原生的ViewController。
展示viewController的限制
只能以present的方式从当前的网页controller中切换到你自己的viewController中;
uexDemoPluginViewController
,然后在它的view上添加一个按钮,按钮会调用EUExDemoPlugin
中的dismissViewController
方法;uexDemoPluginViewController.h
@class EUExDemoPlugin;
@interface uexDemoPluginViewController : UIViewController
- (instancetype)initWithEUExObj:(EUExDemoPlugin *)euexObj;
@end
uexDemoPluginViewController.m
#import "uexDemoPluginViewController.h"
#import "EUExDemoPlugin.h"
@interface uexDemoPluginViewController()
@property (nonatomic,weak)EUExDemoPlugin *euexObj;
@end
@implementation uexDemoPluginViewController
- (instancetype)initWithEUExObj:(EUExDemoPlugin *)euexObj{
self = [super init];
if (self) {
_euexObj = euexObj;
}
return self;
}
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
self.view.backgroundColor = [UIColor whiteColor];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(100, 100, 180, 60);
[button setTitle:@"关闭ViewController" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onCloseButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
- (void)onCloseButtonClick:(id)sender{
[self.euexObj dismissViewController];
}
EUExDemoPlugin
类中实现方法presentController
并在plugin.xml中声明@property (nonatomic,strong)uexDemoPluginViewController *aViewController;
- (void)presentController:(NSMutableArray *)inArguments{
if (self.aViewController) {
return;
}
uexDemoPluginViewController *controller = [[uexDemoPluginViewController alloc]initWithEUExObj:self];
[[self.webViewEngine viewController]presentViewController:controller animated:YES completion:nil];
self.aViewController = controller;
}
EUExDemoPlugin
类中实现方法dismissViewController
关闭被present的ViewController,并给前端一个监听回调onControllerClose- (void)dismissViewController{
if (self.aViewController) {
[self.aViewController dismissViewControllerAnimated:YES completion:^{
[self callbackJSONWithName:@"onControllerClose" object:nil];
self.aViewController = nil;
}];
}
}
uexDemoPlugin.presentController();
//在window.uexOnload中
uexDemoPlugin.onControllerClose = function(){
alert("controller 被关闭!")
}
此步骤应该在您插件所有接口封装完毕,并在调试工程中测试完成后再进行
以下说明中均以范例插件uexDemoPlugin为例进行的操作。
在实际操作时,应该将所有出现的DemoPlugin替换成您自己的插件名字。
EUExDemoPlugin
- Generic iOS Device
libuexDemoPlugin.a
<uexplugins>
节点中其他插件的信息<uexplugins>
节点中按照plugin.xml中注册插件方法的基本规则 完成plugin.xml的编辑plugin.xml空白模板,是一个标准的xml文件
<?xml version="1.0" encoding="utf-8" ?>
<uexplugins>
</uexplugins>
最终完成的plugin.xml示例如下
<?xml version="1.0" encoding="utf-8" ?>
<uexplugins>
<plugin name="uexDemoPlugin">
<method name="helloWorld"></method>
<method name="sendValue"></method>
<method name="sendJSONValue"></method>
<method name="doCallback"></method>
<method name="doSyncCallback"></method>
<method name="addView"></method>
<method name="removeView"></method>
<method name="presentController"></method>
</plugin>
</uexplugins>
<?xml version="1.0" encoding="utf-8" ?>
<uexplugins>
<plugin
uexName="name" version="4.0.x" build="x">
</plugin>
</uexplugins>
其中 name 替换成uex开头的插件名 x替换成当前插件的版本号(非负整数)
<info>
节点和多个(可以为0个)<build>
节点构成。<info>
节点记录了当前版本的简介<build>
节点记录了历史版本的简介<info>
节点改为<build>
节点,同时在其之前添加新的<info>
节点<?xml version="1.0" encoding="utf-8" ?>
<uexplugins>
<plugin
uexName="uexDemoPlugin" version="4.0.1" build="1">
<info>1:添加其他开发说明的示例代码</info>
<build>0:AppCan iOS插件范例</build>
</plugin>
</uexplugins>
uexDemoPlugin
的文件夹并进入。如果您进行了前文中提及所有的可选操作,那么在插件工程目录下面,应该自动生成了此文件夹,可以直接使用。libuexDemoPlugin.a
,plugin.xml
,info.xml
拷贝至此文件夹中。如果您进行了前文中提及所有的可选操作,那么编译工程出来的.a文件将自动在此目录中生成。.bundle
文件.framework
文件uexDemoPlugin.bundle
uexDemoPlugin.plist
uexDemoPlugin
文件夹内的内容如下图所示
uexDemoPlugin
文件夹,得到插件zip包。以下是一些您在开发过程中可能需要的说明。
所有说明均可在范例工程中找到相应的示例代码。
插件引入第三方库的具体规则如下
Copy Files
的Build Phase中,这样可以在编译时将这些文件直接复制至uexDemoPlugin
文件夹中本小节主要介绍了如何建立插件自己的资源捆绑包(.bundle文件)以供使用。
这里的资源文件包括但不限于xib,storyboard,png,jpg,json,xml,js,plist等文件
选中插件静态库工程,然后点击菜单栏中的File - New - Target.. ,在弹出的对话框中选择OS X - Framework & Library - Bundle
product Name取名为uexDemoPluginBundle,点击finish完成创建。
Product Name
对应的值修改为 uexDemoPlugin
Pre-configuration Build Products Path
修改为$SRCROOT/uexDemoPlugin
(注1)Code Signing Identity
修改为Don't Code Sign
Combine High Resolution Artwork
修改为No
(注2)Info.plist File
的值置为空delete
,然后选择Move to Trash
以删除该文件EUExDemoPlugin
这个target的Build Phases ,在Target Dependicies
中添加刚刚创建的bundle的target注1:此settings是为了让build时将此bundle直接生成在
uexDemoPlugin
文件夹中注2:如果你的资源包中不包含图片文件,那么此设置可跳过。
注3:这个设置是为了保证在插件clean时可以清除生成的bundle文件,在插件build时会自动生成新的bundle文件
uexDemoPluginBundle
这个target中即可ac_bundleForPlugin:
方法,用以获取插件对应的NSBunble实例bundle加载@ 2x ,@ 3x图片文件的处理方法
获取到NSBundle实例后,用NSBundle的pathForResource: ofType:
并不能自动识别@ 2x,@ 3x的图片文件,最好用resourcePath
方法获得实际路径,然后拼接得到图片路径。
示例如下
NSBundle *pluginBundle = [NSBundle ac_bundleForPlugin:@"uexDemoPlugin"];
//从bundle中读取资源图片文件的示例
//直接用[pluginBundle pathForResource:@"sun" ofType:@"png"];只能匹配"sun.png"这个文件的路径,找不到会返回nil,而不会寻找文件"sun@2x.png"和"sun@3x.png".
NSString *path = [[pluginBundle resourcePath] stringByAppendingPathComponent:@"sun.png"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
ac_plugin: localizedString:
得到国际化的字符串.示例如下label.text = [NSString ac_plugin:@"uexDemoPlugin" localizedString:@"title"];
AppCan会将大部分ApplicationDelegate事件分发到每个插件入口类,插件入口类用相应的类方法接收即可 目前插件入口类可供接收的类方法有
+ (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
+ (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
+ (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err;
+ (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;
+ (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
+ (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;
+ (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url;
+ (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation;
+ (void)applicationWillResignActive:(UIApplication *)application;
+ (void)applicationDidBecomeActive:(UIApplication *)application;
+ (void)applicationDidEnterBackground:(UIApplication *)application;
+ (void)applicationWillEnterForeground:(UIApplication *)application;
+ (void)applicationWillTerminate:(UIApplication *)application;
+ (void)applicationDidReceiveMemoryWarning:(UIApplication *)application;
+ (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler;
+ (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler;
+ (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler;
//UNUserNotificationCenterDelegate方法(iOS 10+)
//注意此方法的completionHandler参数应为`UNNotificationPresentationOptions`
+ (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(NSUInteger))completionHandler;
+ (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler;
示例:
//EUExDemoPlugin.m中
static NSDictionary *AppLaunchOptions;
+ (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSLog(@"app launched");
//存储launchOptions
AppLaunchOptions = launchOptions;
return YES;
}
AppCan引擎会额外分发如下事件至每个插件入口类
+ (void)rootPageDidFinishLoading;//root页面加载完成的事件,在此事件触发后才能有效执行回调网页的相关方法
示例:
//第一个网页(root页面)加载完成时会触发此事件
//部分事件(比如application:didFinishLaunchingWithOptions:)触发时,第一个网页可能还没加载完成,因此无法当时回调给网页
//这些回调应该延迟至这个事件触发时再回调给root页面
+ (void)rootPageDidFinishLoading{
//AppCanRootWebViewEngine方法可以直接获取root页面对象的网页引擎
[AppCanRootWebViewEngine() callbackWithFunctionKeyPath:@"uexDemoPlugin.onAppLaunched" arguments:ACArgsPack([AppLaunchOptions ac_JSONFragment])];
AppLaunchOptions = nil;
}
uexDemoPlugin.plist
文件,可以通过Xcode新建,也可以在文本编辑器中直接输入如下内容并另存为uexDemoPlugin.plist<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>
UexKeyValues
的key,其Type选为Dictionary
UexKeyValues
这个dictionary中添加你需要添加到info.plist中的内容,打包服务器会自动将这些内容合入最后打包工程的info.plist中uexDemoPlugin.plist
放入uexDemoPlugin
文件夹中上传插件时提示目录结构错误
uexXXX
开头的文件夹libuexXXX.a
,info.xml
,plugin.xml
这3个文件info.xml
,plugin.xml
中的名称保持一致info.xml
中的版本号正确的递增了,以及<info>
节点正确填写了在线打包时出现Undefined symbols for architecture xxx
类型的报错:
出现这种错误主要有以下几种原因
Generic iOS Device
或者在用命令行编译时没有注明-sdk iphoneos
,导致缺少对应的架构。在线打包时出现duplicate symbols for architecture xxx
类型的报错:
出现这种错误的主要原因是类名冲突,请先根据日志找到冲突的类名以及它们分别所属的文件