English | 中文
Real-time Objective-C UI preview for iOS — like Android Compose Preview, powered by InjectionIII.
Annotate any UIViewController with // @Preview, run in the Simulator, make
a code change and save — the preview window updates without rebuilding.
Save .m file
→ FSEventStream detects change (debounced 400 ms)
→ InjectionIII recompiles & dlopen-injects the new implementation
→ INJECTION_BUNDLE_NOTIFICATION fires
→ OCPreviewManager reloads affected OCPreviewWindow(s)
→ NSClassFromString re-instantiates the VC → rendered live
| Item | Version |
|---|---|
| Xcode | 14+ |
| iOS Simulator | 14+ |
| InjectionIII | latest (Mac App Store or GitHub) |
| CocoaPods | 1.12+ (optional) |
| XcodeGen | 2.x (optional, for Example project) |
OCPreview is debug-only. It never ships in production builds.
# Podfile
pod 'OCPreview', :configurations => ['Debug']pod install- Copy the
OCPreview/folder into your project. - Add all
.h/.mfiles to your Debug target only. - Link
CoreServices.framework(weak-link on iOS).
- Install InjectionIII from the Mac App Store or GitHub.
- Launch InjectionIII and open your project directory.
- InjectionIII watches for file saves and compiles changes automatically.
// AppDelegate.m
#ifdef DEBUG
#import <OCPreview/OCPreview.h>
#endif
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#ifdef DEBUG
// Optional: customise before starting
OCPreviewConfig *config = [OCPreviewConfig sharedConfig];
config.position = OCPreviewPositionRight; // Right | Bottom | Center
config.previewSize = CGSizeMake(390, 720);
config.maxPreviewCount = 3;
// SOURCE_ROOT = root of your source tree (add to GCC_PREPROCESSOR_DEFINITIONS)
[[OCPreviewManager sharedManager] startWithSourceDirectory:@SOURCE_ROOT];
#endif
return YES;
}Add SOURCE_ROOT to your Debug build settings:
GCC_PREPROCESSOR_DEFINITIONS (Debug) += SOURCE_ROOT=\"$(SRCROOT)\"
Alternatively pass any absolute path:
startWithSourceDirectory:@"/Users/me/MyApp/Sources".
Place // @Preview on the line immediately before @implementation:
// MyViewController.m
// @Preview
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.systemBlueColor;
// Edit here and ⌘S — preview updates instantly
}
@endMultiple VCs in the same file are supported — annotate each one:
// @Preview
@implementation CardViewController
...
// @Preview
@implementation HeaderViewController
...| Control | Action |
|---|---|
| Drag title bar | Move window anywhere on screen |
| Drag ⌟ corner handle | Resize window |
| ↺ button | Manually reload VC |
| ✕ button | Close preview |
When a compilation error occurs the preview area shows the error inline in red instead of crashing. Fix the error, save — the preview restores automatically.
OCPreviewConfig *config = [OCPreviewConfig sharedConfig];
// Window position relative to main app content
config.position = OCPreviewPositionRight; // default
// Size of the VC preview area (title bar is added on top)
config.previewSize = CGSizeMake(390, 720); // default: 375×667
// Max simultaneously open windows
config.maxPreviewCount = 3; // default
// Auto-reload when InjectionIII fires (set NO to use ↺ only)
config.autoReload = YES; // default
// Background colour of the window chrome
config.containerBackgroundColor = UIColor.systemBackgroundColor;
// Master on/off switch (defaults to YES in DEBUG, NO in Release)
config.enabled = YES;# Clone
git clone https://github.com/your-org/oc-preview.git
cd oc-preview/Example
# Generate the Xcode project (requires XcodeGen)
brew install xcodegen
bash generate_project.sh
# Open
open OCPreviewExample.xcodeproj
# — or, if you ran pod install —
open OCPreviewExample.xcworkspaceThen:
- Select the OCPreviewExample scheme on an iPhone Simulator.
- Press ⌘R to build and run.
- Launch InjectionIII and point it at the
oc-preview/directory. - A floating preview window appears for
SampleViewController. - Edit
SampleViewController.m, press ⌘S — watch the preview update!
OCPreviewManager (singleton)
├── FSEventStream — watches sourceDirectory for .m file saves
│ └── debounce (400 ms) → flushPendingChanges
│ └── OCPreviewScanner.scanFile: → extracts @Preview class names
│ └── ensurePreviewWindowForClass:
│
├── InjectionIII bridge
│ ├── Loads iOSInjection.bundle at startup
│ ├── INJECTION_BUNDLE_NOTIFICATION → reloadViewController (per injected class)
│ └── INJECTION_COMPILE_ERROR → showCompilationError: (all windows)
│
└── previewWindows: { className → OCPreviewWindow }
OCPreviewWindow (UIWindow subclass)
├── Title bar — drag to move, ↺ reload, ✕ close
├── _OCPreviewContainerVC — proper parent VC for correct lifecycle callbacks
│ └── previewChild — the @Preview-annotated VC
├── OCPreviewErrorView — red overlay for compile errors
└── Resize handle (⌟) — pan to resize, clamped to screen + min size
| File | Responsibility |
|---|---|
OCPreviewManager.h/m |
FSEventStream, InjectionIII bridge, window lifecycle, thread safety |
OCPreviewScanner.h/m |
Parses // @Preview annotations from .m source files |
OCPreviewWindow.h/m |
Floating window: drag/resize, VC embedding, error overlay |
OCPreviewErrorView.h/m |
Red error display with formatted compiler output |
OCPreviewConfig.h/m |
All configurable settings, DEBUG-only guard |
Q: Preview doesn't update after I save.
- Is InjectionIII running and connected to your project directory?
- Check the Xcode console for
[OCPreview] Injection succeededor error messages. - Tap the ↺ button to force-reload without InjectionIII.
Q: "Class not found in runtime" error.
- The class hasn't been compiled yet. Build once (⌘B) then save the file.
- Check the class name in
// @Previewmatches@implementationexactly.
Q: Previews work in Simulator but not on device.
- InjectionIII requires a Simulator connection. On-device hot-reload needs InjectionIII's device support.
Q: I want to preview with custom init parameters.
- Add a
+ (instancetype)previewInstanceclass method and override the loading:
// @Preview
@implementation MyViewController
+ (instancetype)previewInstance {
return [[self alloc] initWithModel:[MyModel stub]];
}
...Then in your AppDelegate or a subclass of OCPreviewManager, swap in this factory. (Full custom factory support coming in a future release.)
MIT — see LICENSE.