// Bundle Pixel harness for iOS: a real app with a real WKWebView. // Create an iOS App project in Xcode (UIKit, Storyboard or not), set the bundle id to // cc.cleartrust.bundlepixel.harness, and replace ViewController.swift with this file. // For a local test bench over http, add NSAppTransportSecurity > NSAllowsArbitraryLoads = YES to Info.plist. import UIKit import WebKit class ViewController: UIViewController { static let defaultServer = "https://bundlepixel.cleartrust.app" let modes = ["plain", "url", "mraid3-env", "mraid3-getenv", "mraid2-macro", "xo"] let stepSeconds = 4.5 let web = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) let server = UITextField() let status = UILabel() let runButton = UIButton(type: .system) var runId = "", base = "", idx = 0 var timer: Timer? var appId: String { Bundle.main.bundleIdentifier ?? "unknown" } var device: String { "\(UIDevice.current.model) · iOS \(UIDevice.current.systemVersion)" } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground server.text = UserDefaults.standard.string(forKey: "server") ?? Self.defaultServer server.borderStyle = .roundedRect server.autocapitalizationType = .none server.autocorrectionType = .no server.keyboardType = .URL server.font = .systemFont(ofSize: 13) runButton.setTitle("Run all", for: .normal) runButton.addTarget(self, action: #selector(runAll), for: .touchUpInside) status.font = .systemFont(ofSize: 12) status.numberOfLines = 0 status.text = "\(appId)\n\(device)" if #available(iOS 16.4, *) { web.isInspectable = true } let bar = UIStackView(arrangedSubviews: [server, runButton]) bar.spacing = 8 let stack = UIStackView(arrangedSubviews: [bar, status, web]) stack.axis = .vertical stack.spacing = 8 stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } @objc func runAll() { base = (server.text ?? "").trimmingCharacters(in: .whitespaces).replacingOccurrences(of: "/+$", with: "", options: .regularExpression) UserDefaults.standard.set(base, forKey: "server") runId = "i" + String(Int(Date().timeIntervalSince1970 * 1000), radix: 36) idx = 0 runButton.isEnabled = false next() } func next() { guard idx < modes.count else { status.text = "Done · run \(runId) · results below and on the test bench" if let u = URL(string: "\(base)/real/report.html?run=\(runId)") { web.load(URLRequest(url: u)) } runButton.isEnabled = true return } let mode = modes[idx] idx += 1 status.text = "Running \(idx)/\(modes.count): \(mode) · run \(runId)" load(mode) timer = Timer.scheduledTimer(withTimeInterval: stepSeconds, repeats: false) { [weak self] _ in self?.next() } } func enc(_ s: String) -> String { s.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? s } func load(_ mode: String) { let q = "mode=\(mode)&run=\(runId)&dev=\(enc(device))&hp=\(enc(appId))&plat=ios" switch mode { case "plain", "xo": if let u = URL(string: "\(base)/real/page.html?\(q)") { web.load(URLRequest(url: u)) } case "url": if let u = URL(string: "\(base)/real/page.html?\(q)&bundle=\(enc(appId))") { web.load(URLRequest(url: u)) } default: // Like an ad SDK: fetch the ad markup, wrap it with the MRAID bridge, load it with a base URL. guard let u = URL(string: "\(base)/real/creative.html?\(q)") else { return } URLSession.shared.dataTask(with: u) { data, _, err in var html = data.flatMap { String(data: $0, encoding: .utf8) } ?? "

could not fetch creative: \(String(describing: err))

" if mode == "mraid2-macro" { html = html.replacingOccurrences(of: "%%APP_BUNDLE%%", with: self.appId) } let version = mode.hasPrefix("mraid2") ? "2.0" : "3.0" let env = mode == "mraid3-env" ? "" : "" let page = "" + "" + env + "\(html)" DispatchQueue.main.async { self.web.loadHTMLString(page, baseURL: URL(string: "\(self.base)/real/sdk")) } }.resume() } } }