Introduction
React Native makes it easy to build native iOS and Android apps, there's a lot of modules that allow us to use native APIs made by an awesome community. But sometimes it can be frustrating to realize that your needed module does not exist and the only solution you have is to create it on your own. The thing is native modules using React Native should be developed in Swift and Objective-C for iOS and Java/Kotlin for Android.
In this article, we will be creating a module for React Native that interacts with a Swift SDK. The purpose of this module is to turn your iOS device into an HTTP server to serve a static HTML file.
I know what are you thinking about, why would I turn my device into an HTTP server?
So there's a lot of use cases some of them:
- File Manager/Text editor app and the ability to retrieve/share your files from any other device on your network
- As a Gateway for IoT
- As an Ad-hoc server
And maybe the most valuable case is: Just for the fun of it.
Setup new React Native project
You can skip this step if you already have a React Native project.
The first thing to do is to create a new project :
react-native init WebServerApp
cd WebServerApp
Install GCDWebServer pod
GCDWebServer is a library that allows us to create a lightweight HTTP 1.1 server.
To install this library make sure you have CocoaPods installed if it isn't I suggest you Cocoapods Getting Started guide.
let's install our dependency :
Dive into WebServerApp/ios
folder.
Open Podfile
file in your editor and add :
pod "GCDWebServer", "~> 3.5.3"
Then run the Cocoapod install command
pod install
Bridging Objective-C with Swift :
React Native was made to communicate with Objective-C, that's why we will need to create a bridging header.
In your ios folder, open the code project called WebServerApp.xcworkspace
in Xcode.
In Xcode :
- File -> New -> File or (Cmd + N)
- Select Swift file
- Name it WebServerManager
After creating our class, Xcode will suggest you create an Objective-C bridging header (usually this file is called <MY_PROJECT_NAME>-Bridging-header.h
):
Press Create Bridging Header
button, and you should have a WebServerApp-Bridging-header.h
file created
Add to WebServerApp-Bridging-header.h
file :
// React Native Bridge
#import "React/RCTBridgeModule.h"
// GCDWebServer headers
#import <GCDWebServer/GCDWebServer.h>
#import <GCDWebServer/GCDWebServerDataResponse.h>
Create WebServerManager Swift class
open WebServerManager.swift
file and declare WebServerManager class.
Our class inherited from NSObject so we can expose it to Objective-C
requiresMainQueueSetup
method let React Native know if your module needs to be initialized on the main thread
import Foundation
@objc(WebServerManager)
class WebServerManager: NSObject {
override init(){
super.init()
}
@objc static func requiresMainQueueSetup() -> Bool {
return true
}
}
Exposed WebServerManager methods
Our module will expose only 2 methods which are :
startServer
stopServer
startServer
method :
This method will initialize the server, retrieve the HTML content and return Promise with server URL or threw an error.
/**
Start `webserver` on the Main Thread
- Returns:`Promise` to JS side, resolve the server URL and reject thrown errors
*/
@objc public func startServer(_ resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock) -> Void
{
if (serverRunning == ServerState.Stopped){
DispatchQueue.main.sync{
do{
try self.initWebServer()
serverRunning = ServerState.Running
webServer.start(withPort: 8080, bonjourName: "RN Web Server")
resolve(webServer.serverURL?.absoluteString )
} catch {
reject("0", "Server init failed : \(error.localizedDescription)", error)
}
}
} else {
let errorMessage : String = "Server start failed"
reject("0", errorMessage, createError(message:errorMessage))
}
}
We are using DispatchQueue.main.sync
method because it needs to be executed on the Main thread
.
Add private Variables and Enumerations
ServerState
enumerations are differents server stateErrors
enumerations are error caseswebServer
variable is an instanceGCDWebServer
serverRunning
variable is the webserver state
private enum ServerState {
case Stopped
case Running
}
private enum Errors: Error {
case fileNotFound
case fileNotReadable
}
private let webServer: GCDWebServer = GCDWebServer()
private var serverRunning : ServerState = ServerState.Stopped
Add HTML file :
Create an HTML file with the content you want to serve.
Example :
<html>
<body>
<div>
<img
src="https://media1.tenor.com/images/3d124f67efd8e08b6fd3f0e748255a95/tenor.gif"
/>
<p>This web page is served from your React-Native App</p>
</div>
</body>
<style>
body {
background-color: #282c34;
}
div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
min-height: 100vh;
}
p {
color: #fff;
font-size: xx-large;
font-weight: 900;
font-family: sans-serif;
}
</style>
</html>
Retrieve the HTML content :
getfileContent
method will try to find index.html
file and return its content
If the file does not exist or if it can't read the file, it will throw an error
/**
Read `index.html` file and return its content
- Throws: `Errors.fileNotReadable`
if the content of `filePath` is unreadable
`Errors.fileNotFound`
if file in `filePath` is not found
- Returns: File content
*/
private func getfileContent() throws -> String{
if let filePath = Bundle.main.path(forResource: "index", ofType: "html") {
do {
let contents = try String(contentsOfFile: filePath)
return contents
} catch {
throw Errors.fileNotReadable
}
} else {
throw Errors.fileNotFound
}
}
Generic error method :
createError
method takes an error message and returns an NSError
/**
Creates an NSError with a given message.
- Parameter message: The error message.
- Returns: An error including a domain, error code, and error message.
*/
private func createError(message: String)-> NSError{
let error = NSError(domain: "app.domain", code: 0,userInfo: [NSLocalizedDescriptionKey: message])
return error
}
Initialize the server :
/**
Initialization of the `webserver`
- Throws: `Errors.fileNotReadable`
if the content of `filePath` is unreadable
`Errors.fileNotFound`
if file in `filePath` is not found
*/
public func initWebServer()throws{
do{
let content = try getfileContent()
webServer.addDefaultHandler(forMethod: "GET", request: GCDWebServerRequest.self, processBlock: {request in
return GCDWebServerDataResponse(html:content)
})
} catch Errors.fileNotFound {
throw createError(message:"File not found")
} catch Errors.fileNotReadable {
throw createError(message:"File not readable")
}
}
stopServer
method :
This method is executed when the server is running. It simply stops the server
/**
Stop `webserver` and update serverRunning variable to Stopped case
*/
@objc public func stopServer() -> Void{
if(serverRunning == ServerState.Running){
webServer.stop()
serverRunning = ServerState.Stopped
}
}
Expose WebServerManager
methods to React Native Bridge
As I said previously, RN was made to talk with Objective-C. So we need to create bridging headers.
- File -> New -> File or (Cmd + N)
- Select Objective-C file
- Name it WebServerManager
And add :
#import "React/RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(WebServerManager, NSObject)
RCT_EXTERN_METHOD(initWebServer)
RCT_EXTERN_METHOD(startServer: (RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(stopServer)
@end
We import RCTBridgeModule
to use React Native Macros.
Then RCT_EXTERN_MODULE
to expose our WebServerManager
class to JS side.
Bridging Objective-C and JavaScript
Import WebServerManager
module on JS side.
import { NativeModules } from "react-native";
// our native module
const { WebServerManager } = NativeModules;
Complete App.js
file
First install react-native-elements
dependecy.
yarn add react-native-elements react-native-vector-icons
react-native link react-native-vector-icons
import React, { useState } from "react";
import {
SafeAreaView,
StyleSheet,
View,
Text,
StatusBar,
NativeModules,
TouchableOpacity
} from "react-native";
import { Icon } from "react-native-elements";
// we import our native module
const { WebServerManager } = NativeModules;
const App: () => React$Node = () => {
const [endpoint, setEndpint] = useState("");
const [isServerRunning, setServerState] = useState(false);
const startServer = () => {
WebServerManager.startServer()
.then(url => setEndpint(url))
.then(() => setServerState(true))
.catch(err => console.error(err));
};
const stopServer = () => {
WebServerManager.stopServer();
setEndpint("");
setServerState(false);
};
return (
<>
<StatusBar barStyle="light-content" />
<SafeAreaView style={styles.safeView}>
<View style={styles.infoBlock}>
<Text style={styles.text}>
Press button to turn {isServerRunning ? "Off" : "On"} server
</Text>
</View>
<View style={styles.container}>
<TouchableOpacity>
<Icon
raised
name="power-off"
type="font-awesome"
color={isServerRunning ? "#01b907" : "#f44336"}
onPress={() => (isServerRunning ? stopServer() : startServer())}
/>
</TouchableOpacity>
</View>
{isServerRunning ? (
<View style={styles.container}>
<Text style={{ ...styles.text, ...styles.urlEndpoint }}>
Server is available at this Url : {endpoint}
</Text>
</View>
) : (
<View style={styles.container} />
)}
</SafeAreaView>
</>
);
};
const styles = StyleSheet.create({
safeView: {
backgroundColor: "#282c34",
height: "100%"
},
urlEndpoint: {
paddingTop: 20
},
text: {
color: "#FFF",
fontWeight: "900",
fontSize: 20,
textAlign: "center"
},
infoBlock: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
export default App;
Then run the app in the simulator.
react-native run-ios
Your app should look like this :
Press button and enter given URL in your browser and you should see this :
You can find the complete project on Github