How to embed a web server in your React-Native app in Swift
Tags: reactnative, react, swift, ios
Published: 2019-11-27 22:31:38

# 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 :

```bash
react-native init WebServerApp
cd WebServerApp
```

## Install GCDWebServer pod

[GCDWebServer](https://github.com/swisspol/GCDWebServer) is a library that allows us to create a lightweight HTTP 1.1 server.
To install this library make sure you have [CocoaPods](https://cocoapods.org) installed if it isn't I suggest you [Cocoapods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).

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

```bash
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.

![React Native Module Schema](https://github.com/Tarektouati/react-native-web-server-app/blob/master/assets/RN_Module_Schema.jpg?raw=true)

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 :

```objective-c
// 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

```swift
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.

```swift
  /**
  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 state

- `Errors` enumerations are error cases

- `webServer` variable is an instance `GCDWebServer`

- `serverRunning` variable is the webserver state

```swift
  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
<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

```swift

  /**
  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

```swift
  /**
  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 :

```swift
  /**
  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

```swift
  /**
  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 :

```objective-c
#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.

```js
import { NativeModules } from "react-native";

// our native module
const { WebServerManager } = NativeModules;
```

#### Complete `App.js` file

First install `react-native-elements` dependecy.

```bash
yarn add react-native-elements react-native-vector-icons

react-native link react-native-vector-icons
```

```jsx
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.

```bash
react-native run-ios
```

Your app should look like this :

![iPhone screen](https://github.com/Tarektouati/react-native-web-server-app/blob/master/assets/iPhoneScreen.gif?raw=true)

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

![Browser screen](https://github.com/Tarektouati/react-native-web-server-app/blob/master/assets/browser.gif?raw=true)

You can find the complete project on [Github](https://github.com/Tarektouati/react-native-web-server-app)