如果大家到2022年了还需要混合跨平台开发,并且选择了 RN作为跨平台语言,那么接入原生的UI组件也是我们可能遇到的需求。这里我就总结一下我最近踩的坑,希望对即将接入的同学一点启发。
话不多说 先看看 官方文档接入IOS原生UI 官方的文档,感觉是很久很久了的,一直没有更新,我现在使用的0.68.最新的0.70都出来了,但是文档还是N年前的,无语中。。。。网上也找了很多资料,也没有找到最近一年有人写的swift相关的rn对接文档
官方文档接入原生UI里面全是 OC的代码,由于官方文档在 接入IOS原生模块 说 swift 不支持宏,所以从 Swift 向 React Native 导出类和函数需要多做一些设置 例如 你必须使用@objc 标记来确保类和函数对 Objective-C 公开。
如下图:
那么我们开始把demo翻译成swift语法吧。
第一步:
@objc(CHNativeInfoView)
class CHNativeInfoView: RCTViewManager {
override func view() -> UIView {
let baseView = NativeBaseView()
return baseView
}
override static func requiresMainQueueSetup() -> Bool {
return true
}
}
两点需要注意:
1.必须是 RCTViewManager的子类
2.必须实现 override func view() -> UIView 方法告诉rn你要接入的原生UI是啥View
第二步:
#import <React/RCTViewManager.h>
// 将模块暴露给React端
@interface RCT_EXTERN_MODULE(CHNativeInfoView, RCTViewManager)
// 设置属性 属性sectionIndex 是从 RN 传来的参数
RCT_EXPORT_VIEW_PROPERTY(sectionIndex, NSString);
// 设置回调方法 此回调方法主要是从原生回调去操作RN 的方法
// 注: 方法名前面一定要加上on 否则不会被执行
RCT_EXPORT_VIEW_PROPERTY(onCloseClickFinish, RCTBubblingEventBlock);
@end
第三步:写 NativeBaseView类需要给RN的属性 和回调方法
import UIKit
class NativeBaseView类需要给RN的属性: UIView {
var contentView: UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
var infoButtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "nav_close"), for: .normal)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 13)
btn.setTitle("我是原生ios按钮,点我试试", for: .normal)
btn.backgroundColor = .brown
btn.addTarget(target: self, action: #selector(buttonAction(_:)))
return btn
}()
var rightLbl: UIButton = {
let lbl = UIButton()
lbl.titleLabel?.font = UIFont.systemFont(ofSize: 13)
lbl.setTitle("我是隐藏的按钮,模拟回调", for: .normal)
lbl.isHidden = true
lbl.backgroundColor = .green
lbl.addTarget(target: self, action: #selector(rightAction(_:)))
return lbl
}()
@objc var onCloseClickFinish: RCTBubblingEventBlock?
@objc func buttonAction(_ sender: UIButton) {
rightLbl.isHidden = false
}
@objc func rightAction(_ sender: UIButton) {
print("===隐藏按钮点击==sectionIndex==\(sectionIndex)")
if let onClose = self.onCloseClickFinish {
onClose(["deleteRow": sectionIndex])
print("已注册回到===onCloseClickFinish==\(sectionIndex)")
}
}
init() {
super.init(frame: CGRect(x: 0, y: 0, width: YPScreen.screen_w, height: 200) )
self.backgroundColor = .white
addSubview(contentView)
contentView.backgroundColor = .yellow
contentView.snp.makeConstraints { make in
make.bottom.top.left.right.equalToSuperview()
}
contentView.addSubview(infoButtn)
infoButtn.snp.makeConstraints { make in
make.left.equalToSuperview().inset(20)
make.top.equalToSuperview()
make.height.equalTo(40)
}
contentView.addSubview(rightLbl)
rightLbl.snp.makeConstraints { make in
make.right.equalToSuperview()
make.top.equalTo(50)
make.height.equalTo(30)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var sectionIndex: String = ""
@objc func setSectionIndex(_ newSection: String) {
sectionIndex = newSection
}
}
以上就是原生IOS的写法,代码基本可以拷贝复用,我目前是xcode13,还没有升级14的,哈哈哈
第四步:下面开始写rn端的代码了,这也要感谢我们RN端的老师,都是他的帮助下写好的,我直接先上代码
import React, { Component, useEffect, useState,useRef } from 'react'
import {
Text,
View,
requireNativeComponent,
ViewStyle,
UIManager,
findNodeHandle,
TouchableOpacity,
HostComponent,
} from 'react-native'
const NativeIOSView: HostComponent<any> = requireNativeComponent('CHNativeInfoView')
type Props = {
style: ViewStyle
sectionIndex: string
onClose?: Function
}
const IOSRNView = (props: Props) => {
const { ...other } = props
const closeClickFinish = (event) => {
console.log(event.nativeEvent)
props.onClose?.()
}
return (
<NativeIOSView {...other} onCloseClickFinish={closeClickFinish} />
)
}
export default IOSRNView
说明:
1.CHNativeInfoView 是我们原生中暴露给RN的模块名字
2.NativeIOSView是我们导出后再RN中模块的名字
3.IOSRNView 是我们导出的 NativeIOSView RN中模块的 封装
- const { ...other } = props 是React语法,大家应该了解。这里面包含了我定义的属性 “sectionIndex” 这个和 我们原生swift里面写的是一致的,有点迷糊的同学可以回头看上面的代码
5.onCloseClickFinish 是原生swift代码回调方法,会去回调我rn中的 closeClickFinish
第五步:RN的使用
如图 我传入了属性 sectionIndex 和 回调方法 onClose 到此接入基本完成,
最后遇到的天坑
当然踩坑中还遇到一个奇葩问题,我感觉是 RN的官方的bug,他们提供的方法 UIManager.dispatchViewManagerCommand 调用的时候,会和OC交互,然后就直接挂了如下报错
Exception '*** -[__NSArray0 objectAtIndex:]: index 2 beyond bounds for empty NSArray' was thrown while invoking dispatchViewManagerCommand on target UIManager with params (
1463,
2,
(
{
viewName1 = Home1;
},
{
viewName2 = Home2;
},
{
viewName3 = Home3;
}
)
)
callstack: (
1 libobjc.A.dylib 0x00000001a4a1fbcc objc_exception_throw + 56
2 CoreFoundation 0x00000001a4bfdaf8 E2D6A76B-6879-31A3-8168-DF49F94E17CD + 174840
3 yupao 0x00000001098abcf0 -[RCTUIManager dispatchViewManagerCommand:commandID:commandArgs:] + 1004
4 CoreFoundation 0x00000001a4d038c0 E2D6A76B-6879-31A3-8168-DF49F94E17CD + 1247424
5 CoreFoundation 0x00000001a4bd4a70 E2D6A76B-6879-31A3-8168-DF49F94E17CD + 6768
6 CoreFoundation 0x00000001a4bd5648 E2D6A76B-6879-31A3-8168-DF49F94E17CD + 9800
7 yupao 0x0000000109850830 -[RCTModuleMethod invokeWithBridge:module:arguments:] + 1828
8 yupao 0x000000010985436c _ZN8facebook5reactL11invokeInnerEP9RCTBridgeP13RCTModuleDatajRKN5folly7dynamicEiN12_GLOBAL__N_117SchedulingContextE + 1156
9 yupao 0x0000000109853d0c _ZZN8facebook5react15RCTNativeModule6invokeEjON5folly7dynamicEiENK3$_0clEv + 144
10 yupao 0x0000000109853c70 ___ZN8facebook5react15RCTNativeModule6invokeEjON5folly7dynamicEi_block_invoke + 28
11 libdispatch.dylib 0x00000001a49c29a8 361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A + 371112
12 libdispatch.dylib 0x00000001a49c3524 361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A + 374052
13 libdispatch.dylib 0x00000001a49a0b3c 361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A + 232252
14 libdispatch.dylib 0x00000001a49a154c 361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A + 234828
15 libdispatch.dylib 0x00000001a49aa84c 361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A + 272460
16 libsystem_pthread.dylib 0x00000001a4a14b74 _pthread_wqthread + 272
17 libsystem_pthread.dylib 0x00000001a4a17740 start_wqthread + 8
)
RCTFatal
facebook::react::invokeInner(RCTBridge*, RCTModuleData*, unsigned int, folly::dynamic const&, int, (anonymous namespace)::SchedulingContext)
facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)::$_0::operator()() const
invocation function for block in facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)
361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A
361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A
361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A
361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A
361DA09A-E7CE-30EB-8DFC-9D9AF9DE4A0A
_pthread_wqthread
start_wqthread
上面就是错误的详情,数据都还没有传到原生 就挂了,无语了,总结了一下,就是 rn想调用原生的方法就两种方式:
第一:加载原生模块,然后原生模块自己调用原生方法。
第二:使用官方的 调用iOS原生模块 当然这个也是老文档了,需要自己转swift 对于调用 UIManager.dispatchViewManagerCommand 这个方法的正确用法,有同学知道的话就得留下你的足迹,谢谢