# 简介

本文档主要介绍如何在 iOS 平台开发 uni-app 原生插件,在您阅读此文档时,您需要具备 iOS 应用开发经验,对 HTML、JavaScript、CSS 等前端开发有一定的了解,并且熟悉在JavaScript 和 Objective-C 环境下的 JSON 格式数据操作等。

# 什么是uni原生插件

uni原生插件指的是将您原生开发的功能按照规范封装成插件包,然后即可在 uni-app 前端项目中通过js调用您开发的原生能力。

# 开发环境

  • iOS开发环境,Xcode14 及以上版本
  • 下载开发插件需要的 SDK包 并解压
  • 安装 uni-app 开发工具 HBuilderX

# SDK 包结构说明

|--iOSSDK
	|-- HBuilder-Hello				// uni-app 离线打包工程
	|-- HBuilder-uniPluginDemo		// uni-app 插件开发主工程 (本文档需要使用的工程)
	|-- SDK							// 依赖库及依赖资源文件
	|-- Feature-iOS.xls				// 功能模块与依赖库对应关系说明表格
	|-- readme.txt					// 目录说明

SDK 目录中的 HBuilder-uniPluginDemouni原生插件开发主工程,该工程已经将各项配置都配置齐全,开发uni原生插件需要依赖此工程,本文档的插件示例工程DCTestUniPlugin也在目录中,另外插件市场的 原生增强提示框插件 对应的原生插件工程DCRichAlert也放到了此目录中提供给开发者作为参考,其他工程及文件,开发uni原生插件不需要关心,如果想了解更多可以参考 App离线打包 相关文档

# 创建插件工程

打开 Xcode,创建一个新的工程,template 选择 FrameworkStatic Library(示例工程选择的是 Framework),然后点击 Next

在 Product Name 中输入插件工程名称(建议使用一个性化的前缀,避免与其他人的插件包名冲突),其他项不需要修改保持工程默认填充的即可,然后点击Next

然后选择工程存放路径,建议直接存放在 iOSSDK目录中的 HBuilder-uniPluginDemo 插件开发主工程目录下,如下图所示,然后点击 Create

强烈建议将插件工程存放在 HBuilder-uniPluginDemo 插件开发主工程目录中,因为插件工程需要根据插件开发主工程相对路径引用一些文件,之后您升级SDK的时候只需要将您的插件工程 Copy 到新的 SDK 的相同位置下即可

可以删除工程自动创建的 .h 文件,这个文件用不到

然后选中工程名,在TARGETS->Build Settings中,将 Mach-O Type 设置为 Static Library 如下图所示

然后将插件工程关闭,接下来需要将插件工程导入到插件开发主工程中

# 导入插件工程

打开 iOSSDK/HBuilder-uniPluginDemo工程目录,双击目录中的HBuilder-uniPlugin.xcodeproj 文件运行插件开发主工程

在 Xcode 项目左侧目录选中主工程名,然后点击右键选择Add Files to “HBuilder-uniPlugin” ...

然后选择您刚刚创建的插件工程路径中,选中插件工程文件,勾选 Create folder referencesAdd to targets 两项,然后点击Add

这时在 Xcode 左侧目录中可以看到插件工程已经添加到了主工程中,如下图所示

# 工程配置

然后在 Xcode 项目左侧目录选中主工程名,在TARGETS->Build Phases->Dependencies中点击+

在弹窗中选中插件工程,如图所示,然后点击Add,将插件工程添加到Dependencies

然后在Link Binary With Libraries中点击+,同样在弹窗中选中插件工程,点击Add

此时可以看到 DependenciesLink Binary With Libraries 都添加了插件工程,如下图所示

接下来需要在插件工程的Header Search Paths中添加开发插件所需的头文件引用,头文件存放在主工程的HBuilder-Hello/inc中,添加方法如下图所示,在 Xcode 项目左侧目录选中插件工程名,找到TARGETS->Build Settings->Header Search Paths双击右侧区域打开添加窗口,然后将inc目录拖入会自动填充相对路径,然后将模式改成recursive

# 代码实现

# 插件扩展方式

