← Back to index

SpectraService

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import WebKitFramework importsImports Foundation, WebKit.
@MainActor final class SpectraService: NSObject { static let shared = SpectraService() private override init() { super.init() } private var webView: WKWebView? private var continuation: CheckedContinuation<URL?, Never>? private var showName: String = "" // MARK: - Public func findShowURL(named showName: String) async -> URL? { self.showName = showName webView?.navigationDelegate = nil webView?.stopLoading() webView?.removeFromSuperview() webView = nil print("[Spectra] Starting search for: \(showName)") return await withCheckedContinuation { cont in self.continuation = cont let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() let wv = WKWebView(frame: CGRect(x: 0, y: 0, width: 375, height: 812), configuration: config) wv.navigationDelegate = self wv.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/17.0 Mobile/15E148 Safari/604.1" wv.isUserInteractionEnabled = false // prevent keyboard from ever appearing self.webView = wv if let window = UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .first?.windows.first { wv.isHidden = true window.addSubview(wv) } let url = URL(string: "https://spectra.theater/search")! wv.load(URLRequest(url: url)) } } // MARK: - Private private func typeAndExtract(in wv: WKWebView) { let safe = showName .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "'", with: "\\'") // React controlled inputs ignore plain value assignments. // We must use the native HTMLInputElement prototype value setter // so React sees the change, then fire the 'input' event. let fillJS = """ (function() { var input = document.querySelector('[placeholder*="Search for any"]') || document.querySelector('[placeholder*="search" i]') || document.querySelector('input[type="search"]') || document.querySelector('input[type="text"]'); if (!input) return 'ERROR: no search input found'; // React-compatible value injection var nativeSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value').set; nativeSetter.call(input, '\(safe)'); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' })); input.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'Enter' })); // Do NOT call input.focus() — that causes the system keyboard to appear. return 'typed: ' + input.value; })() """ wv.evaluateJavaScript(fillJS) { result, error in if let err = error { print("[Spectra] Fill error: \(err.localizedDescription)") } else { print("[Spectra] Fill: \(result as? String ?? "nil")") } } // Wait for live results to render, then extract the first show link Task { @MainActor in try? await Task.sleep(nanoseconds: 5_000_000_000) self.extractResult(from: wv) } } private func extractResult(from wv: WKWebView) { // Capture the webView's current URL before running JS — after React processes // the search it may have updated the address bar to include the query string, // giving us a useful fallback URL even if no direct show page is found. let fallbackURL = wv.url let js = """ (function() { // spectra.theater show pages live at /explore/production/<UUID> var patterns = ['/explore/production/', '/production/', '/show/', '/shows/']; var anchors = Array.from(document.querySelectorAll('a[href]')); for (var p of patterns) { var match = anchors.find(function(a) { return a.href.indexOf(p) !== -1; }); if (match) return match.href; } return null; })() """ wv.evaluateJavaScript(js) { [weak self] result, error in guard let self else { return } if let err = error { print("[Spectra] JS error: \(err.localizedDescription)") self.finish(with: fallbackURL); return } if let raw = result as? String, !raw.isEmpty { // Direct show page found — best case. print("[Spectra] Found show page: \(raw)") self.finish(with: URL(string: raw)) } else { // No direct link found; fall back to the search-results page URL so // the user gets a list they can browse (React updates the URL bar). print("[Spectra] No direct link — using search page: \(fallbackURL?.absoluteString ?? "nil")") self.finish(with: fallbackURL) } } } private func finish(with url: URL?) { webView?.removeFromSuperview() webView?.navigationDelegate = nil webView = nil continuation?.resume(returning: url) continuation = nil } }Code blockSee source code for full implementation.
▶ WKNAVIGATIONDELEGATE
extension SpectraService: WKNavigationDelegate { nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { Task { @MainActor in let url = webView.url?.absoluteString ?? "?" print("[Spectra] didFinish: \(url)") // Give the Next.js app a moment to hydrate before we interact try? await Task.sleep(nanoseconds: 1_500_000_000) self.typeAndExtract(in: webView) } } nonisolated func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { Task { @MainActor in print("[Spectra] Navigation failed: \(error.localizedDescription)") self.finish(with: nil) } } nonisolated func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { Task { @MainActor in print("[Spectra] Provisional navigation failed: \(error.localizedDescription)") self.finish(with: nil) } } }`SpectraService` extensionDefines the `SpectraService` extension. Conforms to WKNavigationDelegate.