封装原生UI控件给RN调用
前言
前些日子在做项目的时候,接到一个需求:在APP上,点击一个Cell,跳转到拍照页面进行拍照。按理来说,这个需求实现起来,并不困难,第一想法是想直接用 UIImagePickerController 来实现这个需求。后面了解到,UIImagePickerController 实现后的效果是,只能拍一张图片,而不能像系统自带的相机那样,一直拍摄下去。所以又想着说那就直接跳到系统相机去吧!真被我的机智所感动,后面经过一番Google和百度之后,发现,我还是得自己写一个拍照页面。这个时候问题就来了,由于我们的APP是以RN为主的所以需要对原生的功能模块进行一个桥接。这个时候呢,又是通过一整Google和百度,发现,都是一些简单的页面封装。因此也正好借这个机会,写一下自己封装原生UI,以便给需要的童鞋看看,也可以当做自己的一个笔记,回顾一下。OK,废话不多说,我们直接开始动手干吧!
首先,我们需要准备好我们的RN项目,以及要封装的视图。在这里,我先假设各位童鞋都已经有了编写RN项目经验的,如果没有,那就请移步到 React Native中文网 看教程了。
现在以我的项目为例:
首先在我们的Xcode文件中,新建一个 View 用来实现我们要封装的控件功能。我这里取名为 TakePhotoView。然后经过一番编码(具体功能自己实现)之后,TakePhotoView 就可以为我所用。
然后这个时候呢,我们需要再创建一个 View 继承于 RCTViewManager。在我的项目中,我取名为: TakePhotoManager 。这个文件的作用是用来桥接 RN 层于原生层的通信。
TakePhotoManager.h 文件中的代码如下:
//// TakePhotoManager.h// W00_PRO//// Created by CCYQ on /3/22.// Copyright © Facebook. All rights reserved.////#import "RCTViewManager.h"#import <React/RCTViewManager.h>@interface TakePhotoManager : RCTViewManager<RCTBridgeModule>@end
这个时候,我们需要对 TakePhotoManager.m 文件进行一些编码,来实现我们的桥接功能。 TakePhotoManager.m 页面的代码如下:
//// TakePhotoManager.m// W00_PRO//// Created by CCYQ on /3/22.// Copyright © Facebook. All rights reserved.//#import "TakePhotoManager.h"#import "TakePhotoView.h"@implementation TakePhotoManager// 标记宏(必要)RCT_EXPORT_MODULE()- (UIView *)view {TakePhotoView *takePhotoView = [[TakePhotoView alloc] init];return takePhotoView;}@end
然后在 RN 的项目中,我们新建一个页面,用来承接这个 TakePhotoManager。在 RN 层中,我们新建一个页面,叫做 TakePhotoiOS.js。在这个页面中,我们需要引入在 原生层中暴露出来的 TakePhoto 页面。所以, TakePhotoiOS.js的代码如下:
### TakePhotoiOS.jsimport React, { Component } from 'react';import {AppRegistry,StyleSheet,Text,View,requireNativeComponent,NativeModules,} from 'react-native';// 该方法将 原生层TakePhotoManager 中 return 出来的 View 赋值给 RNTakePhoto。这个时候 RNTakePhoto 就是我们在原生中中的页面了。// requireNativeComponent() 该方法中有两个参数,第一个是原生层暴露的UIView,另一个是在RN层要承接的 Class。在这里我们可以看到,原生层暴露的UIView的文件叫做 TakePhotoManager,而在这里用的话,只用TakePhoto。这只能说明,原生层的封装需要按照一定的规则来做。const RNTakePhoto = requireNativeComponent('TakePhoto', TakePhotoiOS);class TakePhotoiOS extends Component {constructor(props) {super(props);}render() {return (<RNTakePhotostyle={styles.container}/>);}}const styles = StyleSheet.create({container: {flex: 1,backgroundColor: 'transparent',},});module.exports = TakePhotoiOS;
到了这一步看似我们的封装工作就完成了。运行一遍,发现没有报错,页面也是正常跳转的;但是呢在这个时候,我们发现,相机的视图是一片空白,并没有按照我们想象中的,出现预览图。这个时候我们检查了代码之后,发现,原来在页面将要加载的时候,我们需要让我们的相机开始工作。那么这个时候,我们就需要在 TakePhotoiOS.js 文件中增加如下代码:
// 视图加载完成componentDidMount() {//需要启动相机}// 视图将要消失componentWillUnmount() {//需要关闭相机}
这个时候问题就来了,我们该怎么暴露我们的方法给 RN 层调用呢?很简单,这个时候我们先在我们的 TakePhotoView.h 文件中,暴露两个方法。代码如下:
//// TakePhotoView.h// W00_PRO//// Created by CCYQ on /3/22.// Copyright © Facebook. All rights reserved.//#import <UIKit/UIKit.h>@interface TakePhotoView : UIView// 初始化- (instancetype)init;// 相机开始工作- (void)camareStartRunning;// 相机停止工作- (void)camareStopRunning;
然后在 TakePhotoView.m 文件中实现这个两个方法,代码如下:
// 相机开始工作- (void)camareStartRunning{[self.captureSession startRunning];}// 相机停止工作- (void)camareStopRunning {[self.captureSession stopRunning];}
这个时候呢,我们 相机开始工作以及停止工作的两个两个方法都实现了,现在就需要将这两个方法暴露给 RN层那边调用。这个时候,我们需要修改我们的 TakePhotoManager.m 文件,增加一些代码,让我们的RN层能调用到我们露出来的方法。 TakePhotoManager.m的代码改为如下:
//// TakePhotoManager.m// W00_PRO//// Created by CCYQ on /3/22.// Copyright © Facebook. All rights reserved.//#import "TakePhotoManager.h"#import "TakePhotoView.h"@interface TakePhotoManager()@property (nonatomic, strong) TakePhotoView *takePhotoView;@end@implementation TakePhotoManager// 标记宏(必要)RCT_EXPORT_MODULE()- (UIView *)view {_takePhotoView = [[TakePhotoView alloc] init];return _takePhotoView;}/*** 导出方法 * 相机开始工作*/RCT_EXPORT_METHOD(camareStartRunning) {[_takePhotoView camareStartRunning];}/*** 导出方法 * 相机停止工作*/RCT_EXPORT_METHOD(camareStopRunning) {[_takePhotoView camareStopRunning];}@end
既然如此,那我们RN层的 TakePhotoiOS.js 也需要做相应的变化,代码如下:
import React, { Component } from 'react';import {AppRegistry,StyleSheet,Text,View,requireNativeComponent,NativeModules,Dimensions,} from 'react-native';import NavOutView from '../../../components/NavOutView';import { Actions } from 'react-native-router-flux';import { i18n } from "../../../config";// 该方法将 原生层TakePhotoManager 中 return 出来的 View 赋值给 RNTakePhoto。这个时候 RNTakePhoto 就是我们在原生中中的页面了。// requireNativeComponent() 该方法中有两个参数,第一个是原生层暴露的UIView,另一个是在RN层要承接的 Class。在这里我们可以看到,原生层暴露的UIView的文件叫做 TakePhotoManager,而在这里用的话,只用TakePhoto。这只能说明,原生层的封装需要按照一定的规则来做。const RNTakePhoto = requireNativeComponent('TakePhoto', TakePhotoiOS);// 通过该方法,我们可以拿到 TakePhotoManager.js 中暴露出来的方法const TakePhotoManager = NativeModules.TakePhotoManager;class TakePhotoiOS extends Component {constructor(props) {super(props);}componentDidMount() {/** 这里采用延迟250毫秒后调用相机开启的方法是,因为在* 原生层中,TakePhotoView 被创建之后,才能调用 相机开启* 也等同于要 RNTakePhoto 被创建之后,才能调用*/this.timeOutReFresh = setTimeout(() => {TakePhotoManager.camareStartRunning();}, 250);}componentWillUnmount() {// 相机停止工作TakePhotoManager.camareStopRunning();}render() {return (<RNTakePhotostyle={styles.container}/>);}}const styles = StyleSheet.create({container: {flex: 1,backgroundColor: 'transparent',},});module.exports = TakePhotoiOS;
到了这个时候,我们就可以发现我们的相机已经可以工作了。然后这个时候问题又来了,由于我是将相机视图是做成全屏幕的,所以这个时候我也是将返回事件放在了原生的视图中(总之,怎么作死怎么来)。那么问题来了,当我点击了关闭按钮之后,在我的RN层中,要怎么知道我已经点了关闭了呢?这里有两个解决方法:一是直接把关闭按钮做到RN层中,直接在RN层中调用视图返回,关闭相机等方法。二是在原生层中,将关闭按钮的点击事件给暴露出来,然后在RN层中,监听并做相应的处理。这里采用的是第二种方式。于是乎,我们又需要对我们的代码进行改动了。
首先,我们可以先想到,要将 TakePhotoView 中的点击事件方法传出来,可以用到代理,通知,block等方法,这个地方我采用了block。所以在 TakePhotoView.h 文件中,我们需要增加block的声明,代码如下:
//// TakePhotoView.h// W00_PRO//// Created by CCYQ on /3/22.// Copyright © Facebook. All rights reserved.//#import <UIKit/UIKit.h>@interface TakePhotoView : UIView// 关闭按钮的blocktypedef void(^onTouchBackBlock)(NSDictionary *dicBlock);@property (nonatomic, copy) onTouchBackBlock onTouchBackBlock;// 初始化- (instancetype)init;// 相机开始工作- (void)camareStartRunning;// 相机停止工作- (void)camareStopRunning;@end
在 TakePhotoView.m 文件中,我们需要在关闭按钮的点击事件中,添加上我们的block,添加如下代码:
// 关闭按钮的点击事件- (void)btnCloseAction:(UIButton *)sender {//移除所有的通知[self removeNotification];// 相机停止工作[self camareStartRunning];// 实现block_onTouchBackBlock(@{@"message": @"goBack"});}
这个时候,我们还需要在 TakePhotoManager 文件中,将我们的 block 暴露过去给我们的 RN 层调用,那么这个时候我们需要在 TakePhotoManager.m 文件中,增加一个RN 层的 block, 用于将我们 TakePhotoView 的点击回调传递过去,代码如下:
//// TakePhotoManager.m// W00_PRO//// Created by CCYQ on /3/22.// Copyright © Facebook. All rights reserved.//#import "TakePhotoManager.h"#import "TakePhotoView.h"#import <Photos/PHPhotoLibrary.h>#import <AVFoundation/AVCaptureDevice.h>#import <AVFoundation/AVMediaFormat.h>@interface TakePhotoManager()@property (nonatomic, strong) TakePhotoView *takePhotoView;// 点击返回的block@property (nonatomic, copy) RCTBubblingEventBlock onTouchBackBlock;@end@implementation TakePhotoManager// 标记宏(必要)RCT_EXPORT_MODULE()// 事件的导出RCT_EXPORT_VIEW_PROPERTY(onTouchBackBlock, RCTBubblingEventBlock)- (UIView *)view {_takePhotoView = [[TakePhotoView alloc] init];_takePhotoView.onTouchBackBlock = ^(NSDictionary *dicBlock) {};return _takePhotoView;}/*** 相机开始工作*/RCT_EXPORT_METHOD(camareStartRunning) {[_takePhotoView camareStartRunning];}/*** 相机停止工作*/RCT_EXPORT_METHOD(camareStopRunning) {[_takePhotoView camareStopRunning];}@end
然后我们需要在 RN层中 的文件中,增加如下代码,来实现点击事件的传递,代码如下:
import React, { Component } from 'react';import {AppRegistry,StyleSheet,requireNativeComponent,NativeModules,} from 'react-native';import NavOutView from '../../../components/NavOutView';import { Actions } from 'react-native-router-flux';import { i18n } from "../../../config";const RNTakePhoto = requireNativeComponent('TakePhoto', TakePhotoiOS);const TakePhotoManager = NativeModules.TakePhotoManager;class TakePhotoiOS extends Component {constructor(props) {super(props);}componentDidMount() {/** 这里采用延迟250毫秒后调用相机开启的方法是,因为在* 原生层中,TakePhotoView 被创建之后,才能调用 相机开启* 也等同于要 RNTakePhoto 被创建之后,才能调用*/this.timeOutReFresh = setTimeout(() => {TakePhotoManager.camareStartRunning();}, 250);}componentWillUnmount() {TakePhotoManager.camareStopRunning();}render() {return (<RNTakePhotostyle={styles.container}onTouchBackBlock={(event) => {console.log(event.nativeEvent);const eventMessage = event.nativeEvent;console.log(eventMessage.message);if (eventMessage.message === 'goBack') {Actions.pop();}}}/>);}}const styles = StyleSheet.create({container: {flex: 1,backgroundColor: 'transparent',},});module.exports = TakePhotoiOS;
到这里,我们的一个简单的视图封装就搞定了,这个时候,基本上我们的功能也就已经实现完了。然后我们需要对我们的这个功能进行进一步的优化。首先,当我们在拍照的时候,我们需要判断用户是否授权了我们使用相机,以及拍完照片后,是否允许我们拍完之后,将照片写入相册中。当用户拒绝了我们的授权时,我们应该给予提示,并将引导用户跳转到权限开启的页面中。一想到这里,发现我们的工作量还是只是做到了一半而已。那么我们继续来优化吧,首先,我们先判断我们是否有权限去使用相机或者相册吧。由于这个权限的判断比较简单,我们可以直接在 TakePhotoManager 中封装一个方法给予 RN层调用,于是乎,我们可以再 TakePhotoManager.m 中写入如下方法:
//// TakePhotoManager.m// W00_PRO//// Created by CCYQ on /3/22.// Copyright © Facebook. All rights reserved.//#import "TakePhotoManager.h"#import "TakePhotoView.h"#import <Photos/PHPhotoLibrary.h>#import <AVFoundation/AVCaptureDevice.h>#import <AVFoundation/AVMediaFormat.h>@interface TakePhotoManager()@property (nonatomic, strong) TakePhotoView *takePhotoView;// 点击返回的block@property (nonatomic, copy) RCTBubblingEventBlock onTouchBackBlock;//@property (strong, nonatomic) RCTPromiseResolveBlock ocResolve;//@property (strong, nonatomic) RCTPromiseRejectBlock ocReject;@end@implementation TakePhotoManager// 标记宏(必要)RCT_EXPORT_MODULE()// 事件的导出RCT_EXPORT_VIEW_PROPERTY(onTouchBackBlock, RCTBubblingEventBlock)- (UIView *)view {_takePhotoView = [[TakePhotoView alloc] init];_takePhotoView.onTouchBackBlock = ^(NSDictionary *dicBlock) {};return _takePhotoView;}/*** 相机开始工作*/RCT_EXPORT_METHOD(camareStartRunning) {[_takePhotoView camareStartRunning];}/*** 相机停止工作*/RCT_EXPORT_METHOD(camareStopRunning) {[_takePhotoView camareStopRunning];}// 判断是否有权限使用相机RCT_EXPORT_METHOD(sureUseCamare:(RCTPromiseResolveBlock)resolverejecter:(RCTPromiseRejectBlock)reject) {//相机权限AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];//此应用程序没有被授权访问的照片数据。可能是家长控制权限//用户已经明确否认了这一照片数据的应用程序访问if (authStatus ==AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied) {//该方法为错误回调方法,可以通过该方法来做有无权限的判断reject(@"error", @"No permission", nil);}else {//该方法为正确的回调方法,可以通过该方法来做有无权限的判断resolve(@{@"name":@"success"});}}// 去开启权限RCT_EXPORT_METHOD(gotoOpenPermission:(NSDictionary *)dicText) {UIAlertController *alert = [UIAlertController alertControllerWithTitle:dicText[@"title"] message:dicText[@"message"] preferredStyle:UIAlertControllerStyleAlert];// 确定UIAlertAction *okAction = [UIAlertAction actionWithTitle:dicText[@"sureText"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];dispatch_async(dispatch_get_main_queue(), ^{if ([[UIApplication sharedApplication] canOpenURL:url]) {[[UIApplication sharedApplication] openURL:url];}});}];UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:dicText[@"cancelText"] style:UIAlertActionStyleCancel handler:nil];[alert addAction:okAction];[alert addAction:cancelAction];// 弹出对话框[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:true completion:nil];}@end
然后在RN中,我们就可以做一个权限的判断了,TakePhotoiOS.js 的代码改为如下:
import React, { Component } from 'react';import {AppRegistry,StyleSheet,Text,View,requireNativeComponent,NativeModules,Dimensions,} from 'react-native';import NavOutView from '../../../components/NavOutView';import { Actions } from 'react-native-router-flux';import { i18n } from "../../../config";const RNTakePhoto = requireNativeComponent('TakePhoto', TakePhotoiOS);const TakePhotoManager = NativeModules.TakePhotoManager;class TakePhotoiOS extends Component {constructor(props) {super(props);}componentDidMount() {/** 这里采用延迟250毫秒后调用相机开启的方法是,因为在* 原生层中,TakePhotoView 被创建之后,才能调用 相机开启* 也等同于要 RNTakePhoto 被创建之后,才能调用*/this.timeOutReFresh = setTimeout(() => {TakePhotoManager.sureUseCamare().then((sure)=> {// 有权限啦,可以TakePhotoManager.camareStartRunning();}).catch((e) => {console.warn(e);// 没有权限咯,要引导用户去开启权限哟TakePhotoManager.gotoOpenPermission({title: i18n.tip_title,message: '没有权限,是否去开启权限',sureText: i18n.bt_confirm,cancelText: i18n.bt_cancel,});});}, 250);}componentWillUnmount() {TakePhotoManager.camareStopRunning();}render() {return (<RNTakePhotostyle={styles.container}onTouchBackBlock={(event) => {console.log(event.nativeEvent);const eventMessage = event.nativeEvent;console.log(eventMessage.message);if (eventMessage.message === 'goBack') {Actions.pop();}}}/>);}}const styles = StyleSheet.create({container: {flex: 1,backgroundColor: 'transparent',},});module.exports = TakePhotoiOS;
Demo 链接地址
结尾
到这里,我们的原生拍照视图就已经封装完成了,接下还是有一些优化需要我们去做的,比如一些国际化之类的东西。但是到这里的话,也算是暂时告一段落了。很感谢各位看官能够坚持看到这里,如果对于文章中有什么错误之处,请帮忙纠正,我将感激不尽;如果这篇文章有帮到你一点点小忙,也可以点个小赞鼓励一下我。谢谢