import UIKit import CoreML class ViewController: UIViewController { // MARK: - UI private let scrollView = UIScrollView() private let logLabel = UILabel() private let stackView = UIStackView() private var logText = "" { didSet { DispatchQueue.main.async { self.logLabel.text = self.logText let bottom = CGPoint(x: 0, y: max(0, self.scrollView.contentSize.height - self.scrollView.bounds.height)) self.scrollView.setContentOffset(bottom, animated: false) } } } // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground setupUI() appendLog("App ready.") appendLog("iOS \(UIDevice.current.systemVersion)") appendLog("Tap a test button to begin.") } // MARK: - Test entry points @objc private func testSync_all() { runSyncTest(units: .all, label: "Sync .all") } @objc private func testSync_cpu() { runSyncTest(units: .cpuOnly, label: "Sync .cpuOnly") } @objc private func testSync_cpuGPU() { runSyncTest(units: .cpuAndGPU, label: "Sync .cpuAndGPU") } @objc private func testAsync_all() { runAsyncTest(units: .all, label: "Async .all") } @objc private func clearLog() { logText = "" FileLogger.shared.clearLog() appendLog("Log cleared.") } @objc private func shareLog() { let url = FileLogger.shared.getLogURL() let ac = UIActivityViewController(activityItems: [url], applicationActivities: nil) present(ac, animated: true) } // MARK: - Sync load (background thread) private func runSyncTest(units: MLComputeUnits, label: String) { appendLog("─────────────────────────────") appendLog("TEST: \(label)") DispatchQueue.global(qos: .userInitiated).async { self.appendLog("Thread: background ✓") let config = MLModelConfiguration() config.computeUnits = units self.appendLog("Config created — computeUnits=\(units.rawValue)") guard let modelURL = Bundle.main.url(forResource: "MDNA_GaitEncoder_v1_3", withExtension: "mlmodelc") else { self.appendLog("❌ ERROR: mlmodelc not found — add .mlpackage to Xcode target first") return } self.appendLog("Model URL found: \(modelURL.lastPathComponent)") self.appendLog("Calling MLModel(contentsOf:) ...") let t0 = Date() log("[\(label)] load START") do { let model = try MLModel(contentsOf: modelURL, configuration: config) let elapsed = Date().timeIntervalSince(t0) self.appendLog("✅ SUCCESS — \(String(format: "%.3f", elapsed))s") self.appendLog("Inputs: \(model.modelDescription.inputDescriptionsByName.keys)") log("[\(label)] load SUCCESS \(String(format: "%.3f", elapsed))s") } catch { let elapsed = Date().timeIntervalSince(t0) self.appendLog("❌ ERROR after \(String(format: "%.3f", elapsed))s") self.appendLog("\(error)") log("[\(label)] load ERROR \(elapsed)s: \(error)") } } } // MARK: - Async load private func runAsyncTest(units: MLComputeUnits, label: String) { appendLog("─────────────────────────────") appendLog("TEST: \(label)") let config = MLModelConfiguration() config.computeUnits = units appendLog("Config created — computeUnits=\(units.rawValue)") guard let modelURL = Bundle.main.url(forResource: "MDNA_GaitEncoder_v1_3", withExtension: "mlmodelc") else { appendLog("❌ ERROR: mlmodelc not found — add .mlpackage to Xcode target first") return } appendLog("Model URL found: \(modelURL.lastPathComponent)") appendLog("Calling MLModel.load(contentsOf:) async ...") let t0 = Date() log("[\(label)] async load START") MLModel.load(contentsOf: modelURL, configuration: config) { result in let elapsed = Date().timeIntervalSince(t0) switch result { case .success(let model): self.appendLog("✅ SUCCESS — \(String(format: "%.3f", elapsed))s") self.appendLog("Inputs: \(model.modelDescription.inputDescriptionsByName.keys)") log("[\(label)] async load SUCCESS \(String(format: "%.3f", elapsed))s") case .failure(let error): self.appendLog("❌ ERROR after \(String(format: "%.3f", elapsed))s") self.appendLog("\(error)") log("[\(label)] async load ERROR \(elapsed)s: \(error)") } } } // MARK: - Log helper private func appendLog(_ msg: String) { log(msg) DispatchQueue.main.async { self.logText += msg + "\n" } } // MARK: - UI Setup private func setupUI() { // iOS version banner let banner = UILabel() banner.text = "iOS \(UIDevice.current.systemVersion)" banner.font = .monospacedSystemFont(ofSize: 13, weight: .bold) banner.textAlignment = .center banner.backgroundColor = .systemYellow banner.translatesAutoresizingMaskIntoConstraints = false view.addSubview(banner) // Button stack stackView.axis = .vertical stackView.spacing = 8 stackView.translatesAutoresizingMaskIntoConstraints = false let buttons: [(String, Selector, UIColor)] = [ ("Sync Load — .all", #selector(testSync_all), .systemBlue), ("Sync Load — .cpuOnly", #selector(testSync_cpu), .systemIndigo), ("Sync Load — .cpuAndGPU", #selector(testSync_cpuGPU), .systemPurple), ("Async Load — .all", #selector(testAsync_all), .systemGreen), ] for (title, action, color) in buttons { let btn = UIButton(type: .system) btn.setTitle(title, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .semibold) btn.backgroundColor = color btn.setTitleColor(.white, for: .normal) btn.layer.cornerRadius = 8 btn.heightAnchor.constraint(equalToConstant: 44).isActive = true btn.addTarget(self, action: action, for: .touchUpInside) stackView.addArrangedSubview(btn) } // Utility row let utilRow = UIStackView() utilRow.axis = .horizontal utilRow.spacing = 8 utilRow.distribution = .fillEqually utilRow.heightAnchor.constraint(equalToConstant: 44).isActive = true let clearBtn = UIButton(type: .system) clearBtn.setTitle("Clear Log", for: .normal) clearBtn.backgroundColor = .systemGray clearBtn.setTitleColor(.white, for: .normal) clearBtn.layer.cornerRadius = 8 clearBtn.addTarget(self, action: #selector(clearLog), for: .touchUpInside) let shareBtn = UIButton(type: .system) shareBtn.setTitle("🐞 Share Log", for: .normal) shareBtn.backgroundColor = .systemOrange shareBtn.setTitleColor(.white, for: .normal) shareBtn.layer.cornerRadius = 8 shareBtn.addTarget(self, action: #selector(shareLog), for: .touchUpInside) utilRow.addArrangedSubview(clearBtn) utilRow.addArrangedSubview(shareBtn) stackView.addArrangedSubview(utilRow) view.addSubview(stackView) // Log scroll view scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.backgroundColor = .secondarySystemBackground scrollView.layer.cornerRadius = 8 view.addSubview(scrollView) logLabel.numberOfLines = 0 logLabel.font = .monospacedSystemFont(ofSize: 11, weight: .regular) logLabel.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(logLabel) let g = view.safeAreaLayoutGuide NSLayoutConstraint.activate([ banner.topAnchor.constraint(equalTo: g.topAnchor), banner.leadingAnchor.constraint(equalTo: view.leadingAnchor), banner.trailingAnchor.constraint(equalTo: view.trailingAnchor), banner.heightAnchor.constraint(equalToConstant: 28), stackView.topAnchor.constraint(equalTo: banner.bottomAnchor, constant: 12), stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16), stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16), scrollView.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 12), scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16), scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16), scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -12), logLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 8), logLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 8), logLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -8), logLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -8), logLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -16), ]) } }