ReactNative與iOS的互動

BulldogX發表於2018-12-20

本文簡要展示RN與iOS原生的互動功能。

1.1 RCTRootView初始化問題

/**
 * - Designated initializer -
 */
- (instancetype)initWithBridge:(RCTBridge *)bridge
                    moduleName:(NSString *)moduleName
             initialProperties:(NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;

/**
 * - Convenience initializer -
 * A bridge will be created internally.
 * This initializer is intended to be used when the app has a single RCTRootView,
 * otherwise create an `RCTBridge` and pass it in via `initWithBridge:moduleName:`
 * to all the instances.
 */
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                       moduleName:(NSString *)moduleName
                initialProperties:(NSDictionary *)initialProperties
                    launchOptions:(NSDictionary *)launchOptions;
複製程式碼

1、當Native APP內只有一處RN的入口時,可以使用initWithBundleURL,否則的話就要使用initWithBridge方法。
2、因為initWithBundleURL會在內部建立一個RCTBridge,當有多個RCTRootView入口時,就會存在多個RCTBridge,容易導致Native端與RN互動時多次響應,出現BUG。

1.2 建立自定義的RNBridgeManager

由於APP內有RN多入口的需求,所以共用一個RCTBridge

RNBridgeManager.h


#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNBridgeManager : RCTBridge
/**
 RNBridgeManager單例
 */
+ (instancetype)sharedManager;

@end

NS_ASSUME_NONNULL_END

複製程式碼

RNBridgeManager.m

#import "RNBridgeManager.h"

#import <React/RCTBundleURLProvider.h>

//dev模式下:RCTBridge required dispatch_sync to load RCTDevLoadingView Error Fix
#if RCT_DEV
#import <React/RCTDevLoadingView.h>
#endif
/**
 自定義類,實現RCTBridgeDelegate
 */
@interface BridgeHandle : NSObject<RCTBridgeDelegate>

@end

@implementation BridgeHandle

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge{
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
}
@end


@implementation RNBridgeManager

+ (instancetype)sharedManager{
    static RNBridgeManager *manager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[RNBridgeManager alloc] initWithDelegate:[[BridgeHandle alloc] init] launchOptions:nil];
#if RCT_DEV
        [manager moduleForClass:[RCTDevLoadingView class]];
#endif
    });
     return manager;
}


@end

複製程式碼

1.3 Native進入RN頁面

 RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:[RNBridgeManager sharedManager] moduleName:@"RNTest" initialProperties:nil];
 UIViewController *vc = [[UIViewController alloc] init];
 vc.view = rootView;
 [self.navigationController pushViewController:vc animated:YES];
複製程式碼

1.4 RN呼叫Native方法

  • 建立一個互動的類,實現<RCTBridgeModule>協議;
  • 固定格式:在.m的實現中,首先匯出模組名字RCT_EXPORT_MODULE();RCT_EXPORT_MODULE接受字串作為其Module的名稱,如果不設定名稱的話預設就使用類名作為Modul的名稱;
  • 使用RCT_EXPORT_METHOD匯出Native的方法;

1.4.1 比如我們匯出Native端的SVProgressHUD提示方法:

RNInterractModule.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNInterractModule : NSObject<RCTBridgeModule>

@end

NS_ASSUME_NONNULL_END

複製程式碼

RNInterractModule.m

import "RNInterractModule.h"
#import "Util.h"
#import <SVProgressHUD.h>

@implementation RNInterractModule
////RCT_EXPORT_MODULE接受字串作為其Module的名稱,如果不設定名稱的話預設就使用類名作為Modul的名稱
RCT_EXPORT_MODULE();

//==============1、提示==============
RCT_EXPORT_METHOD(showInfo:(NSString *) info){
    dispatch_sync(dispatch_get_main_queue(), ^{
        [SVProgressHUD showInfoWithStatus:info];
    });
}
@end

複製程式碼

1.4.2 RN端呼叫匯出的showInfo方法:

我們在RN端把Native的方法通過一個共同的utils工具類引入,如下


import { NativeModules } from 'react-native';

//匯出Native端的方法
export const { showInfo} = NativeModules.RNInterractModule;
複製程式碼

具體的RN頁面使用時:

import { showInfo } from "../utils";

//通過Button點選事件觸發
<Button
	title='1、呼叫Native提示'
	onPress={() => showInfo('我是原生端的提示!')}
/>
複製程式碼

呼叫效果:

image

1.4.3 RN回撥Native

RN文件顯示,目前iOS端的回撥還處於實驗階段

我們提供一個例子來模擬:目前的需求是做麵包,RN端能提供麵粉,但是不會做,Native端是有做麵包的功能;所以我們需要先把麵粉,傳給Native端,Native加工好麵包之後,再通過回撥回傳給RN端。

Native端提供方法

// 比如呼叫原生的方法處理圖片、視訊之類的,處理完成之後再把結果回傳到RN頁面裡去
//TODO(RN文件顯示,目前iOS端的回撥還處於實驗階段)
RCT_EXPORT_METHOD(patCake:(NSString *)flour successBlock:(RCTResponseSenderBlock)successBlock errorBlock:(RCTResponseErrorBlock)errorBlock){
    __weak __typeof(self)weakSelf = self;
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSString *cake = [weakSelf patCake:flour];
        //模擬成功、失敗的block判斷
        if([flour isKindOfClass:[NSString class]]){
            successBlock(@[@[cake]]);//此處引數需要放在陣列裡面
        }else{
            NSError *error = [NSError errorWithDomain:@"com.RNTest" code:-1 userInfo:@{@"message":@"型別不匹配"}];
            errorBlock(error);
        }
    });
}


