SpeedOf.Me API - iOS Integration

This example demonstrates how to integrate the SpeedOf.Me speed test API into an iOS app using WKWebView.

How It Works

The SpeedOf.Me API is JavaScript-based, so iOS apps use WKWebView to:

  1. Load an HTML page containing the speed test, served from your registered domain
  2. Receive results via WKScriptMessageHandler

Files

Prerequisites

Quick Start

1. Add Files to Your Project

Copy SpeedTestView.swift into your Xcode project. speedtest.html does not go in the app: it is served from your own domain, see step 3.

2. Configure API Credentials

In speedtest.html, replace the placeholder values:

SomApi.account = "YOUR_API_KEY";
SomApi.domainName = "your-domain.com";

3. Host speedtest.html on Your Registered Domain

Upload speedtest.html to the domain registered on your SpeedOf.Me account, and point the WKWebView at that URL:

if let url = URL(string: "https://your-domain.com/speedtest.html") {
    webView.load(URLRequest(url: url))
}

This step is not optional. The engine validates the page that embeds api.js against your registered domain, and it reads that from the page's own URL. A page loaded from the app bundle has no host, so it can never match and every test fails with error 1002 (Domain Mismatch).

4. Use the View

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            SpeedTestView()
        }
    }
}

Architecture

iOS App
   └── SpeedTestView (SwiftUI)
           └── SpeedTestWebView (UIViewRepresentable)
                   └── WKWebView
                           └── speedtest.html (on your registered domain)
                                   └── api.js (defines the SomApi global)
                                           ▼
                           window.webkit.messageHandlers.speedTest.postMessage()
                                           ▼
                           WKScriptMessageHandler.didReceive(message:)

JavaScript-to-Swift Communication

The HTML page sends messages to Swift via:

window.webkit.messageHandlers.speedTest.postMessage({
    type: 'completed',
    data: result
});

Message Payloads

speedtest.html normalizes what the engine sends before forwarding it, so the Swift Codable structs always decode. A blank number arrives as 0, and a blank string (testServer, ip_address, hostname) arrives as "".

progress

FieldValue
type"download", "upload" or "latency"
passPass number, '' on latency events
percentDone0-100
currentSpeedMbps, only when SomApi.config.progress.verbose is true, otherwise ''. Also '' on latency events
maxSpeedMbps, same rule as currentSpeed, and also '' while the running maximum is still 0
latencyms, only on the final latency event, otherwise ''
jitterms, same rule as latency

The template turns verbose mode on and forwards type, pass, percentDone and currentSpeed.

completed

Every field is always present. A disabled sub-test sends a blank value rather than omitting the key.

FieldValue
downloadMbps
uploadMbps, '' when uploadTestEnabled is false
maxDownloadMbps
maxUploadMbps, '' when uploadTestEnabled is false
latencyms, '' when latencyTestEnabled is false
jitterms, '' when latencyTestEnabled is false
testServer'' when testServerEnabled is false
ip_address'0.0.0.0' when userInfoEnabled is false
hostname'' when userInfoEnabled is false
userAgentUser agent string
testDateISO 8601 timestamp

The template forwards download, upload, latency, jitter, testServer, ip_address and hostname.

Error Codes

onError receives { code, message }.

CodeMeaning
1001Invalid Account: the API key is wrong or inactive
1002Domain Mismatch: the page URL does not match your registered domain
2001Test Error
2002Invalid server response
2003Request timeout
2004Test timeout: the run exceeded config.testTimeout
2005Speed test could not start (status N)
2006Speed test engine did not load

Code 2006, and code 2005 with status 0, mean the request never left the device, which is almost always a content blocker, or a frame-src policy that does not allow https://speedof.me. A 2005 carrying any other status is a real HTTP response from the server, so read the status in the message.

UIKit Alternative

If you are not using SwiftUI, here is a UIKit version.

Register the handler through a proxy that holds it weakly, and remove it on teardown. The user content controller retains its message handler and the view controller retains the web view that owns it, so passing self straight to add(_:name:) closes that loop and neither object is ever released.

import UIKit
import WebKit

// The user content controller retains its message handler, and the view
// controller retains the web view that owns it. Registering `self` directly
// would close that loop and neither object would ever deallocate, so route
// through a proxy that holds the real handler weakly.
final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
    weak var delegate: WKScriptMessageHandler?

    init(delegate: WKScriptMessageHandler) {
        self.delegate = delegate
    }

    func userContentController(_ userContentController: WKUserContentController,
                               didReceive message: WKScriptMessage) {
        delegate?.userContentController(userContentController, didReceive: message)
    }
}

class SpeedTestViewController: UIViewController, WKScriptMessageHandler {
    private var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = WKWebViewConfiguration()
        config.userContentController.add(WeakScriptMessageHandler(delegate: self),
                                         name: "speedTest")

        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(webView)

        if let url = URL(string: "https://your-domain.com/speedtest.html") {
            webView.load(URLRequest(url: url))
        }
    }

    deinit {
        webView?.configuration.userContentController
            .removeScriptMessageHandler(forName: "speedTest")
    }

    func userContentController(_ userContentController: WKUserContentController,
                               didReceive message: WKScriptMessage) {
        guard let body = message.body as? [String: Any],
              let type = body["type"] as? String,
              let data = body["data"] else { return }

        switch type {
        case "completed":
            if let result = data as? [String: Any] {
                print("Download: \(result["download"] ?? "N/A") Mbps")
            }
        case "progress":
            // Handle progress
            break
        case "error":
            // Handle error
            break
        default:
            break
        }
    }
}

Configuration Options

SomApi.config.sustainTime = 6;             // 1-8 seconds
SomApi.config.testServerEnabled = true;    // Report the test server
SomApi.config.userInfoEnabled = true;      // Report IP and hostname
SomApi.config.latencyTestEnabled = true;   // Include latency test
SomApi.config.uploadTestEnabled = true;    // Include upload test
SomApi.config.progress.enabled = true;     // Fire onProgress during the test
SomApi.config.progress.verbose = true;     // Required for progress.currentSpeed

Production Considerations

  1. API Key Security: Store API keys in Keychain or fetch from server
  2. Network Permissions: Add usage description if required
  3. Background Handling: Speed tests should run in foreground only
  4. Memory: WKWebView manages memory well, but monitor for leaks

Troubleshooting

Links