原生插件是基于 DCUniPlugin 规范来实现,扩展原生功能有两种方式:

  • module:不需要参与页面布局,只需要通过 API 调用原生功能,比如:获取当前定位信息、数据请求等功能,通过扩展module的方式来实现;
  • component:需要参与页面布局,比如:mapimage等需要显示UI的功能,通过扩展component即组件的方法来实现;

您需要根据实际的情况选择扩展方式,当然插件中可以同时存在 modulecomponent,也可以是多个 module 和 多个 component

特别注意 如果需要扩展自定义的 module 或者 component ,一定注意不要将 ocruntime 暴露给 JS ,不要将一些诸如 dlopen()dlsym()respondsToSelector:performSelector:method_exchangeImplementations() 的动态和不可控的方法暴露给JS,也不要将系统的私有API暴露给JS。否则将可能面临苹果上架审核问题。

# 扩展 module

TestModule为例,源码请查看 iOSSDK/HBuilder-uniPluginDemo/DCTestUniPlugin 插件工程;

新建TestModule类,继承 DCUniModule,引入 DCUniModule.h 头文件。

TestModule.h 文件

#import <Foundation/Foundation.h>
// 引入 DCUniModule.h 头文件
#import "DCUniModule.h"

@interface TestModule : DCUniModule

@end

TestModule.h 文件截图:

然后在 TestModule.m 文件中添加实现方法

异步方法实现

/// 异步方法(注:异步方法会在主线程(UI线程)执行)
/// @param options js 端调用方法时传递的参数   支持:String、Number、Boolean、JsonObject 类型
/// @param callback 回调方法,回传参数给 js 端   支持: NSString、NSDictionary(只能包含基本数据类型)、NSNumber 类型
- (void)testAsyncFunc:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {

    // options 为 js 端调用此方法时传递的参数 NSLog(@"%@",options); // 可以在该方法中实现原生能力,然后通过 callback 回调到 js

   if (callback) {
       // 第一个参数为回传给js端的数据,第二个参数为标识,表示该回调方法是否支持多次调用,如果原生端需要多次回调js端则第二个参数传 YES;
        callback(@"success",NO);

    }
}

通过宏 UNI_EXPORT_METHOD 将异步方法暴露给 js 端,只有通过UNI_EXPORT_METHOD暴露的原生方法才能被 js 端识别到

// 通过宏 UNI_EXPORT_METHOD 将异步方法暴露给 js 端
UNI_EXPORT_METHOD(@selector(testAsyncFunc:callback:))

同步方法实现

/// 同步方法(注:同步方法会在 js 线程执行)
/// @param options js 端调用方法时传递的参数   支持:String、Number、Boolean、JsonObject 类型
- (NSString *)testSyncFunc:(NSDictionary *)options {
    // options 为 js 端调用此方法时传递的参数
    NSLog(@"%@",options);

    /*
     可以在该方法中实现原生功能,然后直接通过 return 返回参数给 js
     */

    // 同步返回参数给 js 端  支持:NSString、NSDictionary(只能包含基本数据类型)、NSNumber 类型
    return @"success";
}

通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端

// 通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端
UNI_EXPORT_METHOD_SYNC(@selector(testSyncFunc:))

TestModule.m 文件截图:

# Hook系统事件

如果需要在 App 启动时初始化或者需要获取系统的一些事件, 需要新建一个XXXXProxy类(注意命名加前缀防止冲突),继承 NSObject 遵守UniPluginProtocol协议

-(void)onCreateUniPlugin;

- (BOOL)application:(UIApplication *_Nullable)application didFinishLaunchingWithOptions:(NSDictionary *_Nullable)launchOptions;
- (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *_Nullable)deviceToken;
- (void)didFailToRegisterForRemoteNotificationsWithError:(NSError *_Nullable)err;
- (void)didReceiveRemoteNotification:(NSDictionary *_Nullable)userInfo;
- (void)didReceiveLocalNotification:(UILocalNotification *_Nullable)notification;
- (BOOL)application:(UIApplication *_Nullable)application handleOpenURL:(NSURL *_Nullable)url;
- (BOOL)application:(UIApplication *_Nullable)app openURL:(NSURL *_Nonnull)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *_Nullable)options NS_AVAILABLE_IOS(9_0);