//使用RN端傳遞的引數字串:"",呼叫Native端的做麵包方法,加工成麵包,再回傳給RN
- (NSString *)patCake:(NSString *)flour{
    NSString * cake = [NSString stringWithFormat:@"使用%@,做好了:??????",flour];
    return cake;
}
複製程式碼

RN端呼叫:

//首先工具類裡先引入
export const { showInfo,patCake } = NativeModules.RNInterractModule;


//具體頁面使用
<Button
	title='4、回撥:使用麵粉做蛋糕'
	onPress={() => patCake('1斤麵粉',
		(cake) => alert(cake),
		(error) => alert('出錯了' + error.message))}
	/>
複製程式碼

呼叫效果:

image

1.4.4 使用Promise回撥

Native端提供方法


RCT_EXPORT_METHOD(callNameTointroduction:(NSString *)name resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock) reject){
    __weak __typeof(self)weakSelf = self;
    dispatch_sync(dispatch_get_main_queue(), ^{
        if ([name isKindOfClass:NSString.class]) {
            resolve([weakSelf introduction:name]);
        }else{
            NSError *error = [NSError errorWithDomain:@"com.RNTest" code:-1 userInfo:@{@"message":@"型別不匹配"}];
            reject(@"class_error",@"Needs NSString Class",error);
        }
    });
}

- (NSString *)introduction:(NSString *)name{
    return [NSString stringWithFormat:@"我的名字叫%@,今年18歲,喜歡運動、聽歌...",name];
}
複製程式碼

RN端呼叫:

//首先工具類裡先引入
export const { showInfo,patCake, callNameTointroduction} = NativeModules.RNInterractModule;

//具體頁面使用
<Button
	title='5、Promise:點名自我介紹'
	onPress={
		async () => {
			try {
				let introduction = await callNameTointroduction('小明');
				showInfo(introduction);
			} catch (e) {
				alert(e.message);
			}
		}
	}
/>

複製程式碼

呼叫效果:

image

1.5 Native端傳送通知到RN

Native端繼承RCTEventEmitter,實現傳送RN通知類:

RNNotificationManager.h


#import <Foundation/Foundation.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNNotificationManager : RCTEventEmitter

+ (instancetype)sharedManager;

@end

NS_ASSUME_NONNULL_END

複製程式碼

RNNotificationManager.m


#import "RNNotificationManager.h"

@implementation RNNotificationManager
{
    BOOL hasListeners;
}


+ (instancetype)sharedManager{
    static RNNotificationManager *manager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
        manager = [[self alloc] init];
    });
    return manager;
}

- (instancetype)init{
    self = [super init];
    if (self) {
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center removeObserver:self];
        [center addObserver:self selector:@selector(handleEventNotification:) name:@"kRNNotification_Login" object:nil];
        [center addObserver:self selector:@selector(handleEventNotification:) name:@"kRNNotification_Logout" object:nil];
    };
    return self;
}


RCT_EXPORT_MODULE()

- (NSArray<NSString *> *)supportedEvents{
    return @[
             @"kRNNotification_Login",
             @"kRNNotification_Logout"
             ];
}
//優化無監聽處理的事件
//在新增第一個監聽函式時觸發
- (void)startObserving{
    //setup any upstream listenerse or background tasks as necessary
    hasListeners = YES;
    NSLog(@"----------->startObserving");
}

//will be called when this mdules's last listener is removed,or on dealloc.
- (void)stopObserving{
    //remove upstream listeners,stop unnecessary background tasks.
    hasListeners = NO;
    NSLog(@"----------->stopObserving");
}

+ (BOOL)requiresMainQueueSetup{
    return YES;
}

- (void)handleEventNotification:(NSNotification *)notification{
    if (!hasListeners) {
        return;
    }
    
    NSString *name = notification.name;
    NSLog(@"通知名字-------->%@",name);
    [self sendEventWithName:name body:notification.userInfo];
    
}

@end

複製程式碼

RN端註冊監聽:

//utils工具類中匯出
export const NativeEmitterModuleIOS = new NativeEventEmitter(NativeModules.RNNotificationManager);


//具體頁面使用
import { NativeEmitterModuleIOS } from "../utils";

export default class ActivityScene extends Component {

	constructor(props) {
		super(props);
		this.subscription = null;
		this.state = {
			loginInfo: '當前未登入',
		};
	}

	updateLoginInfoText = (reminder) => {
		this.setState({loginInfo: reminder.message})
	};

	//新增監聽
	componentWillMount() {
		this.subscription = NativeEmitterModuleIOS.addListener('kRNNotification_Login', this.updateLoginInfoText);
		
	}
	//移除監聽
	componentWillUnmount() {
			console.log('ActivityScene--------->', '移除通知');
			this.subscription.remove();
	}
	render() {
		return (
			<View style={{flex: 1, backgroundColor: 'white'}}>
				<Button
					title='3、RN Push到Native 傳送通知頁面'
					onPress={() => pushNative(RNEmitter)}
				/>
				<Text style={{fontSize: 20, color: 'red', textAlign: 'center',marginTop:50}}>{this.state.loginInfo}</Text>
			</View>
		);
	}
}
複製程式碼

效果展示:

image

1.6 完整Demo(包含iOS & Android)

RN-NativeTest

相關文章