#!/usr/bin/env python3 """Static inventory of SwiftUI screens in iosApp/iosApp/. Finds every `struct View: View { ... }` or `struct Screen: View { ... }` declaration across the production iOS source tree (excluding generated/build dirs) and categorises them by path. Output schema: { "screens": [ {"name": "LoginView", "path": "iosApp/iosApp/Login/LoginView.swift", "category": "auth"}, ... ] } """ from __future__ import annotations import json import re from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parent.parent SOURCE_ROOT = REPO_ROOT / "iosApp" / "iosApp" OUT_PATH = REPO_ROOT / "docs" / "ios-parity" / "screens.json" # Also scan widget target (HoneyDue/) for completeness. WIDGET_SOURCE_ROOT = REPO_ROOT / "iosApp" / "HoneyDue" EXCLUDED_DIR_PARTS = {"build", "DerivedData", ".build", "Pods"} STRUCT_RE = re.compile( r"^\s*struct\s+([A-Za-z_][A-Za-z0-9_]*(?:View|Screen))\s*:\s*View\s*\{", re.MULTILINE, ) CATEGORY_RULES: list[tuple[str, str]] = [ # path-part substring (case-insensitive) -> category ("Login", "auth"), ("Register", "auth"), ("PasswordReset", "auth"), ("VerifyEmail", "auth"), ("Onboarding", "onboarding"), ("Task", "task"), ("Residence", "residence"), ("Document", "document"), ("Contractor", "contractor"), ("Profile", "profile"), ("Subscription", "subscription"), ("Widget", "widget"), # matches WidgetIconView etc. (HoneyDue/) ("HoneyDue", "widget"), # widget target dir ("Shared", "shared"), ("Core", "shared"), ("Subviews", "shared"), ("Dev", "dev"), ("AnimationTesting", "dev"), ] def category_for(rel_path: Path) -> str: parts_lower = [p.lower() for p in rel_path.parts] for needle, cat in CATEGORY_RULES: if needle.lower() in parts_lower: return cat # filename fallback stem = rel_path.stem.lower() for needle, cat in CATEGORY_RULES: if needle.lower() in stem: return cat return "shared" def should_skip(path: Path) -> bool: return any(part in EXCLUDED_DIR_PARTS for part in path.parts) def find_swift_files(root: Path) -> list[Path]: if not root.is_dir(): return [] out: list[Path] = [] for p in root.rglob("*.swift"): if should_skip(p.relative_to(REPO_ROOT)): continue out.append(p) return sorted(out) def extract_from(path: Path) -> list[dict[str, Any]]: try: text = path.read_text(encoding="utf-8") except (UnicodeDecodeError, OSError): return [] found: list[dict[str, Any]] = [] seen: set[str] = set() for m in STRUCT_RE.finditer(text): name = m.group(1) if name in seen: continue seen.add(name) rel = path.relative_to(REPO_ROOT) found.append( { "name": name, "path": str(rel), "category": category_for(rel), } ) return found def main() -> int: screens: list[dict[str, Any]] = [] for swift_file in find_swift_files(SOURCE_ROOT): screens.extend(extract_from(swift_file)) for swift_file in find_swift_files(WIDGET_SOURCE_ROOT): screens.extend(extract_from(swift_file)) # Sort by category then name for stable output. screens.sort(key=lambda s: (s["category"], s["name"], s["path"])) output = {"screens": screens} OUT_PATH.parent.mkdir(parents=True, exist_ok=True) with OUT_PATH.open("w", encoding="utf-8") as f: json.dump(output, f, indent=2) f.write("\n") print(f"[extract_ios_screens] wrote {OUT_PATH}") print(f" screens={len(screens)}") # category histogram hist: dict[str, int] = {} for s in screens: hist[s["category"]] = hist.get(s["category"], 0) + 1 for cat, n in sorted(hist.items(), key=lambda kv: (-kv[1], kv[0])): print(f" {cat}: {n}") return 0 if __name__ == "__main__": raise SystemExit(main())