- (void)applicationWillResignActive:(UIApplication *_Nullable)application;
- (void)applicationDidBecomeActive:(UIApplication *_Nullable)application;
- (void)applicationDidEnterBackground:(UIApplication *_Nullable)application;
- (void)applicationWillEnterForeground:(UIApplication *_Nullable)application;


- (void)applicationMain:(int)argc argv:(char * _Nullable [_Nonnull])argv;

- (BOOL)application:(UIApplication *_Nullable)application continueUserActivity:(NSUserActivity *_Nullable)userActivity restorationHandler:(void(^_Nullable)(NSArray * __nullable restorableObjects))restorationHandler API_AVAILABLE(ios(8.0));

具体示例请查看 离线SDK包HBuilder-uniPluginDemo/DCTestUniPlugin/DCTestUniPlugin/TestPluginProxy.m

Module 进阶

关于 ModuleModule 方法的执行特性(同步、异步;执行线程),需要了解:

uniExecuteThread

Module 方法默认会在 UI 线程(iOS 主线程)中被调用,建议不要在这做太多耗时的任务。

如果你的任务不需要在 UI 线程执行或需要在特定线程执行,需要实现 DCUniModule 中的 uniExecuteThread 的属性,并返回你希望方法执行所在的线程。

Module 自定义队列和线程

DCUniModule 中可以通过 uniExecuteQueue来实现自定义 queue,通过uniExecuteThread 来实现自定义的 thread

特别注意 如果同时指定了 uniExecuteQueueuniExecuteThread, 只会执行 uniExecuteQueue, uniExecuteThread将会被忽略。

如果只实现了 uniExecuteThread, 代码中注意 要线程保活

参考示例代码

    -(NSThread*)uniExecuteThread
{
    if ( nil == _uniExecuteThread) {
        _uniExecuteThread = [[NSThread alloc] initWithTarget:self selector:@selector(uniNewThread) object:nil];
        [_uniExecuteThread setName:@"TestUniModule"];
        [_uniExecuteThread start];
    }

    return _uniExecuteThread;
}

-(void)uniNewThread
{
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
        [runLoop run];

    }
}

# 配置插件信息

选中工程中的HBuilder-uniPlugin-Info.plist文件右键->Open As->Source Code找到dcloud_uniplugins节点,copy下面的内容添加到dcloud_uniplugins节点下,按您插件的实际信息填写对应的项

<dict>
	<key>hooksClass</key>
	<string>填写 hooksClass 类名 </string>
	<key>plugins</key>
	<array>
		<dict>
			<key>class</key>
			<string>填写 module 或 component 的类名</string>
			<key>name</key>
			<string>填写暴露给js端对应的 module 或 component 名称</string>
			<key>type</key>
			<string>填写 module 或 component</string>
		</dict>
	</array>
</dict>

