// SpeedTestView.swift // SpeedOf.Me API - iOS/SwiftUI Integration // // This file demonstrates how to integrate the SpeedOf.Me speed test // into a SwiftUI app using WKWebView. import SwiftUI import WebKit // MARK: - Data Models struct SpeedTestResult: Codable { let download: Double let upload: Double let latency: Double let jitter: Double let testServer: String? let ip_address: String? let hostname: String? } struct SpeedTestProgress: Codable { let testType: String let currentSpeed: Double let percentComplete: Double let passNumber: Int } struct SpeedTestError: Codable { let code: Int let message: String } struct WebViewMessage: Codable { let type: String let data: AnyCodable } // Helper for decoding mixed JSON struct AnyCodable: Codable { let value: Any init(_ value: Any) { self.value = value } init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let dict = try? container.decode([String: AnyCodable].self) { value = dict.mapValues { $0.value } } else if let array = try? container.decode([AnyCodable].self) { value = array.map { $0.value } } else if let string = try? container.decode(String.self) { value = string } else if let double = try? container.decode(Double.self) { value = double } else if let int = try? container.decode(Int.self) { value = int } else if let bool = try? container.decode(Bool.self) { value = bool } else { value = NSNull() } } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encodeNil() } } // MARK: - Observable Model @MainActor class SpeedTestViewModel: ObservableObject { @Published var status: String = "Ready" @Published var result: SpeedTestResult? @Published var progress: SpeedTestProgress? @Published var error: SpeedTestError? @Published var isRunning: Bool = false func handleMessage(_ message: WebViewMessage) { switch message.type { case "ready": status = "Ready to test" case "started": isRunning = true result = nil error = nil status = "Starting test..." case "progress": if let data = message.data.value as? [String: Any], let jsonData = try? JSONSerialization.data(withJSONObject: data), let progress = try? JSONDecoder().decode(SpeedTestProgress.self, from: jsonData) { self.progress = progress let testType = progress.testType == "download" ? "Download" : "Upload" status = "\(testType): \(String(format: "%.1f", progress.currentSpeed)) Mbps" } case "completed": if let data = message.data.value as? [String: Any], let jsonData = try? JSONSerialization.data(withJSONObject: data), let result = try? JSONDecoder().decode(SpeedTestResult.self, from: jsonData) { self.result = result self.progress = nil isRunning = false status = "Test complete" } case "error": if let data = message.data.value as? [String: Any], let jsonData = try? JSONSerialization.data(withJSONObject: data), let error = try? JSONDecoder().decode(SpeedTestError.self, from: jsonData) { self.error = error isRunning = false status = "Error: \(error.message)" } default: break } } } // MARK: - WebView Wrapper struct SpeedTestWebView: UIViewRepresentable { @ObservedObject var viewModel: SpeedTestViewModel func makeUIView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() // Add message handler for JavaScript-to-Swift communication config.userContentController.add( context.coordinator, name: "speedTest" ) let webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = context.coordinator // Load the speed test HTML // Option 1: Load from bundle if let url = Bundle.main.url(forResource: "speedtest", withExtension: "html") { webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) } // Option 2: Load from remote server // if let url = URL(string: "https://your-domain.com/speedtest.html") { // webView.load(URLRequest(url: url)) // } return webView } func updateUIView(_ uiView: WKWebView, context: Context) { // No updates needed } func makeCoordinator() -> Coordinator { Coordinator(viewModel: viewModel) } class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { var viewModel: SpeedTestViewModel init(viewModel: SpeedTestViewModel) { self.viewModel = viewModel } // Handle messages from JavaScript func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == "speedTest", let body = message.body as? [String: Any], let jsonData = try? JSONSerialization.data(withJSONObject: body), let msg = try? JSONDecoder().decode(WebViewMessage.self, from: jsonData) else { return } Task { @MainActor in viewModel.handleMessage(msg) } } } } // MARK: - Main View struct SpeedTestView: View { @StateObject private var viewModel = SpeedTestViewModel() var body: some View { VStack(spacing: 0) { // Status bar HStack { Text(viewModel.status) .font(.headline) Spacer() if viewModel.isRunning { ProgressView() .scaleEffect(0.8) } } .padding() .background(Color(.systemBackground)) // WebView SpeedTestWebView(viewModel: viewModel) // Native results display (optional - results also shown in WebView) if let result = viewModel.result { VStack(spacing: 12) { ResultRow(label: "Download", value: "\(result.download) Mbps") ResultRow(label: "Upload", value: "\(result.upload) Mbps") ResultRow(label: "Latency", value: "\(result.latency) ms") } .padding() .background(Color(.systemBackground)) } } .navigationTitle("Speed Test") } } struct ResultRow: View { let label: String let value: String var body: some View { HStack { Text(label) .foregroundColor(.secondary) Spacer() Text(value) .fontWeight(.semibold) .foregroundColor(.blue) } } } // MARK: - Preview #Preview { NavigationView { SpeedTestView() } }