封装原生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 2018/3/22.
//  Copyright © 2018年 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 2018/3/22.
//  Copyright © 2018年 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.js
import 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 2018/3/22.
//  Copyright © 2018年 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 2018/3/22.
//  Copyright © 2018年 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 2018/3/22.
//  Copyright © 2018年 Facebook. All rights reserved.
//#import <UIKit/UIKit.h>@interface TakePhotoView : UIView// 关闭按钮的block
typedef 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 2018/3/22.
//  Copyright © 2018年 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 2018/3/22.
//  Copyright © 2018年 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 链接地址

结尾

    到这里,我们的原生拍照视图就已经封装完成了,接下还是有一些优化需要我们去做的,比如一些国际化之类的东西。但是到这里的话,也算是暂时告一段落了。很感谢各位看官能够坚持看到这里,如果对于文章中有什么错误之处,请帮忙纠正,我将感激不尽;如果这篇文章有帮到你一点点小忙,也可以点个小赞鼓励一下我。谢谢

封装iOS原生UI 控件给RN调用相关推荐

  1. iOS基础——UI控件之UIAlertController、UINavigationController、Segue、SVProgressHUD

    iOS基础--UI控件之UIAlertController.UINavigationController.Segue 一.UIAlertController 1.普通对话框 -(void)update ...

  2. IOS 常用UI控件

    目录 下拉刷新 模糊效果 AutoLayout 富文本 图表 表相关与Tabbar 隐藏与显示 HUD与Toast 对话框 其他UI 具体内容 下拉刷新 EGOTableViewPullRefresh ...

  3. sketch里的ios控件_30个让你眼前一亮的iOS Swift UI控件!

    前言 笔者接触 iOS 开发有一段时间了,尤其特别喜欢UI部分,特意收集整理了30个让你惊艳的第三方开源控件(swift),无论是应用到项目中还是用来学习都能让你大呼过瘾,废话不多说,直接上图上链接! ...

  4. iOS 使用UI控件的外观协议UIAppearance进行设置默认UI控件样式

    在iOS开发中,经常会对UINavigationBar的样式进行全局样式.采用的设置方式有两种: 第一种,采用方式如下: [UINavigationBar appearance] 这种是对一类对象的默 ...

  5. 推荐爱码哥移动开发平台十大常用的原生UI控件

    imag.js是一种NativeScript形式的框架,它兼具 Web 应用的灵活和 Native 应用的高性能,可以使用 JavaScript 来开发 iOS 和 Android 原生应用.在 Ja ...

  6. iOS基本UI控件总结

    包括以下几类: //继承自NSObject:(暂列为控件) UIColor *_color;    //颜色 UIImage *_image;    //图像 //继承自UIView:只能相应手势UI ...

  7. IOS开发UI控件UIScrollView和Delegate的使用

    1. 什么是UIScrollView 移动设备的屏幕大小是极其有限的,因此直接展示在用户眼前的内容也相当有限 当展示的内容较多,超出一个屏幕时,用户可通过滚动手势来查看屏幕以外的内容 普通的UIVie ...

  8. iOS开发-UI控件:UIImagePickerController 视频录制操作,视频大小,时间长度

    转自: http://www.cnblogs.com/cocoajin/p/3494290.html 简介: 使用 iOS 系统 UIImagePickerController 获取视频大小 获取视频 ...

  9. 黑马程序员——IOS学习—基本UI控件的代码创建

    -----------Java培训.Android培训.IOS培训..Net培训.期待与您交流!------------  本节采用代码的方式系统了解一下IOS主要UI控件的创建和使用,在开始之前首先 ...

最新文章

  1. XXL-REGISTRY v1.0.2 发布,分布式服务注册中心
  2. 【推荐】R for Data Science 新书抢先看
  3. 深度学习与PyTorch实战
  4. srm 593 dv2 1000pt
  5. Java认证值得吗?
  6. Oracle 数据库中较为复杂或典型的 SQL 语句的解读
  7. 小程序mpvue图片绘制水印_开发笔记:使用 mpvue 开发斗图小程序
  8. MS的完整形式是什么?
  9. vue实现消息badge 标记_Vue $mount实战之实现消息弹窗组件
  10. linux中产生随机数函数,如何用C++产生随机数
  11. windows “文件大小”与“占用空间”、文件系统与文件拷贝
  12. mysql 表增加多个索引_mysql给同一个表添加多个索引的测试
  13. sass函数:@function
  14. 基于SSM的大学生助学贷款管理系统
  15. 杭州是个技术乐观派的城市
  16. GSP算法与SPADE算法
  17. 使用PyTorch实现手写文字识别的学习
  18. 啊哈添柴挑战Java1223. 输出对勾
  19. 虚拟服务器怎么连uk,rustUKn建造服务器指令
  20. 基于jupyter notebook的python编程-----MNIST数据集的的定义及相关处理学习

热门文章

  1. ubuntu 分区已满
  2. 测试用例设计之等价类法
  3. SpringBoot+Maven+Nacos搭建微服务应用
  4. 线性代数04 行列式的性质:举一反三,从三个到十个
  5. 搜客网游戏源码社区开通啦!游戏源码、游戏技术交流、游戏辅助等交流共享平台
  6. 屏幕在休眠唤醒后闪屏
  7. [ 注意力机制 ] 经典网络模型1——SENet 详解与复现
  8. Unity 音乐可视化(音乐频谱控制物体的运动)
  9. 实现一个数字的千分位【Python】【整数与字符串之间的转换】
  10. 可重入锁与非可重入锁