ReactNative接入Swift原生UI组件

1,276 阅读5分钟

如果大家到2022年了还需要混合跨平台开发,并且选择了 RN作为跨平台语言,那么接入原生的UI组件也是我们可能遇到的需求。这里我就总结一下我最近踩的坑,希望对即将接入的同学一点启发。

话不多说 先看看 官方文档接入IOS原生UI 官方的文档,感觉是很久很久了的,一直没有更新,我现在使用的0.68.最新的0.70都出来了,但是文档还是N年前的,无语中。。。。网上也找了很多资料,也没有找到最近一年有人写的swift相关的rn对接文档

官方文档接入原生UI里面全是 OC的代码,由于官方文档在 接入IOS原生模块 说 swift 不支持宏,所以从 Swift 向 React Native 导出类和函数需要多做一些设置 例如 你必须使用@objc 标记来确保类和函数对 Objective-C 公开。 如下图: image.png

那么我们开始把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中模块的 封装

  1. const { ...other } = props 是React语法,大家应该了解。这里面包含了我定义的属性 “sectionIndex” 这个和 我们原生swift里面写的是一致的,有点迷糊的同学可以回头看上面的代码

5.onCloseClickFinish 是原生swift代码回调方法,会去回调我rn中的 closeClickFinish

第五步:RN的使用

image.png

如图 我传入了属性 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 这个方法的正确用法,有同学知道的话就得留下你的足迹,谢谢