|
| 1 | +# UI Modernization Plan |
| 2 | + |
| 3 | +Companion to [MODERNIZATION.md](MODERNIZATION.md) (Theater/actor removal). That doc is about |
| 4 | +the concurrency layer; this one is about the UI layer: killing the last UIKit shells, |
| 5 | +Objective-C files, and dead code left over from the storyboard era. |
| 6 | + |
| 7 | +Each PR below is a small, independently shippable slice. The **worklog** at the bottom |
| 8 | +collects raw material (numbers, before/after, war stories) for a blog post about |
| 9 | +modernizing this app with Claude (Fable). |
| 10 | + |
| 11 | +## Where the app actually is (inventory, 2026-07-05) |
| 12 | + |
| 13 | +Better than expected: |
| 14 | + |
| 15 | +- **Zero storyboards, zero xibs.** Launch screen is the `UILaunchScreen` plist dict. |
| 16 | +- **UIScene lifecycle fully adopted**; `AppDelegate` is minimal, window built in `SceneDelegate`. |
| 17 | +- **Deprecated-API greps come back clean**: no `keyWindow`, `statusBarOrientation`, |
| 18 | + `UIAlertView`, `AVCaptureStillImageOutput`, `openURL(_:)`. |
| 19 | +- **Four of five screens are already SwiftUI**, hosted by thin UIKit shells: |
| 20 | + `WelcomeView`, `RolePickerView`, `DeviceScannerView`, `MonitorView`. |
| 21 | + |
| 22 | +What's left: |
| 23 | + |
| 24 | +| Surface | Size | Blocker | |
| 25 | +|---|---|---| |
| 26 | +| Dead code (old permission system, orphaned IB controller, stubs, unused extensions) | ~10 files | none — delete | |
| 27 | +| Objective-C: `CPSoundManager`, `RCTimer` (+ app bridging header) | 3 files | none — trivial ports | |
| 28 | +| UIKit shells: `WelcomeViewController`, `RolePickerController` | ~320 lines | thin coordinators | |
| 29 | +| `DeviceScannerViewController` | 367 lines | owns actor lifecycle; is `ViewCtrlActor<Self>` target | |
| 30 | +| `MonitorViewController` + glue | ~820 lines | `MonitorActor: ViewCtrlActor<MonitorViewController>` calls the concrete VC | |
| 31 | +| `CameraViewController` | 1729 lines | AVFoundation capture graph; no SwiftUI view exists | |
| 32 | +| `WatchRemoteCameraController` | 322 lines | embeds CameraViewController as child | |
| 33 | + |
| 34 | +The structural blocker for the hard tier is Theater's `ViewCtrlActor<ConcreteVC>` generic |
| 35 | +binding — actors are type-welded to UIKit view controllers. That work overlaps with |
| 36 | +MODERNIZATION.md Phases 4–5. |
| 37 | + |
| 38 | +## PR sequence |
| 39 | + |
| 40 | +### PR 1: Dead code sweep — **this PR** |
| 41 | +Pure deletion, no behavior change. |
| 42 | + |
| 43 | +Files deleted (all verified zero callers): |
| 44 | +- `CameraAccess.swift` — the pre-`PermissionManager` permission system, entirely superseded |
| 45 | +- `RolePickerOptionController.swift` — IBOutlet controller whose xib died long ago |
| 46 | +- `PopoverController.swift` — SwiftUI "Hello, World!" stub |
| 47 | +- `TorchTest.swift` — diagnostic scaffolding |
| 48 | +- `UIOrientationHelpers.swift` — literally empty (header comment + import) |
| 49 | +- `CGImage.swift` — unused `rotated(by:)` |
| 50 | +- `UIViewController.swift` — unused child-VC helpers (call sites use `addChild` directly) |
| 51 | +- `UIButton.swift` — `styleButton`, only caller was `RolePickerOptionController` |
| 52 | +- `UIView.swift` — `roundCorners`/`styleEmbeddedView`, only caller was `styleButton` |
| 53 | +- `UIImage+ImageProcessing.h/.m` — old ObjC frame path, replaced by `JPEGFrameEncoder`/`HEICFrameEncoder` |
| 54 | +- `RemoteShutterTests-Bridging-Header.h`, `RemoteShutterUITests-Bridging-Header.h` — empty |
| 55 | + |
| 56 | +Members trimmed from live files: |
| 57 | +- `OrientationUtils`: `transformToUIKit`, `transformToUIImage`, `transformOrientationToImage` |
| 58 | +- `UIImage+gif`: `gifImageWithURL` |
| 59 | + |
| 60 | +Plus: drop `UIImage+ImageProcessing.h` from the app bridging header, clear the test |
| 61 | +targets' `SWIFT_OBJC_BRIDGING_HEADER` settings, fix stale storyboard claims in CLAUDE.md. |
| 62 | + |
| 63 | +### PR 2: Retire the Objective-C |
| 64 | +Port `CPSoundManager` (countdown beeps, `AVAudioPlayer`) and `RCTimer` |
| 65 | +(recursive-`dispatch_after` countdown) to small Swift types; delete |
| 66 | +`RemoteCam-Bridging-Header.h`. Zero ObjC left in the app target. |
| 67 | + |
| 68 | +### PR 3: Collapse the easy shells |
| 69 | +Delete `iAdViewController` (empty base class), fold `WelcomeViewController` down, |
| 70 | +slim `RolePickerController`; move the `RemoteCamSystem.shared` declaration out of it. |
| 71 | + |
| 72 | +### PR 4: Decouple actors from concrete VCs |
| 73 | +Re-target `ViewCtrlActor` bindings at protocols/view models instead of concrete VC types |
| 74 | +(coordinates with MODERNIZATION.md Phases 4–5). Unblocks DeviceScanner and Monitor shells. |
| 75 | + |
| 76 | +### PR 5+: Camera surface |
| 77 | +SwiftUI chrome around a `UIViewRepresentable` preview layer for `CameraViewController`; |
| 78 | +`WatchRemoteCameraController` follows. |
| 79 | + |
| 80 | +## Worklog (blog raw material) |
| 81 | + |
| 82 | +### PR 1 — dead code sweep |
| 83 | +- Inventory ran as two parallel read-only agents: one walked every view controller, |
| 84 | + the other chased ObjC/bridging headers/deprecated APIs. ~30 min wall clock. |
| 85 | +- Expected storyboards to hunt; found none. The docs said "DeviceScannerViewController — |
| 86 | + UIKit + Storyboard, entry point" — the code said otherwise. Docs lie, greps don't. |
| 87 | +- Best find: an entire permission system (`CameraAccess.swift`) still compiling, |
| 88 | + fully replaced, zero callers — each method politely apologizing in comments that |
| 89 | + the new system had taken over. |
| 90 | +- Dead-code cascade: deleting one orphaned IB controller made `UIButton.swift` dead, |
| 91 | + which made `UIView.swift` dead. Delete one leaf, the branch comes with it. |
| 92 | +- Two "dead" files (`PopoverController.swift`, `TorchTest.swift`) turned out not to be |
| 93 | + in the pbxproj at all — orphaned on disk, never even compiling. |
| 94 | +- Plot twist: removing `UIImage+ImageProcessing.h` from the bridging header broke the |
| 95 | + build in 10 files that never imported UIKit. Its `#import <UIKit/UIKit.h>` had been |
| 96 | + silently granting UIKit to every Swift file in the target for years. The dead code |
| 97 | + was load-bearing — not for what it did, but for what it imported. Fix: explicit |
| 98 | + `import UIKit` in the 10 files that were freeloading. |
| 99 | +- Net: 27 files changed, +136/−557 lines; 13 files deleted; 348/348 tests green. |
0 commit comments