开发语言:ReactNative 0.59.5 Swift 5
开发环境:VSCode Xcode 10.2
1、项目目录
参考文章:集成到现有原生应用
首先,我们按照建立一下目录结构,其中:
Code目录放置所有公用的ReactNative脚本,包,以及相关配置。
IOS目录放置原IOS项目。
Code (根目录)
--IOS (一级目录)
2、开发环境准备
2.1、package.json配置
在Code目录下创建package.json文件,编辑文件输入以下内容。
{
"name": "AppName",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "yarn react-native start"
}
}
2.2、React和React Native模块安装
在Code目录下使用控制台执行以下语句来安装React Native。
yarn add react-native
- 注意,执行完以上命令后,可能会出现以下提示内容,表示我们需要安装指定版本的React(此例子中需要安装版本为16.8.3的React)。
warning " > [email protected]" has unmet peer dependency "[email protected]".
在Code目录下使用控制台执行以下语句来安装指定版本的React
yarn add [email protected]
3、安装CocoaPods
3.1、安装CocoaPods(如果已安装过可跳过此步骤)
控制台执行以下命令
brew install cocoapods
3.2、创建podfile(如果IOS原有项目已配置过podfile可跳过此步骤)
在IOS根目录下使用控制台执行以下语句来创建podfile文件
pod init
3.3、使用podfile引用React Native
打开podfile,将与React Native有关的pod项目加入到podfile中。(下面例子中的新增部分)
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
#新增部分
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target 'ReactNativeDemo' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for ReactNativeDemo
#引用需要的第三方库,例如
pod 'SnapKit', '~>4.2.0'
#新增部分
#引用React,注意path路径应该指向根目录code中的node_modules,subspecs中为原生app解析rn控件需要的库,
#例如,我们在rn中使用text,则需要在subspecs中引用RCTText
# Pods for RNDemo
pod 'React', :path => '../../node_modules/react-native', :subspecs => [
'Core',
'CxxBridge', # 如果RN版本 >= 0.47则加入此行
'DevSupport', # 如果RN版本 >= 0.43,则需要加入此行才能开启开发者菜单
'RCTImage',
'RCTText',
'RCTNetwork',
'RCTWebSocket', # 调试功能需要此模块
'RCTAnimation', # FlatList和原生动画功能需要此模块
'RCTActionSheet',
'RCTGeolocation',
'RCTPushNotification',
'RCTSettings',
'RCTVibration',
'RCTLinkingIOS'
]
#新增部分
# 如果你的RN版本 >= 0.42.0,则加入下面这行,注意path路径
pod "yoga", :path => "../../node_modules/react-native/ReactCommon/yoga"
#新增部分
# 如果RN版本 >= 0.45则加入下面三个第三方编译依赖,注意podspec路径
pod 'DoubleConversion', :podspec => '../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../../node_modules/react-native/third-party-podspecs/Folly.podspec'
end
保存文件后,在IOS根目录下使用控制台执行以下语句来安装pod包。
pod install
4、脚本创建
在Code根目录下创建Scripts文件夹,用于存放React Native的脚本文件
Code (根目录)
--Scripts(一级目录,用于存放所有React Native的脚本)
然后我们可以在Scripts目录下开始写ReactNative的脚本了。
首先我们创建一个FrameText.js,然后写入如下内容:
import React, { Component } from 'react'
import { View, Text } from 'react-native'
export default class FrameText extends Component {
render() {
return (
{"我来自ReactNative,我是FrameText"}
);
}
}
// 整体js模块的名称
export { FrameText }
在FrameText中,我们创建了一个简单的组件,供其他脚本使用。
然后我们再创建一个index.js,然后写入如下内容
import {AppRegistry} from 'react-native'
import {FrameText} from 'FrameText'
// 整体js模块的名称
AppRegistry.registerComponent('Component-1', () => FrameText);
在index中,我们注册了FrameText组件,供app使用,我们可以在index.js注册很多不同的组件,app可以通过我们注册的名字(本例中为Component-1)来创建这些组件,下面我们来看看怎么在app内使用他们。
5、IOS项目修改
本例中期望在app的一个controller内,同时使用原生语言与ReactNative脚本分别显示2个View,现在我们来看看是如果在原生app中加载ReactNative的View。
5.1、创建 MyReactNativeBridge
新建一个swift文件,创建MyReactNativeBridge类,顾名思义,此类负责桥接原生app与ReactNative,注意,一个app中最好只创建一个桥接实例,所以使用单利模式创建。
import Foundation
import React
class MyReactNativeBridge {
static let sharedInstance = MyReactNativeBridge()
public let bridge : RCTBridge
public func initBridge() {
}
private init() {
#if DEBUG
//在debug使用虚拟服务器实时更新脚本,注意url中的/Scripts/index路径对应的是根目录中index.js的相对路径
let jsCodeLocation = URL(string: "http://127.0.0.1:8081/Scripts/index.bundle?platform=ios")
#else
//在release使用jsbundle包中的脚本
let jsCodeLocation = URL(string: "bundle/index.ios.jsbundle")
#endif
bridge = RCTBridge.init(bundleURL: jsCodeLocation,
moduleProvider: nil, launchOptions: nil)
#if DEBUG
//仅在debug时显示加载进度条
bridge.module(for: RCTDevLoadingView.self)
#endif
}
}
- 注意,在debug和release模式下,我们使用不同的js包,具体内容请参考代码中的注释。
5.2、创建并使用RCTRootView
RCTRootView为ReactNative脚本描述的View,我们可以通过MyReactNativeBridge来创建RCTRootView,然后就可以像使用其他UIView一样使用RCTRootView了。
import UIKit
import React
import SnapKit
class ViewController: UIViewController, RCTRootViewDelegate {
var rnView : RCTRootView?
@IBOutlet weak var rnRoot: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
rnView = RCTRootView.init(bridge: MyReactNativeBridge.sharedInstance.bridge,
moduleName: "Component-1",
initialProperties: nil)!
rnView?.delegate = self
rnView?.sizeFlexibility = RCTRootViewSizeFlexibility.widthAndHeight
rnRoot.addSubview(rnView!)
}
func rootViewDidChangeIntrinsicSize(_ rootView: RCTRootView!) {
rnView?.snp.removeConstraints()
rnView?.snp.makeConstraints({ (make) in
make.top.equalTo(0)
make.left.equalTo(0)
make.width.equalTo(rootView.intrinsicContentSize.width)//200
make.height.equalTo(rootView.intrinsicContentSize.height)//100
})
}
}
- 注意我们实现了RCTRootViewDelegate代理,这并不是必须的,在实现了此代理后,我们可以指定sizeFlexibility的模式(默认为none),ReactNative会修改组件的尺寸为js指定的实际大小(此例为200*100),然后通过rootViewDidChangeIntrinsicSize回调通知原生app已经完成尺寸修改,所以我们在此回调中添加约束。
现在我们运行xcode模拟器,并且命令开启ReactNative服务器(yarn start),就可以看到上面例子中的画面了。
6、原生与ReactNative通信
6.1、原生向ReactNative传递数据
注意我们在创建RCTRoot时的语句的第三个参数initialProperties,代表由原生向ReactNative传递的参数。
rnView = RCTRootView.init(bridge: MyReactNativeBridge.sharedInstance.bridge,
moduleName: "Component-1",
initialProperties: nil)!
现在修改创建语句和脚本
- Swift代码:通过initialProperties向JS传递参数
class ViewController: UIViewController, RCTRootViewDelegate {
var mockData:Dictionary = ["content":"初始化"]
...
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
rnView = RCTRootView.init(bridge: MyReactNativeBridge.sharedInstance.bridge,
moduleName: "Component-1",
initialProperties: mockData)!
}
}
- JS脚本:通过this.props["content"]使用原生App传递的参数(也可以使用this.props.content)
...
export default class FrameText extends Component {
render() {
return (
{this.props["content"]}
);
}
}
...
重新编译代码后,再次运行App,可以看到界面变成如下的样子
RCTRootView还有一个属性appProperties,我们可以通过修改这个属性来对已经创建的View传递参数。
- Swift代码:通过appProperties属性向已创建的View传递参数
func rootViewDidChangeIntrinsicSize(_ rootView: RCTRootView!) {
rnView?.snp.removeConstraints()
rnView?.snp.makeConstraints({ (make) in
make.top.equalTo(0)
make.left.equalTo(0)
make.width.equalTo(rootView.intrinsicContentSize.width)//200
make.height.equalTo(rootView.intrinsicContentSize.height)//100
})
mockData["content"] = #"重加载"#
rnView!.appProperties = mockData
}
重新编译代码后,再次运行App,可以看到界面变成如下的样子
6.2、ReactNative向原生传递数据
ReactNative定义了很多宏来处理这一需求,但我们无法在Swift中使用宏,需要使用oc与Swift混编
首先我们创建一个MyReactNativeBridge.swift文件
import Foundation
import React
@objc(MyReactNativeCommunication)
class MyReactNativeCommunication : NSObject {
//此方法处理ReactNative警告
@objc
static func requiresMainQueueSetup() -> Bool {
return true
}
//此方法供ReactNative调用,向原生app传递信息
@objc(test:)
func test(_ str :String) -> Void {
print(str)
}
}
然后在创建一个MyReactNativeBridge.m文件,同时系统会提示是否创建OC-Swift桥接文件,选是
MyReactNativeBridge.m
#import
#import
@interface RCT_EXTERN_MODULE(MyReactNativeCommunication, NSObject)
//此处的test对应MyReactNativeCommunication的text方法
//通过RCT_EXTERN_METHOD导出方法供ReactNative调用
RCT_EXTERN_METHOD(test:(NSString *)str)
@end
然后在桥接文件中引入如下头文件
#import
最后修改JS代码,调用test方法
import { NativeModules } from 'react-native'
export default class FrameText extends Component {
render() {
NativeModules.MyReactNativeCommunication.test("我回来啦");
...
}
}
重新编译代码后,再次运行App,可以看到xcode的输出 “我回来啦”