Skip to content

[Proposal] Migrate iOS layer from Objective-C to Swift #39

Description

@cisen

Summary

I'd like to propose migrating the iOS integration layer from Objective-C to Swift, inspired by the Blinc project's Rust+Swift architecture. After a thorough comparative analysis of both projects, I believe this change would significantly improve safety, maintainability, and long-term viability.

Current Situation

The current iOS integration uses Objective-C (main.m) as the app entry point and relies heavily on Rust-side objc2::msg_send! calls to interact with UIKit APIs. This approach has several systemic issues:

1. Pervasive unsafe Code

Every platform service (clipboard, device_info, battery, etc.) is implemented in Rust using msg_send!, which requires unsafe blocks and provides zero compile-time type checking:

// clipboard/ios.rs — 66 lines, all unsafe
pub fn set_text(text: &str) -> Result<(), String> {
    unsafe {
        let pasteboard: *mut AnyObject = msg_send![class!(UIPasteboard), generalPasteboard];
        let ns_text: *mut AnyObject = msg_send![class!(NSString), alloc];
        let ns_text: *mut AnyObject = msg_send![ns_text,
            initWithBytes: text.as_ptr() as *const c_void,
            length: text.len(), encoding: 4u64];
        let _: () = msg_send![pasteboard, setString: ns_text];
        Ok(())
    }
}

The compiler cannot verify Obj-C message signatures — a wrong return type or missing argument silently corrupts memory at runtime.

2. Runtime Dynamic Class Registration

Custom UIView/UIViewController subclasses are registered at runtime via ClassBuilder:

fn register_metal_view_class() -> &'static AnyClass {
    METAL_VIEW_CLASS_REGISTERED.call_once(|| {
        let mut decl = ClassBuilder::new(c"GPUIMetalView", superclass).unwrap();
        decl.add_ivar::<*mut c_void>(c"gpui_window_ptr");
        decl.add_method(sel!(touchesBegan:withEvent:), touches_began);
        // ...
        decl.register();
    });
}

These dynamically-registered classes:

  • Cannot be debugged in Xcode (no breakpoints, no view hierarchy inspector)
  • Have no compile-time validation of method signatures
  • Fail silently on name collisions or signature mismatches

3. No Bidirectional Communication

The FFI is one-directional: Obj-C calls Rust. When Rust needs platform APIs, it can only use msg_send! — there's no way to call back into Swift/Obj-C code. This means:

  • Complex platform features (camera, share sheet, haptics) must be reimplemented in Rust with msg_send!
  • Cannot leverage Swift ecosystem or third-party libraries
  • Every new platform feature requires more unsafe Rust code

4. Memory Management Risks

Objects created via msg_send! are outside ARC's scope:

let ns_text: *mut AnyObject = msg_send![class!(NSString), alloc];
let ns_text: *mut AnyObject = msg_send![ns_text, initWithBytes:...];
// Who calls release? Manual memory management is error-prone

5. Legacy iOS Lifecycle

The app still uses the traditional UIWindow pattern without UIScene support. Apple has warned:

"UIScene lifecycle will soon be required. Failure to adopt will result in an assert in the future."

6. Objective-C Ecosystem Decline

Apple's focus has fully shifted to Swift. New iOS frameworks (WidgetKit, App Intents, etc.) are Swift-first or Swift-only. The Obj-C developer pool is shrinking.

How Blinc Solves This

The Blinc project uses a Rust+Swift architecture that addresses all of the above issues:

Native Bridge for Bidirectional Communication

Blinc implements a NativeBridge pattern where Swift registers handlers by namespace/function name, and Rust calls them with type-safe NativeValue arguments:

Swift side — native, type-safe, ARC-managed:

BlincNativeBridge.shared.register(namespace: "clipboard", name: "copy") { args in
    let text = args.first as? String ?? ""
    UIPasteboard.general.string = text
    return nil
}

Rust side — clean, no msg_send!:

native_call::<(), _>("clipboard", "copy", (text,))?;

Swift-Native View Controller

All UIKit interaction (touch handling, keyboard management, Metal layer setup) is done natively in Swift:

class BlincViewController: UIViewController {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let ctx = renderContext else { return }
        for touch in touches {
            let point = touch.location(in: view)
            blinc_handle_touch(ctx, touchId, Float(point.x), Float(point.y), 0)
        }
    }
}

No runtime class registration, no ClassBuilder, no msg_send! for touch events.

Modern iOS Lifecycle

Full UISceneDelegate support with SceneDelegate.swift, avoiding Apple's deprecation warnings and enabling multi-window on iPad.

Rich Platform Features

Blinc's Swift layer already implements:

  • Haptic feedback (UIImpactFeedbackGenerator, UINotificationFeedbackGenerator)
  • Camera preview (AVCaptureSession)
  • Audio recording (AVAudioRecorder)
  • Native edit menu (UIMenuController)
  • Share sheet (UIActivityViewController)
  • Keyboard management (hidden UITextField delegate pattern)
  • Dark mode detection, safe area insets, device info

All implemented in Swift with full type safety and ARC — zero unsafe Rust code needed.

Graceful Degradation via dlsym

Optional Swift functions are resolved at runtime, so the Rust code doesn't crash if a Swift handler is missing:

fn keyboard_show_fn() -> Option<extern "C" fn()> {
    static FN: OnceLock<Option<extern "C" fn()>> = OnceLock::new();
    *FN.get_or_init(|| unsafe { lookup_extern_fn(b"blinc_ios_show_keyboard\0") })
}

Proposed Migration Path

This doesn't need to be a big-bang rewrite. A phased approach:

Phase 1: Replace Entry Point

  • Replace main.m with AppDelegate.swift + SceneDelegate.swift
  • Add a Bridging Header declaring existing Rust FFI functions
  • Keep all existing FFI function signatures unchanged

Phase 2: Introduce Native Bridge

  • Implement a NativeBridge pattern for Rust → Swift calls
  • Migrate platform services one by one (start with clipboard, device_info, keyboard)
  • Each migrated service removes msg_send! code from Rust

Phase 3: Swift View Controller

  • Create BlincViewController-style Swift view controller
  • Move touch event handling from Rust dynamic classes to Swift
  • Move Metal layer management to Swift

Phase 4: Remove objc2 Dependency

  • Eliminate all msg_send! calls from Rust
  • Remove ClassBuilder dynamic class registration
  • All platform APIs go through Native Bridge

Comparison Summary

Dimension Current (Obj-C) Proposed (Swift)
Type safety unsafe + msg_send! ✅ Swift compile-time checks
Memory management ❌ Manual (no ARC) ✅ ARC automatic
Debuggability ❌ Dynamic classes invisible to Xcode ✅ Full Xcode support
Bidirectional FFI ❌ One-way only ✅ Native Bridge
iOS lifecycle ❌ Legacy UIWindow ✅ UIScene
Platform features ⚠️ Limited, requires unsafe Rust ✅ Rich, native Swift
Extensibility ❌ New feature = more unsafe code ✅ Register a Swift handler
Obj-C ecosystem ❌ Declining ✅ Swift is Apple's future

Trade-offs Acknowledged

  • Bridging Header maintenance: Every Rust FFI function needs a C declaration in the header. This can be mitigated with code generation (e.g., cbindgen or a build script).
  • JSON serialization overhead: The Native Bridge uses JSON for argument passing. For hot paths (touch events), direct C FFI with primitive types should be used instead (as Blinc does with blinc_handle_touch).
  • Swift runtime size: Adds ~2-5MB for Swift stdlib. Negligible for modern iOS apps.
  • Migration effort: Significant but can be done incrementally with both systems coexisting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions