How to embed a web server in your React-Native app in Swift

Tarek Touati - Nov 27 '19 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Then run the Cocoapod install command

pod install
Enter fullscreen mode Exit fullscreen mode

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.

React Native Module Schema

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
    }
  }
Enter fullscreen mode Exit fullscreen mode

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 state

  • Errors enumerations are error cases

  • webServer variable is an instance GCDWebServer

  • 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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")
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

Then run the app in the simulator.

react-native run-ios
Enter fullscreen mode Exit fullscreen mode

Your app should look like this :

iPhone screen

Press button and enter given URL in your browser and you should see this :

Browser screen

You can find the complete project on Github

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .