Running an HTTP server inside your iOS app
Were you ever in a situation where you already had a functioning web app and wanted to make it into a mobile app with native features but did not have enough resources to build a team? This is exactly what happened to me. 😭
Why run an HTTP server in an iOS app? 🤓
- Local content serving 📁
- Offline functionality 🔻
- A bridge between native and web technologies 🌁
- Real-time data transfer from a native module (sensor) to web UI 🔥
If you are familiar with desktop app development, you may have heard the term hybrid apps in which the apps are rendered using a web-based UI and native functionalities are added to it using backend environments like Rust, node.js, java, etc.
Popular tools like IntelliJ run HTTP servers behind the scenes to provide functionality like automatically installing themes and plugins from their marketplace websites.
What if we wanted to achieve something similar in an iOS application where the web client would make an API call and we would give some information back to it? We can achieve this by using a wonderful library called Telegraph. ⭐️
Before you facepalm 🤦🏾♂️ and suggest things like why don't you use react-native or flutter, hear me out. I’m a big fan of cross-platform frameworks but they do not give you fine-grain control over all the capabilities of the device like VPN, NFC, Bluetooth, etc. And even if they do, it’s very difficult to do it.
Let’s begin 🧑💻
I’m assuming that you have a basic knowledge of how an iOS app is set up and how to use cocoapods in this article. With that in mind, let’s start by adding the Telegraph server pod to the Podfile. To get this
- Go to the Cocoapods’ official website
- Search for Telegraph
- Click on the Installation Guide button
- Copy the contents
- Add them to your Podfile
- Run pod install from your terminal
Now that you have all the required dependencies installed. Create a new group(folder) called Server and create a swift file called HttpServer inside it. This is how your project will look like once you have made these changes.
One thing which is required by the Telegraph server to work is to configure App Transport Security in your application’s Info.plist file.
With iOS 9 Apple introduced APS (App Transport Security) to improve user security and privacy by requiring apps to use secure network connections over HTTPS. It means that without additional configuration unsecure HTTP requests in apps that target iOS 9 or higher will fail. In iOS 9, APS is unfortunately also activated for LAN connections. Apple fixed this in iOS 10, by adding NSAllowsLocalNetworking
.
You can disable APS by adding the key App Transport Security Settings
to your Info.plist, with a sub-key where Allow Arbitrary Loads
is set to Yes
Once this change is done, let’s go to the HttpServer.swift file and do this 📄
import Foundation
import Telegraph
public class HttpServer: NSObject {
var server: Server!
var websocketClient: WebSocketClient!
let PORT:Int = 3000
}
// MARK: - Initial server and routes setup
public extension HttpServer {
func start(){
DispatchQueue.global().async {
self.setupServer()
}
}
func setupServer(){
// Start the server in HTTP mode (Can also be configured to start in HTTPs mode)
self.server = Server()
server.delegate = self
server.webSocketDelegate = self
server.route(.GET,"/:name",handleGet)
server.route(.POST,"/",handlePost)
server.route(.PUT,"/:name",handlePut)
server.route(.DELETE,"/:name/:age",handleDelete)
// Can handle upto 5 requests concurrently
server.concurrency = 5
do {
try server.start(port:self.PORT) //Start the server in port 3000
} catch {
print("Error when starting error:",error.localizedDescription)
}
}
}
// MARK: - Server route handlers
extension HttpServer {
func handleGet(request: HTTPRequest) -> HTTPResponse {
/// Get the name of the person from the parameter
let name = request.params["name"] ?? "stranger"
return HTTPResponse(content:"Hi \(name)!")
}
func handlePost(request: HTTPRequest) -> HTTPResponse {
/// Parse the request body
let person = try? JSONDecoder().decode(Person.self, from: request.body)
/// Print the request body to check if the request body is decoded properly
print(person ?? "Unknown")
/// Your modifications...
/// Return response
return HTTPResponse(content:"Hi \(person?.name ?? "Unknown"), your account was created!")
}
func handlePut(request: HTTPRequest) -> HTTPResponse {
/// Get the name of the person from the parameter
let name = request.params["name"] ?? "stranger"
/// Parse the request body
let person = try? JSONDecoder().decode(Person.self, from: request.body)
/// Print the request body to check if the request body is decoded properly
print(person ?? "Unknown")
/// Your modifications...
/// Return the response
return HTTPResponse(content:"\(name) ,your account data was changed!")
}
func handleDelete(request: HTTPRequest) -> HTTPResponse {
/// Get the name of the person from the parameter
let name = request.params["name"] ?? "stranger"
/// Your modifications...
/// Return response
return HTTPResponse(content:"\(name) ,your account was deleted.")
}
}
// MARK: - Server delegates
extension HttpServer: ServerDelegate {
public func serverDidStop(_ server: Telegraph.Server, error: (any Error)?) {
print("Server stopped:",error?.localizedDescription ?? "Unknown")
}
}
extension HttpServer: ServerWebSocketDelegate {
public func server(_ server: Telegraph.Server, webSocketDidDisconnect webSocket: any Telegraph.WebSocket, error: (any Error)?) {
print("Websocket client disconnected")
}
public func server(_ server: Telegraph.Server, webSocketDidConnect webSocket: any Telegraph.WebSocket, handshake: Telegraph.HTTPRequest) {
print("Websocket client connected")
}
public func server(_ server: Server, webSocket: WebSocket, didReceiveMessage message: WebSocketMessage) {
print("WebSocket message received:", message)
}
public func server(_ server: Server, webSocket: WebSocket, didSendMessage message: WebSocketMessage) {
webSocket.send(text: "Hello from websocket!")
print("WebSocket message sent:", message)
}
}
Whew! 😅. Let’s try to understand what is happening in this file. I have kept separate extensions for the Server setup, HttpServer delegate methods, and ServerWebsocket delegate handler methods.
Server setup 💻
Inside this extension and setupServer method, we are defining the server object, its delegates, the API routes, concurrency settings and finally starting the server.
Note that we are running the setupServer method using DispatchQueue asynchronously to avoid blocking the main thread and let the system optimize power usage for non-UI opearations.
Server Delegate
Inside this extension, we implement the necessary delegate method required by the ServerDelegate protocol. Also, I have added my API route handler methods inside this extension code block.
We can see that the majority of methods like GET, POST, PUT & DELETE can be handled by the server, and features like data extraction from query parameters, and JSON request body extraction are also possible.
PATCH, HEAD, TRACE, OPTIONS, CONNECT methods are also supported
Websocket Delegate
Here, we implement all the methods required by the ServerWebSocketDelegate protocol. We can see that it includes the majority of websocket lifecycle methods like
- webSocketDidConnect
- webSocketDidDisconnect
- didReceiveMessage
- didSendMessage
Multiple websocket connections can be maintained by the server if required
That covers the Telegraph part of the code. The code in ViewController.swift file is pretty simple where we just create an object of the HttpServer class and call the start method.
//
// ViewController.swift
// HttpServer
//
// Created by Nikhil Adiga
//
import UIKit
class ViewController: UIViewController {
var httpServer: HttpServer!
override func viewDidLoad() {
super.viewDidLoad()
self.httpServer = HttpServer()
self.httpServer.start()
}
}
Testing the app 🔨
To test whether your app is running or not, you have 2 choices.
- Create a simple web app that makes an API call to http://localhost:3000/<your_apis> and render it using webview.
- Run your code on a physical device and make sure your device and the build system (Mac) are connected to the same network. To ensure that they are in the same system, check the IP addresses of both devices.
To get the IP address of your Mac system, navigate to the terminal and run the ifconfig command.
If both the IP addresses are in the same subnet you can test the APIs through an HTTP client like Postman. This is possible because by default your HTTP server will be listening to all requests coming from 0.0.0.0
If you want to make your server more secure and entertain requests only from your device, do this
try server.start(port:self.PORT, interface:"localhost")
Now, your server will only accept requests from 127.0.0.1 and not other devices in your network 🔒
Other features ✍️
Telegraph Library also has additional support for features like
- Local content serving
- Handling CORS
- Adding custom middleware
- Ability to run the server in HTTPs mode using SSL certificates
While Telegraph supports a plethora of features, there are a few caveats with the library. For example, if the server stops after the app goes to the background for some time, we cannot upload large files to an API as form data, etc.
Conclusion ✅
Implementing an HTTP server within an iOS app using the Telegraph library opens up a world of possibilities for developers. While this approach comes with its challenges, particularly in terms of battery usage and potential App Store scrutiny, it also offers unique advantages in specific scenarios.
Alternatives to Telegraph 🤖
Potential use cases
- Sending device-specific data to the like sensor data, NFC sticker scanned data, QR code scan data, device ID, device network details, etc.
- Offline-first mobile apps with web UI. We can connect the server to work with a database like SQLite and give the data back to the locally rendered web app build.
While these use cases demonstrate the potential of running an HTTP server in an iOS app, it’s essential to weigh the benefits against the challenges
Ultimately, the decision to use an embedded HTTP server should be driven by specific app requirements that cannot be efficiently met through traditional iOS development patterns. When implemented thoughtfully, this approach can lead to innovative, powerful applications that push the boundaries of what’s possible on iOS devices 💪
The code used for this project can be found in my GitHub repository.