| @MainActor
final class IBDBService: NSObject {
static let shared = IBDBService()
private override init() { super.init() }
// MARK: - State
private enum Phase { case loadingForm, loadingResults }
private var webView: WKWebView?
private var continuation: CheckedContinuation<URL?, Never>?
private var phase: Phase = .loadingForm
private var showName: String = ""
// MARK: - Public
func findShowURL(named showName: String) async -> URL? {
self.showName = showName
self.phase = .loadingForm
webView?.navigationDelegate = nil
webView?.stopLoading()
webView?.removeFromSuperview()
webView = nil
print("[IBDB] 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
// Must be in a window so WebKit's JS engine runs at full speed
if let window = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first?.windows.first {
wv.isHidden = true
window.addSubview(wv)
}
let url = URL(string: "https://www.ibdb.com/search")!
wv.load(URLRequest(url: url))
}
}
// MARK: - Private
/// Fills the Quick Search bar and submits. IBDB redirects straight to the
/// show page when there is a clear match, so the result URL arrives in
/// the very next didFinish call — no JavaScript extraction required.
private func submitSearch(in wv: WKWebView) {
let safe = showName
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
let js = """
(function() {
var input = document.getElementById('KeywordTop');
if (!input) return 'ERROR: KeywordTop not found';
input.value = '\(safe)';
var btn = document.getElementById('quickSearchTopBtn');
if (!btn) return 'ERROR: quickSearchTopBtn not found';
btn.click();
return 'submitted: ' + input.value;
})()
"""
wv.evaluateJavaScript(js) { result, error in
print("[IBDB] Submit: \(result as? String ?? "nil")")
}
}
private func finish(with url: URL?) {
webView?.removeFromSuperview()
webView?.navigationDelegate = nil
webView = nil
continuation?.resume(returning: url)
continuation = nil
}
} | Code block | See source code for full implementation. |
| extension IBDBService: WKNavigationDelegate {
nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
Task { @MainActor in
let url = webView.url?.absoluteString ?? "?"
print("[IBDB] didFinish: \(url) (phase: \(self.phase))")
switch self.phase {
case .loadingForm:
self.phase = .loadingResults
self.submitSearch(in: webView)
case .loadingResults:
// IBDB redirects directly to the show page — the URL is the result
let result = webView.url
print("[IBDB] Result URL: \(result?.absoluteString ?? "none")")
self.finish(with: result)
}
}
}
nonisolated func webView(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error) {
Task { @MainActor in
print("[IBDB] Navigation failed: \(error.localizedDescription)")
self.finish(with: nil)
}
}
nonisolated func webView(_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error) {
Task { @MainActor in
print("[IBDB] Provisional navigation failed: \(error.localizedDescription)")
self.finish(with: nil)
}
}
} | `IBDBService` extension | Defines the `IBDBService` extension. Conforms to WKNavigationDelegate. |