Beyond the Framework, using React-Native with Swift and Kotlin
Part 2: Building Native UI for React Native using Swift and Kotlin
By: Daniel Friyia, Agile Software Engineer, TribalScale
Great to have you back again! In the last part of this series, I explained how to create basic native modules using Swift and Kotlin. If you haven’t completed that article yet, please go back and do it. It’s imperative to grasp the basics of native modules before moving on to native UI, which is more challenging to create. We also set up Kotlin in the previous article, which is important in moving forward with this one. As always, if you get stuck, feel free to check out my sample code here.
What are we building?
In this lesson, we will be building a simple button that changes colour when you press it by wrapping Swift and Kotlin code in React-Native’s JavaScript. A bit of a lame example, I know, but to set up something as small as this requires quite a bit of code and effort, as you’ll soon see! A more complex example, I suppose, could be a three-part Medium article in and of itself. My goal here is to teach you the basics to use the fundamentals from this tutorial to make any native UI component as complex or as simple as you’d like.
Making the Native Component with basic props and exposing it to React Native
TypeScript/JavaScript
Unlike writing native modules, I find it’s more helpful to start in TypeScript/JavaScript. Start by opening your GoingNative
project from the last blog. Next, create a file and call it SampleNativeComponent.tsx
. You’ll want to import a few things from react-native
that we need to use the native components:
import {
HostComponent,
requireNativeComponent,
NativeSyntheticEvent,
} from 'react-native';
After that, create a type that will represent our sample view’s props. Let's start with just a color myColor
. style
here is just the built in React-Native styles. It has nothing to do with native components.
type SampleNativeViewProps = {
myColor: string
style: object
}
Before adding any TSX, we have to set up the ability to hot-reload this component. For some reason, React-Native doesn’t support this well out of the box. To set up hot reload, we start by creating a wrapper for the JavaScript globals object so we can prevent type warnings. Create another file called GlobalsWrapper.ts
and wrap the globals object as follows:
Back in the SampleNativeComponent.tsx
you’ll want to import the methods you just created.
import {cacheNativeView, getCachedNativeView} from './GlobalsWrapper';
We then create a global variable and try to retrieve the native component if it already is cached in memory:
let component: HostComponent<SampleNativeViewProps>;
const globalView = getCachedNativeView();
Now, if there is no global view, and we are in __DEV__
mode we’ll want to cache the native view and assign it to the global variable:
if (__DEV__ && !globalView) {
component = requireNativeComponent<SampleNativeViewProps('SampleComponent');
cacheNativeView(component);
}
If the view has already been cached we just set it to our global variable
if (__DEV__ && !globalView) {
component = requireNativeComponent<SampleNativeViewProps('SampleComponent');
cacheNativeView(component);
} else if (__DEV__ && getCachedNativeView()) {
component = getCachedNativeView();
}
Lastly, if this is a production build, we just create the component and assign it to the global variable
if (__DEV__ && !globalView) {
component = requireNativeComponent<SampleNativeViewProps>('SampleComponent');
cacheNativeView(component);
} else if (__DEV__ && getCachedNativeView()) {
component = getCachedNativeView();
} else {
component = requireNativeComponent<SampleNativeViewProps>('SampleComponent');
}
At this point we can export the result of this call just how you would expect
export default component;
This may seem like a strange thing to do. However, if you fail to cache the view, each time you hot reload, the app will try and re-register a native view with the same name causing your app to red screen. This is the best way to protect against this type of red screening. Taken together, your file should look like this so far:
Finally, import the SampleNativeComponent
and add it under the seconds in your component.
return (
<View style={styles.container}>
<Text>{date}</Text>
<Text>The seconds count is: {seconds}</Text
<SampleNativeComponent
myColor={'red'}
style={styles.button}
/>
</View>
);
...const styles = StyleSheet.create({
...
button: {
height: 50,
width: 175,
},
});
iOS
Now that the boilerplate has been set up in TypeScript, lets move on to setting up the native code for this component in Swift. The first thing we need to do is create a plain Swift View. Let’s start by creating a group in the root of the iOS
project and calling it SampleView. In that group lets create SampleView.swift
.
In this file, you’ll want to import UIKit
which is the standard library for the UI Views created by Apple. Create a class called SampleView.swift
then add this method to it:
func updateComponent() {
switch self.myColor {
case "blue":
self.backgroundColor = .blue
break
default:
self.backgroundColor = .red
break
}
}
This method will change the background color of the app to blue if the string "blue"
is passed in from TypeScript. In any other case it will be red. Next, we’ll need a variable to keep track of the background color. Start by declaring it at the top of the class like this:
@objc var myColor: NSString = ""
Like native modules, we need to mark our variable as objc
because this property will need to be exposed to React-Native. There is a cool syntax sugar in Swift that allows us to perform a function every time this data is set that we’ll use here.
@objc var myColor: NSString = "" {
didSet {
updateComponent()
}
}
The didSet
method calls updateComponent
every time myColor
is set. Altogether your class should look like this:
The next thing we need in order to expose a swift view to TypeScript is to create a ComponentManager.
This is actually pretty easy. Just create a file called SampleComponentManager.swift
and add the following code:
@objc (SampleComponentManager)
class SampleComponentManager: RCTViewManager {
override func view() -> UIView! {
let labelView = SampleView()
labelView.backgroundColor = .red
return labelView
}
}
Lets break this code down. Because this class needs to be accessible by Objective-C macros later on, we need to annotate this class with the @objc
decorator. RCTViewManager
is the class exposed by React-Native that allows you to use native views. Finally the view()
method is called every time the app renders the screen and creates the view that the user sees. We return the sample view here so that the user can see it.
The final step to adding the native view on the iOS side is creating an Objective-C file to expose them. To do this we first create a file called SampleViewManager.m
then import the Event Emitter and View manager.
#import <React/RCTViewManager.h>
#import <React/RCTEventEmitter.h>
At this point we use the provided Objective-C marcos to expose our view to React-Native:
@interface RCT_EXTERN_MODULE(SampleComponentManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(myColor, NSString)
@end
In this code RCT_EXTERN_MODULE
exposes our component to React-Native. RCT_EXPORT_VIEW_PROPERTY
exposes myColor
as a prop to React-Native. There is a bit of magic here though. The framework already knows how to set myColor
, you don’t need to add any extra code to do this.
The entire code for this file should look like this:
#import <Foundation/Foundation.h>
#import <React/RCTViewManager.h>
#import <React/RCTEventEmitter.h>@interface RCT_EXTERN_MODULE(SampleComponentManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(myColor, NSString)
@end
If you now compile the app you should see a red box:
Android
So iOS is working now which is great, but as Cross-Platform developers we can’t stop there. Let’s finish up this section by completing the Android side of things. As a quick heads up you will have to really buckle down for this. Making native components for Android is more steps and missing one can make things tough to debug. You can really get lost here. If you do please refer to the sample project.
To start out, in the directory android/app/res/
create a directory called layout
. You can initiate this by right clicking on res
and going to Android Resource Directory
.
Next, specify your resource type as layout
and name your directory layout
and click ok.
At this point we need to create a layout file. Right click on layout and go to new > Layout Resource File
. Name the file sample_layout.xml.
After that go to the code view and copy and paste in the following code. Its just a plain layout.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:gravity="center_vertical"
android:layout_height="match_parent">
</LinearLayout>
Lets use our layout xml file in a proper Android view. To start, create a package under the java
directory and name it samplecomponent
. After this, create a file called SampleNativeComponent.kt
. The file should look like this:
package com.goingnative.samplecomponent
import android.annotation.SuppressLint
import android.widget.LinearLayout
import com.facebook.react.uimanager.ThemedReactContext
import com.goingnative.R
@SuppressLint("ViewConstructor")
class SampleNativeComponent(context: ThemedReactContext) : LinearLayout(context) {
private var rContext: ThemedReactContext = context
init {
inflate(rContext, R.layout.sample_component, this)
}
}
All this file really does is use Kotlin to “inflate” the view so that it can be shown in the app.
Now that we have a view, we need a way to send props to it and have control over it from the React-Native side. To do this, we create a file SampleNativeComponentManager.kt.
Start this class with the following imports:
import android.graphics.Color
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
We import the Color class here so we can change the color for our view. The rest of the classes are used for communication between React-Native and Android. We start our class with the getName()
function because it required for React-Native to know the name of our component. It should look like this:
class SampleNativeComponentViewManager : SimpleViewManager<SampleNativeComponent>() {
override fun getName(): String {
return "SampleComponent"
}
After this, we create an instance of our view for TypeScript/JavaScript consumption:
override fun createViewInstance(reactContext: ThemedReactContext): SampleNativeComponent {
return SampleNativeComponent(reactContext)
}
Lastly, we add a prop that we want to expose to React-Native:
@ReactProp(name = "myColor")
fun setMyColor(view: SampleNativeComponent, myColor: String) {
val hexColor = if (myColor == "red") "#FF0000" else "#0000FF"
view.setBackgroundColor(Color.parseColor(hexColor))
}
Taken together, your file should look like this:
Just like for native modules, we need to create a package in order to expose our native view to React-Native. Lets call this file SampleNativeComponentPackage.kt
. The file really isn’t that complicated. We just create our view manager in the createViewManagers
function which exposes it to React-Native.
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import java.util.*
class SampleNativeComponentPackage : ReactPackage {
override fun createViewManagers(reactContext: ReactApplicationContext)
: MutableList<ViewManager<*, *>>
{
return mutableListOf(
SampleNativeComponentViewManager()
)
}
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
return Collections.emptyList()
}
}
Finally in the MainApplication.java
file we instantiate our manager and add it to the React-Native package list:
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
...
packages.add(new SampleNativeComponentPackage());
return packages;
}
At this point, we should be able to compile and see our component in action:
Managing Events sent From React-Native to the Native code layer
Phew, like I said, quite a bit of work for such a small little component right 😉. We aren’t even done yet 😱! We still need to be able to recieve touch events from React-Native and do something with them in the native code layer. Since you probably still have Android Studio open. Let’s start there.
Android
The first thing we need to do in Android is to create an Event object that can be used by the React-Native Android framework to pass events to the TypeScript layer. Create a file called SampleNativeComponentEvent
. In this file start by importing Event and Arguments so that we can pass data to React-Native.
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
Next, create the SampleNativeComponentEvent
class:
class SampleNativeComponentEvent(surfaceId: Int, viewTag: Int) : Event<SampleNativeComponentEvent>() {
private val kEventName = "onUpdate"
init {
super.init(surfaceId, viewTag)
}
In this first part we use the class constructor syntax sugar to shorten the class a little. The surfaceId and viewTag are used by React-Native to process your events. We create a constant kEventName
to use later when we want an update event. The initializer just calls the superclass.
After this we define the method getEventName
so that JavaScript knows which method to look for.
override fun getEventName(): String {
return kEventName
}
Finally, we make the event that is actually used on the TS/React-Native side:
override fun getEventData(): WritableMap? {
val event = Arguments.createMap()
event.putBoolean("isPressed", true)
return event
}
Altogether your class should look like this:
Now that we have an event we can use, lets add it to the list of events that can be recieved by our component. To do this we go to the SampleNativeComponentViewManager.kt
that we created earlier and add this method:
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.of(
"onUpdate", MapBuilder.of("registrationName","onUpdate")
)
}
Finally, in our SampleNativeComponent.kt
file we add this listener to the initializer so that every time the view is tapped we send an event to React-Native:
import com.facebook.react.uimanager.UIManagerHelper...this.setOnClickListener {
val surfaceId = UIManagerHelper.getSurfaceId(rContext)
UIManagerHelper.getEventDispatcherForReactTag(rContext, id)
?.dispatchEvent(SampleNativeComponentEvent(surfaceId, id))
}
Altogether your file should look like this:
At this point we should have everything we need to get events on the React-Native side. Lets write some TypeScript to check. First, add the onUpdate method to your props in the component file. It should now look like this:
type SampleNativeViewProps = {
myColor: string;
style: object;
onUpdate: (e: NativeSyntheticEvent<{isPressed: boolean}>) => void;
};
To your App.tsx add the following code:
const [color, setColor] = useState<string>('red');...const onUpdate = (e: NativeSyntheticEvent<{isPressed: boolean}>) => {
if (e.nativeEvent.isPressed) {
setColor(color === 'red' ? 'blue' : 'red');
}
};...return (
<View style={styles.container}>
<Text>{date}</Text>
<Text>The seconds count is: {seconds}</Text
<SampleNativeComponent
myColor={color}
style={styles.button}
onUpdate={onUpdate}
/>
</View>
);
Your entire App.tsx should now look like this:
And with that we’ve achieved that glorious moment when we can communicate between Native Code and React-Native through a tap
iOS
Let finish up with iOS. We are almost at the end here, let’s do this guys 😤 ! As always, doing this in iOS is much, much, simpler than Android.
In SampleView.swift
add the following code:
@objc var onUpdate: RCTDirectEventBlock?...override init(frame: CGRect) {
super.init(frame: frame)
let press = UITapGestureRecognizer(
target: self,
action: #selector(sendUpdate(_:))
)
self.addGestureRecognizer(press)
}required init?(coder: NSCoder) {
super.init(coder: coder)
}@objc func sendUpdate(_ sender: UITapGestureRecognizer? = nil) {
if onUpdate != nil {
onUpdate!(["isPressed": true])
}
}
The @objc
here exposes onUpdate
to React-Native and allows it to recieve events on the JS side using callbacks. sendUpdate
allows us to send the tap event to React Native. We add a press event so that Native tapping can be picked up on by Swift.
Taken together your view should now look like this:
Next add this line to your SampleViewManager.m
to allow React-Native to use onUpdate as a prop:
RCT_EXPORT_VIEW_PROPERTY(onUpdate, RCTDirectEventBlock)
And thats it for iOS 😅. Really don’t understand why Android can’t have an easy interface like this.
Conclusion
So here we are at the end. At this point, you should be able to use this button on both platforms. As you can see, making native UI with React-Native is a lot of steps, so it should be used sparingly. That being said, you should never be afraid to use it! I hope you’ll look forward to adding a native UI or two to your next project! Like I stated in the original article, we need to be ready as React-Native developers to build native code when no good alternatives exist in the community.🥳 🎉 .
Have questions about using React-Native with Swift and Kotlin? Click here to speak to one of our experts.
Daniel is an Agile Software Developer specializing in Flutter and React-Native. As much as he likes cross platform he is passionate about all types of mobile development including native iOS and Android and is always looking to advance his skill in the mobile development world.
TribalScale is a global innovation firm that helps enterprises adapt and thrive in the digital era. We transform teams and processes, build best-in-class digital products, and create disruptive startups. Learn more about us on our website. Connect with us on Twitter, LinkedIn & Facebook!