因为实习的原因,已经好久没有写博客了。倒不是因为忙,而是因为每天都被业务代码填满,找不到很好的可以拿来写博客的素材。
我的公司是一家小公司,我一个人做Android开发,然后某天我技术主管让我学学React Native,顺带把iOS也接过去。
于是我踏上了一条不归路......
原来的项目是原生的iOS项目,在完全不懂iOS开发的情况下,靠着官方文档,以及各种教程,iOS接入也算是顺利。但是在原生与RN通信这块地方花了不少时间。因为原本公司的iOS项目是Swift写的,我们技术主管让我需要原生部分实现的地方都用Swift给他写(技术主管就是iOS开发)。
但是官网上的例子都是OC实现的,有关Swift的导出就一小部分介绍而已......网上的博客也是抄来抄去,毫无参考价值。
可能是我不懂iOS开发,也可能是因为我蠢,但为了这些和我一样蠢的人将来能少花点时间,我决定写这篇博客。
跳过各种如何接入啥啥的步骤,我们直接从iOS原生与RN如何通信开始(介绍不会全面,只介绍目前我项目中用的几种通信):
RN调用iOS原生方法
这部分官网是有教程的,也包括如何用Swift实现,步骤如下:
1:新建一个IOSIntentModule.h文件:
#import #import @interface IOSIntentModule : NSObject @end
2:新建一个IOSIntentModule.m文件:
#import "IOSIntentModule.h" @implementation IOSIntentModule.h RCT_EXPORT_MODULE(); RCT_EXPORT_METHOD(test:(NSString *)name) { NSLog(name); } @end
3:在RN中调用:
import { NativeModules } from 'react-native'; NativeModules. IOSIntentModule.test('Hello');
当你执行到“NativeModules. IOSIntentModule.test('Hello');” 这句的时候,就会去调用原生的test方法。然后会在控制台打印”Hello“。
“NativeModules”是RN里和原生通信的一个组件。“IOSIntentModule”是原生导出给RN调用的组件的名字,这个名字是在步骤2中,由“RCT_EXPORT_MODULE();” 这个宏指定的。
比如你这样写:“RCT_EXPORT_MODULE(iAmSoHandsome);” 。
那你就得这么调用:
“NativeModules. iAmSoHandsome.test('Hello');”。
如果你不写,那这个名字就和你的类名一样,在本例中就是“ IOSIntentModule”。
“RCT_EXPORT_METHOD()”这个宏是用来将原生的方法导出,只有用这个宏包裹的方法,才可以被RN调用。
那这样的话,其实是RN调用原生的OC方法了,如果我们想用Swift来写怎么办呢?
官网介绍,Swift不支持宏。所以我们还是得靠OC来当作一个桥梁,也就是RN——>OC——>Swift。RN调用原生的OC方法,然后OC在交给Swift去处理。具体步骤如下:
1:新建一个IOSIntentModule.swift文件:
@objc(IOSIntentModule) class IOSIntentModule: NSObject { @objc func test(name: String) -> Void { print(name) } }
2:修改一下我们的IOSIntentModule.m文件:
#import @interface RCT_EXTERN_MODULE(IOSIntentModule, NSObject) RCT_EXTERN_METHOD(test:(NSString *)name) @end
3:在桥接文件中引入RCTBridgeModule.h:
一旦你在项目混用OC和Swift两种语言,那就需要这个桥接文件。一般在项目的Supporting Files文件夹里(一般系统会自动生成的,叫做“项目名-Bridging-Header.h”,项目名就是你工程的项目名)。
在里面加上:
#import
然后你就可以在RN中通过“NativeModules. IOSIntentModule.test('Hello');”调用了。虽然看不懂iOS代码,但是我猜测应该是RN先调用OC的方法,然后OC再去调用Swift的方法。
这样你照猫画虎的多写几个方法,然后就可以愉快的用Swift调用啦。
RN调用iOS原生方法并回调
有时候我们并不想简单的调用方法,让他执行完毕就OK了这么简单。我们需要接收他们的返回值啥的,这个时候我们就需要一个回调。
比如进行图片选择的时候,需要RN调用原生的图片选择器,然后将图片地址返回。
回调的方式有两种,一种是callback,一种是Promise,个人比较喜欢Promise,所以下面以Promise为例子(callback方法其实一样的,可以看看官网例子):
1:我们修改最开始的IOSIntentModule.m文件:
#import "IOSIntentModule.h" @implementation IOSIntentModule.h RCT_EXPORT_MODULE(); RCT_EXPORT_METHOD(test:(NSString *)name resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { resolve(name); } @end
在这里我们增加了两个参数。只要在方法参数的最后面(注意是最后面)加上“ resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject”这两个参数,就会产生一个Promise对象。
然后你就可以用resolve("信息")来返回正常信息。用reject("错误信息")来返回错误信息。
2:然后你就可以在RN中这样来接收这个返回值:
NativeModules.IOSIntentModule.test().then(msg => { //msg就是你用resolve返回的值 }).catch(error => { //error就是你用reject返回的值 });
但是这样的话,是用OC写的,我们如何用Swift来完成呢?
很简单,你在OC的test方法里,调用Swift的方法,然后接收Swift方法的返回值,再将其返回就可以了。那这里就涉及一个问题,如何在OC里调用Swift的方法呢?
OC调用Swift方法
这部分内容其实百度就可以知道了,具体步骤如下:
1:进入你项目的Build Settings里,将Defines Module设置为YES。
设置 Product Module Name ,也可以不设置,默认为工程的名字。
2:编写一个Swift类,就叫做IOSIntentModuleSwift.swift好了:
public class IOSIntentModuleSwift :NSObject{ public func testSwift(_ name:String) -> String { return name } }
注意这个类一定要继承NSObject,不然OC会找不到这个类。
还有就是参数最前面要加上“ _ ”,不然会报参数不匹配的错误(百度了一下说是swift3 的一个语法,参数必须有名字,但是从RN传过来的参数没有名字啥啥的,不懂iOS开发......总之加上“ _ ”就行了)。
3:在IOSIntentModule.m中添加头文件:“项目名-Swift.h” 。这个项目名就是之前让你设置的Product Module Name ,没设置的话就是你的工程名。
4:然后你就可以在OC中调用Swift了,就像这样子:
#import "IOSIntentModule.h" #import "项目名-Swift.h" @implementation IOSIntentModule.h RCT_EXPORT_MODULE(); RCT_EXPORT_METHOD(test:(NSString *)name resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { IOSIntentModuleSwift* intentSwift = [[IOSIntentModuleSwift alloc] init]; NSString* str = [intentSwift testSwift:name]; resolve(str); } @end
这样当调用到OC的test方法时,就会去执行Swift的testSwift方法,然后testSwift将其接收到的参数“name”返回,然后OC接收到Swift方法的返回值,再将其返回给RN。
正常来说这样就行了,但是现实总会有奇奇怪怪的事情发生。比如我公司的项目......加了“项目名-Swift.h”这个头文件后,总是编译不过去,说“项目名-Swift.h”里的一个@import xxxxx找不到。
解决办法也很简单。其实“项目名-Swift.h”就是一个桥接文件,里面的内容大概长这样:
SWIFT_CLASS("_TtC6alltuu20IOSIntentModuleSwift") @interface IOSIntentModuleSwift : NSObject - (NSDictionary * _Nonnull)pickImage SWIFT_WARN_UNUSED_RESULT; - (NSString * _Nonnull)reactUplaodPicToOSS:(NSString * _Nonnull)bucket albumSetId:(NSString * _Nonnull)albumSetId objectkey:(NSString * _Nonnull)objectkey filePath:(NSString * _Nonnull)filePath contentId:(NSInteger)contentId type:(NSInteger)type SWIFT_WARN_UNUSED_RESULT; - (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER; @end
你可以自己新建一个比如说“IOSIntentModule-Swift.h”文件,然后把“项目名-Swift.h”文件的东西拿出来,去掉报错的地方,粘贴进去。然后在”IOSIntentModule.m“里直接“#import IOSIntentModule-Swift.h”就可以了。
iOS原生主动给RN发送事件
这一部分我卡了好久......后来我仔仔细细的看了下官网,发现有一句不起眼的话......
大家看最后一句,通过bridge向JS发送事件。
然后我尝试了一下,发现RCTRootView里就有一个bridge,这个bridge里有个eventDispatcher(),然后这个eventDispatcher()里就有三个发送事件的方法(虽然说是快过时了,但是可以用,而且用起来简单)
代码类似于这样子:
(ctrl.view as! RCTRootView).bridge.eventDispatcher().sendDeviceEvent(withName: "refresh", body: "Hello")
这边我们用的是sendDeviceEvent()方法,还有另外两种,不知道啥区别......大家可以自行百度。
其中withName:"refresh"表示你要发送的事件的名字,在RN中监听的时候,也要监听这个名字。body:"Hello"就是你要发送的数据。
然后你就可以在RN中监听这个事件:
//组件渲染之前调用此方法 componentWillMount() { this.subscription = DeviceEventEmitter.addListener('refresh', function(msg) { alert(msg); }); } componentWillUnmount() { // 移除监听器 this.subscription.remove(); }
我们监听了refresh事件,当iOS原生那边发送的时候,就会被RN接收到。msg就是iOS原生那边的body参数。
结束
虽然没有全面将iOS原生与RN通信的方式全部写完。但是我觉得有这三个例子,看看官网,举一反三应该没啥问题。而且我们的重点是如何用Swift来实现通信的逻辑。
还有就是有问题多看看官网...逐字逐句的那种,官网超坑的(也可能是我太垃圾)。
那就这样结束了,才疏学浅,有不对的地方,还请大家批评指正。
最后
感谢我可爱的女朋友。