配置说明

  • hooksClass:App系统方法钩子类,值是类名,是给有些插件需要在 app 启动时做初始化或者获取系统事件用的,如果没有可以不填为空
  • class:module 或 component 对应的原生类名(示例中为 TestModule
  • name:暴露给js端使用的 module 或 component 对应的名称(注意:module 的 name 必须以插件id为前缀或和插件id相同,示例为DCTestUniPlugin-TestModule其中 DCTestUniPlugin 为插件的id,需要保证唯一性,避免与其他插件冲突,component 的name 没有强制要求,但是也要保证唯一比如 dc-map
  • type:module 或 component (示例为module

配置完如下图所示**(必须严格按照格式配置)**:

到此,我们已经完成了一个简单的 module 扩展,接下来讲解如何在 uni-app 项目中调用刚刚扩展的 module 方法

# 在 uni-app 项目中调用 module 方法

module 支持在 vue 和 nvue 中调用,添加如下代码

<template>
	<div>
		<button type="primary" @click="testAsyncFunc">testAsyncFunc</button>
		<button type="primary" @click="testSyncFunc">testSyncFunc</button>
	</div>
</template>

<script>
	// 首先需要通过 uni.requireNativePlugin("ModuleName") 获取 module
	var testModule = uni.requireNativePlugin("DCTestUniPlugin-TestModule")
	export default {
		methods: {
			testAsyncFunc() {
				// 调用异步方法
				testModule.testAsyncFunc({
						'name': 'uni-app',
						'age': 1
					},
					(ret) => {
						uni.showToast({
							title:'调用异步方法 ' + ret,
							icon: "none"
						})
					})
			},
			testSyncFunc() {
				// 调用同步方法
				var ret = testModule.testSyncFunc({
					'name': 'uni-app',
					'age': 1
				})

				uni.showToast({
					title:'调用同步方法 ' + ret,
					icon: "none"
				})
			}
		}
	}
</script>

然后我们要生成 uni-app 项目的本地打包资源,导入到插件开发工程中,测试一下功能是否正常

# 导入 uni-app 资源

# 生成 uni-app 本地打包资源

本操作需要在 HBuilderX 中进行,需要您新建一个uni-app项目, 并编写js端代码,最后导出本地资源 右键->发现->原生App-本地打包->生成本地打包App资源

项目编译完成后会在 HBuilderX 控制台输出资源存路径,点击路径会自动打开资源所在文件夹

如下图所示,__UNI_7F5F813文件夹即为应用资源包(__UNI_7F5F813 为 uni-app 的 id)

# 导入 uni-app 资源

接下来,将应用资源导入到插件开发主工程的HBuilder-Hello/Pandora/apps/中,如下图所示,直接拖进去即可

然后打开工程的 control.xml 文件,将 appid 改成 uni-app 的 id,如下图所示

然后运行项目测试,如下图所示(能调到 module 的方法,并且可以获取 module 返回的数据,则说明功能正常)

# 再次导入资源时注意事项

:前端代码修改后重新导入资源时,需要在插件开发工程中删除之前导入的资源,同时将模拟器或真机上的 App 删除,在按照上面的教程操作,避免因为缓存问题导致加载的还是旧的资源;

接下来,我们学习一下如何扩展 Component

# 扩展组件 component

TestComponent为例,源码请查看 iOSSDK/HBuilder-uniPluginDemo/DCTestUniPlugin 插件工程;

新建TestComponent类,继承DCUniComponent类(如果这个类里什么代码也不写,它和默认的的 <view> 组件能力是一致的)

#import "DCUniComponent.h"

@interface TestComponent : DCUniComponent

@end

# 复写 DCUniComponent 中的生命周期方法

- loadView 方法

一个组件默认对应一个原生 view,如果未复写loadView方法提供自定义view,会默认调用基类方法返回一个继承于 UIView 的实例。比如我们要实现一个组件支持地图功能,我们可以返回系统的 MKMapView

**注:**不需要为view设置frameview的大小及位置由前端css决定

- (UIView *)loadView {
    return [MKMapView new];
}

- viewDidLoad

如果需要对组件view做一些配置,比如设置delegate,在 viewDidLoad 生命周期方法中是一个比较好的时机

**注:**可以直接通过 self.view 获取 view 实例

- (void)viewDidLoad {
      ((MKMapView*)self.view).delegate = self;
}

至此,已经完成了一个简单 component 的实现

然后我们需要在info.plist 中添加 component 的配置信息,和 module 配置格式一样具体请参考 配置插件信息

  • class:填写TestComponent
  • name:填写dc-testmap (在页面中使用时的组件名称,添加一个个性化的前缀,避免与其他组件冲突)
  • type:填写component

配置完后如下图所示

接下来可以在 uni-app 中使用组件

# 在 uni-app 中使用组件

注意:扩展的 component 只能在 nvue 文件中使用,不需要引入即可直接使用

在 uni-app 项目中新建 nvue 文件,加入下面的代码

<template>
    <view>
        <dc-testmap style="width:750rpx;height:300px"></dc-testmap>
    </view>
</template>

然后生成本地打包资源,导入到插件开发工程中进行测试,具体操作参考文档上方的教程 导入 uni-app 资源

然后运行测试,效果如下图:

刚刚做的组件只实现了 UI 显示,下面讲解组件的交互方式等一些高阶用法

# 自定义事件

# 对于每个组件默认提供了一些事件能力,如点击等。假如想给我们的地图组件提供 mapLoaded 事件。

uni-app 中是通过 @事件名="方法名" 添加事件,如下方代码所示在nvue中,给地图组件添加mapLoaded 事件

<template>
    <div>
        <dc-testmap style="width:750rpx;height:300px" @mapLoaded="onMapLoaded"></dc-testmap>
    </div>
</template>

<script>
export default {
    methods: {
        onMapLoaded:function(e) {
            // 原生端传递的数据保存在 e.detail 中
            console.log("map loaded: "+JSON.stringify(e.detail))
        }
    }
}
</script>
# 对应的原生端实现

我们需要添加一个 BOOL 类型成员变量 mapLoadedEvent 用来记录该事件是否生效,如下方代码所示:

/// 前端注册的事件会调用此方法
/// @param eventName 事件名称
- (void)addEvent:(NSString *)eventName {
    if ([eventName isEqualToString:@"mapLoaded"]) {
        _mapLoadedEvent = YES;
    }
}

/// 对应的移除事件回调方法
/// @param eventName 事件名称
- (void)removeEvent:(NSString *)eventName {
    if ([eventName isEqualToString:@"mapLoaded"]) {
        _mapLoadedEvent = NO;
    }
}
# 原生端向前端发送事件

在地图加载完毕的方法中触发 mapLoaded 事件

- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView {
    if (_mapLoadedEvent) {
        // 向前端发送事件,params 为传给前端的数据 注:数据最外层为 NSDictionary 格式,需要以 "detail" 作为 key 值
        [self fireEvent:@"mapLoaded" params:@{@"detail":@{@"mapLoaded":@"success"}} domChanges:nil];
    }
}

# 自定义属性

给我们的地图组件添加一个新的属性showTraffic,在前端代码里可以控制组件是否显示路况信息

<template>
    <div>
        <dc-testmap style="width:750rpx;height:300px" showTraffic="true"></dc-testmap>
    </div>
</template>
# 对应的原生端实现

覆盖组件方法 onCreateComponentWithRef... 给组件添加一个成员变量记录 showTraffic 属性的值,并在 init 方法中初始化

-(void)onCreateComponentWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events uniInstance:(DCUniSDKInstance *)uniInstance
{
    if (attributes[@"showsTraffic"]) {
        _showsTraffic = [DCUniConvert BOOL: attributes[@"showsTraffic"]];
    }
}

在生命周期方法中记得将属性值同步给地图控件

- (void)viewDidLoad {
  ((MKMapView*)self.view).showsTraffic = _showsTraffic;
}

当前端更新属性时,会触发updateAttributes:方法,同步给地图控件**

/// 前端更新属性回调方法
/// @param attributes 更新的属性
- (void)updateAttributes:(NSDictionary *)attributes {
    // 解析属性
    if (attributes[@"showsTraffic"]) {
        _showsTraffic = [DCUniConvert BOOL: attributes[@"showsTraffic"]];
        ((MKMapView*)self.view).showsTraffic = _showsTraffic;
    }
}

# 更多的组件生命期方法

组件是由框架管理的,比如创建、布局、渲染、销毁。组件的生命周期方法都是可以重写的,你可以在这些生命周期中去做自己的事情。

方法 描述
onCreateComponentWithRef:type:... 用给定的属性初始化一个component后会调用
layoutDidFinish 在component完成布局时候会调用.
loadView 创建component管理的view.
viewWillLoad 在component的view加载之前会调用.
viewDidLoad 在component的view加载完之后调用.
viewWillUnload 在component的view被释放之前调用.
viewDidUnload 在component的view被释放之后调用.
updateStyles: 在component的style更新时候调用.
updateAttributes: 在component的attribute更新时候调用.
addEvent: 给component添加event的时候调用.
removeEvent: 在event移除的时候调用.

# 给组件添加方法

原生端实现

在组件代码中使用宏 UNI_EXPORT_METHOD 暴露原生方法供前端调用

@implementation TestMapComponent

// 通过 UNI_EXPORT_METHOD 将方法暴露给前端
UNI_EXPORT_METHOD(@selector(focus:))

// options 为前端传递的参数,支持 NSDictionary 或 NSString 类型
- (void)focus:(NSDictionary *)options {
    NSLog(@"%@",options);
}
@end

在 uni-app 中调用 focus: 方法

<template>
  <dc-testmap ref='mycomponent'></dc-testmap>
</template>
<script>
  module.exports = {
    created: function() {
      // 通过 this.$refs.mycomponent 获取地图组件
      // 调用组件 focus 方法
      this.$refs.mycomponent.focus({'value':'Hello'});
    }
  }
</script>

uni-app 完整项目存放在 iOSSDK/HBuilder-uniPluginDemo/UniPluginDemo

# globalEvent 事件

modulecomponent中 用于页面监听持久性事件,例如定位信息,陀螺仪等的变化。

globalEvent事件只能通过页面的DCUniSDKInstance实例给当前页面发送globalEvent事件。其他页面无法接受。

示例:

页面监听event事件

var globalEvent = uni.requireNativePlugin('globalEvent');
globalEvent.addEventListener('myEvent', function(e) {
  console.log('myEvent'+JSON.stringify(e));
});

在原生代码 发出myEvent事件

NSDictionary * dict = [NSDictionary dictionaryWithObjectsAndKeys:@"value",@"key",nil];
NSString * eventName = @"myEvent";

DCUniSDKInstance * instance = self.uniInstance;

[instance fireGlobalEvent:eventName params:params];

插件开发完毕并通过测试后,接下来就可以生成插件包了

# 生成插件包

此步骤应该在您插件所有功能都开发完毕,并在开发工程中测试完成进行

# 插件包结构说明

|-- 插件id							// 插件包是一个以插件id命名的文件夹
	|-- android						// 存放 android 插件所需要的依赖库及资源文件
	|-- ios							// 存放 ios 插件所需要的依赖库及资源文件
	|-- package.json				// 插件配置文件

插件包的目录结构是固定的,是一个以插件id命名的文件夹,其中 android 路径中放的是 android 端插件所需要的依赖库及资源文件,ios 路径中放的是 ios 端插件所需要的依赖库及资源文件,package.json为插件的配置文件,接下来,我们需要生成 iOS 插件所需的依赖库,和编写 package.json 文件

# 编译生成插件库文件(.framework 或 .a)

如下图所示,将编译工程选择为插件项目(DCTestUniPlugin),运行设备选择Generic iOS Device

然后点击Edit Scheme...

在弹窗中,将Run->Info->Build Configuration切换到Release,然后点击Close关闭弹窗

然后在 Xcode 左侧目录中选中插件工程名,查看TARGETS->Build Settings->Architectures,确保

  • Build Active Architecture Only->ReleaseNo
  • Valid Architectures 中至少包含 arm64(一般保持工程默认配置即可)

iOS Deployment Target中选中最低支持的 iOS 版本,建议选择 iOS11.0

然后点击运行按钮Command + B 编译运行工程

编译完成后,点击顶部菜单 Product > Show Build Folder in Finder 打开编译后的路径,Release-iphoneos 中的库就是编译后的插件库文件;

# 编写 package.json 配置文件

package.json 为插件的配置文件,配置了插件id、格式、插件资源以及插件所需权限等等信息

新建一个 package.json 文件,然后请参考 uni原生插件包格式 说明,根据您插件实际情况填写插件配置信息,示例插件配置完后如下所示

{
	"name": "TestUniPlugin",
	"id": "DCTestUniPlugin",
	"version": "1.0.0",
	"description": "uni示例插件",
	"_dp_type": "nativeplugin",
	"_dp_nativeplugin": {
		"ios": {
			"plugins": [{
				"type": "module",
				"name": "DCTestUniPlugin-TestModule",
				"class": "TestModule"
			}, {
				"type": "component",
				"name": "dc-testmap",
				"class": "TestComponent"
			}],
			"frameworks": ["MapKit.framework"],
			"integrateType": "framework",
			"deploymentTarget": "9.0"
		}
	}
}

然后以插件id为名新建一个文件夹,将编辑好的 package.json 放进去,然后在文件夹中在新建一个 ios 文件夹,将刚刚生成的依赖库(DCTestUniPlugin.framework)copy 到 ios 根目录,这样我们的插件包就构建完成了,如下图所示

**注:**iOS 插件包至少需要包含:package.json文件 和ios文件夹(小写的ios)里面包含 .a 或 .framework 依赖库;

# 使用插件

# 放到 HBuilderX 的 uni-app 项目中直接使用

插件包开发完后,可以直接放到 HX 的 uni-app 项目中使用,具体请参考文档 HBuilderX 中使用本地插件

# 提交插件市场

按照上面的方式放到 HBuilderX 的 uni-app 项目中提交打包测试完插件后,可以将插件包压缩成 zip 格式,然后提交到插件市场共享给其他开发者使用,登录注册DCloud插件市场,然后按照提示步骤提交插件,(需要编写对应插件的使用说明文档,.md(markdown)格式);

# 常见问题

# Q:集成广告SDK说明

A: 由于官方 UniAD 广告组件集成了“广点通”和“穿山甲”SDK,目前不支持自行开发包含这两个SDK的原生插件,云打包会导致冲突;

# Q:插件开发需要依赖第三方SDK,或需要依赖资源文件

  • 如果您的插件需要依赖第三方的SDK,开发阶段引入三方SDK的时候要引入到主工程,然后将三方SDK提供的 .h 头文件直接添加到插件工程中这样就可以正常调用三方SDK的 API 了,功能开发完毕后在构建插件包的时候,需要将依赖的三方SDK库文件放到ios路径下,然后按照规范编辑 package.json
  • 如果需要依赖资源文件比如图片、xib资源等,建议把资源文件放到.bundle包中使用,开发阶段资源文件也要添加到主工程中,不然引用不到,功能开发完毕后在构建插件包的时候,把 bundle 包放到 "ios/BundleResources" 路径下即可,打包时会将 BundleResources 中的所有文件都添加到应用中;

具体请参考开源项目 百度OCR识别插件源码,对应插件市场的 插件

# Q: 如何跳转原生 UIViewController

A: 因为 uni 框架机制,module 的 uniInstance 没有绑定 viewController,故 uniInstance.viewController 值为 nil,如果想通过 UIViewController 来跳转页面可使用下面的方法获取 UIViewController**

// 获取当前显示的 UIViewController
+ (UIViewController *)dc_findCurrentShowingViewController {
    //获得当前活动窗口的根视图
    UIViewController *vc = [UIApplication sharedApplication].keyWindow.rootViewController;
    UIViewController *currentShowingVC = [self findCurrentShowingViewControllerFrom:vc];
    return currentShowingVC;
}
+ (UIViewController *)findCurrentShowingViewControllerFrom:(UIViewController *)vc
{
    // 递归方法 Recursive method
    UIViewController *currentShowingVC;
    if ([vc presentedViewController]) {
        // 当前视图是被presented出来的
        UIViewController *nextRootVC = [vc presentedViewController];
        currentShowingVC = [self findCurrentShowingViewControllerFrom:nextRootVC];

    } else if ([vc isKindOfClass:[UITabBarController class]]) {
        // 根视图为UITabBarController
        UIViewController *nextRootVC = [(UITabBarController *)vc selectedViewController];
        currentShowingVC = [self findCurrentShowingViewControllerFrom:nextRootVC];

    } else if ([vc isKindOfClass:[UINavigationController class]]){
        // 根视图为UINavigationController
        UIViewController *nextRootVC = [(UINavigationController *)vc visibleViewController];
        currentShowingVC = [self findCurrentShowingViewControllerFrom:nextRootVC];

    } else {
        // 根视图为非导航类
        currentShowingVC = vc;
    }

    return currentShowingVC;
}

# Q: 如何获取页面可访问图片资源存储路径 (vue页面需要注意,nvue页面不存这个问题)

A: 有些插件可能需要返回本地的图片路径给页面来显示,如果页面是vue的话是使用 WKWebview 渲染,访问本地图片资源可能会存在跨域问题访问不到图片资源,所以需要将图片存放到指定路径下,按照下面的方法获取图片存储路径即可

原生端实现

// 引用头文件
#import "PDRCoreApp.h"
#import "PDRCoreAppManager.h"
#import "PDRCoreAppInfo.h"

// 获取路径信息
PDRCoreAppInfo *appinfo = [PDRCore Instance].appManager.getMainAppInfo;
// 将图片存储到 appinfo.documentPath 路径下即可,可以创建子目录;
// 示例,原生图片存储路径为
NSString *imgPath = [appinfo.documentPath stringByAppendingPathComponent:@"test.png"];

js 端获取图片路径有两种方式

说明:"_doc" 是一个特殊字符,和原生端的 appinfo.documentPath对应

// 1.使用相对路径(直接使用 "_doc/" 拼接图片路径)
var relativePath = "_doc/test.png"

// 2.使用平台的绝对路径(先将 "_doc/" 转换成原生绝对路径在拼接图片路径)
var docPath = plus.io.convertLocalFileSystemURL("_doc/");
var absolutePath = docPath + 'test.png'

# Q: 打包报错 “Undefined symbols for architecture”

A: 这类错误基本都是缺少依赖库导致的,仔细看报错信息,查看未定义的符号属于哪个库(有可能是系统库,也有可能是第三方库)然后将缺少的库放到插件包或修改package.json 配置文件后重新提交打包;

# Q: 依赖库冲突 “duplicate symbols for ...”

A:如果您依赖的三方库与SDK依赖的三方库冲突

  • 为了保证SDK功能的完整性所以请您使用SDK内置的三方库,移除您依赖的三方库,对于源码开源的三方库比如 SDWebImage、ZXing等,这些库的 .h 头文件存放在 SDK/inc 路径中的,将对应库的头文件引入到插件工程中使用即可,如果您使用内置的三方库导致原生功能异常,请反馈给我们;
  • 如果引入的是 .a 或 .framework 库里面包含某个三方库导致符号定义冲突,可根据 这篇文档 移除对应库的符号,然后测试一下功能是否正常;

依赖的三方库版本说明

# Q: 之前引用的头文件和资源 现在找不到了

A:为了解决部分功能库引用了一些第三方库 可能与开发者项目的第三方库重复引用 我们从3.0.7后 对于部分库和资源进行了调整。

如果开发者原来工程对资源文件有引用,现在找不到资源文件,请参考下边列表(文件路径去离线SDK下的 SDK/Bundles/下寻找)

旧资源 新资源
TZImagePickerController.bundle DCTZImagePickerController.bundle
SVProgressHUD.bundle DCSVProgressHUD.bundle

如果开发者原来工程对库的头文件有引用,现在找不到头文件,请参考下边列表(文件路径去离线SDK下的 SDK/inc/下寻找)

TZImagePickerController -> DCTZImagePickerController

旧头文件 新头文件
TZAssetCell.h DCTZAssetCell.h
TZAssetModel.h DCTZAssetModel.h
TZGifPhotoPreviewController.h DCTZGifPhotoPreviewController.h
TZImageCropManager.h DCTZImageCropManager.h
TZImageManager.h DCTZImageManager.h
TZImagePickerController.h DCTZImagePickerController.h
TZLocationManager.h DCTZLocationManager.h
TZPhotoPickerController.h DCTZPhotoPickerController.h
TZPhotoPreviewCell.h DCTZPhotoPreviewCell.h
TZPhotoPreviewController.h DCTZPhotoPreviewController.h
TZProgressView.h DCTZProgressView.h
TZVideoPlayerController.h DCTZVideoPlayerController.h
UIViewControllerHUD.h DCUIViewControllerHUD.h
NSBundle+TZImagePicker.h NSBundle+DCTZImagePicker.h
UIView+Layout.h UIView+DCLayout.h

SVProgressHUD -> DCSVProgressHUD

旧头文件 新头文件
SVIndefiniteAnimatedView.h DCSVIndefiniteAnimatedView.h
SVProgressAnimatedView.h DCSVProgressAnimatedView.h
SVProgressHUD.h DCSVProgressHUD.h
SVRadialGradientLayer.h DCSVRadialGradientLayer.h

SocketRoket -> DCSocketRoket

旧头文件 新头文件
SRWebSocket.h DCSRWebSocket.h

# Q: 升级后使用推送插件收不到消息

A: 为了应对苹果审核规范,我们从3.6.2后,对于推送相关代理事件进行了调整,使用第三方推送插件时,云打包需要勾选Push模块,离线SDK需要引入liblibPush.a