Skip to content

Instantly share code, notes, and snippets.

@devtanc
Last active September 2, 2022 09:07
Show Gist options
  • Save devtanc/8ef2c8afcc4d8f87061b42f4a9c7dc80 to your computer and use it in GitHub Desktop.
Save devtanc/8ef2c8afcc4d8f87061b42f4a9c7dc80 to your computer and use it in GitHub Desktop.
How to have `onXYZ` event hooks in a ReactNative Native UI Component in Swift

How to have onXYZ event hooks in a ReactNative Native UI Component in Swift

I wanted to have event syntax on my JSX with a native component written in Swift.

// Basically just this:
<MyComponent onChange={changes => console.log(changes)} />

No matter where I looked, no place seemed to have clear documentation on how to do all of this in one clear, concise place. After reading over StackOverflow posts and some Gists and some blog posts and putting it all together, this is what I got working. Please see the GitHub link below for a link to this repo that also has a working Android implementation.

This is the file structure of the Native Module (created via create-react-native-module):

├── README.md
├── android
│   └── ...later
|   // This file is where everything from the native modules ties in to the final exported React Native component
├── index.js
├── ios
|   // These files are modified and added via XCode. Be sure they're part of the project "Target Membership"!
│   ├── RTEEventEmitter.m
│   ├── RTEEventEmitter.swift
│   ├── RichTextEditor-Bridging-Header.h
│   ├── RichTextEditor.m
│   ├── RichTextEditorManager.swift
│   ├── RichTextEditor.xcworkspace
│   └── RichTextEditor.xcodeproj
├── package.json
├── react-native-rich-text-editor.podspec
└── yarn.lock

This module is then installed in a React Native app, and pulled into the file, as shown in App.js at the end. Thanks to eschos24 for his help with all of this!

Additional reading:

This post helped me understand the evens a little better

The official docs had some ObjC that we translated and it helped get events set up

My GitHub repo that has all of these files in their most recent iteration.

// This file is from an actual react app that would install the library outlined in the rest of these files
import React from 'react'
import { SafeAreaView, StyleSheet, View } from 'react-native'
import RichTextEditor from 'react-native-rich-text-editor'
const styles = StyleSheet.create({
body: {
flex: 1,
},
inputContainer: {
flex: 1,
padding: 20,
},
input: {
flex: 1,
backgroundColor: 'lightgray',
borderRadius: 4,
},
spacer: {
flex: 3,
},
})
const App = () => {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.inputContainer}>
<View style={styles.spacer} />
<RichTextEditor
style={styles.input}
onMentionStart={text => console.log('An "@" was typed:', text)}
/>
<View style={styles.spacer} />
</View>
</SafeAreaView>
)
}
export default App
import React from 'react';
import {
requireNativeComponent,
NativeModules,
NativeEventEmitter,
} from 'react-native';
import isFunction from 'lodash.isfunction';
const { RTEEventEmitter } = NativeModules;
// Connects the JS and Native event emitters over the RNBridge
const RTVEventEmitter = new NativeEventEmitter(RTEEventEmitter);
const RichTextEditor = requireNativeComponent('RichTextEditor');
// This component is all about abstracting away the native module interface
// and allowing downstream users to use the simple `onXYZ` hooks in the props
export default class RichTextView extends React.Component {
subscriptions = [];
componentDidMount() {
// Only add the listener if the associated prop is a callback function
if (this.isValidCallback(this.props.onMentionStart)) {
this.subscriptions.push(
// This is when the `startObserving` function is called on the native side
// if this is the first component using `RTEEventEmitter` that mounted
RTVEventEmitter.addListener('StartMention', this.handleStartMention),
);
}
}
// Check that a prop exists and is a function
isValidCallback = prop => prop && isFunction(prop);
// Call the prop callback (which we already know is a function)
handleStartMention = text => this.props.onMentionStart(text);
// Remove all listeners when the component is unmounted
// This is when the `stopObserving` function is called on the native side
// if this is the last component using `RTEEventEmitter` that unmounted
componentWillUnmount = () => this.subscriptions.forEach(sub => sub.remove());
render() {
return <RichTextEditor {...this.props} />;
}
}
#import <React/RCTBridgeModule.h>
#import <React/RCTViewManager.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(RichTextEditorManager, RCTViewManager)
@end
import UIKit
@objc(RichTextEditorManager)
class RichTextEditorManager : RCTViewManager, UITextViewDelegate {
let richTextView = UITextView()
override func view() -> UIView! {
if richTextView.delegate != nil {
return richTextView
}
richTextView.delegate = self
richTextView.text = "Please type here..."
return richTextView
}
override class func requiresMainQueueSetup() -> Bool {
return true
}
func textViewDidBeginEditing(_ textView: UITextView) {
richTextView.text = ""
print("BEGIN EDITING")
}
func textViewDidEndEditing(_ textView: UITextView) {
print("END EDITING")
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
print("Range: \(range)")
print("Text: \(text)")
if (text == "@") {
RTEEventEmitter.shared?.emitEvent(withName: "StartMention", body: ["data": text])
}
return true
}
}
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(RTEEventEmitter, RCTEventEmitter)
@end
import Foundation
@objc(RTEEventEmitter)
class RTEEventEmitter : RCTEventEmitter {
static var shared:RTEEventEmitter?
private var supportedEventNames: Set<String> = ["StartMention"]
private var hasAttachedListener = false
// Allows a shared EventEmitter instance to avoid initializing without the RNBridge
// Without this step, you'll run into errors talking aobut a missing bridge
override init() {
super.init()
RTEEventEmitter.shared = self
}
override class func requiresMainQueueSetup() -> Bool {
return false
}
// These functions make sure that there is an attached listener so that events are
// only sent when a listener is attached
override func startObserving() {
hasAttachedListener = true
}
override func stopObserving() {
hasAttachedListener = false
}
// Must return an array of the supported events. Any unsupported events will throw errors
// if they are passed in to `sendEvent`
override func supportedEvents() -> [String] {
return Array(supportedEventNames)
}
// Allows sending of supported events and adds protections for when either no listeners
// ar attached or the specified event isn't a supported event
func emitEvent(withName name: String, body: Any!) {
if hasAttachedListener && supportedEventNames.contains(name) {
sendEvent(withName: name, body: body)
}
}
}
@nacholopeztoral
Copy link

This is brilliant and exactly what I had been trying to achieve for a week!! Thanks for making this public @devtanc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment