madsmtm / objc2

Bindings to Apple's frameworks in Rust
https://docs.rs/objc2/
MIT License
368 stars 39 forks source link

Add closure wrapper to allow more easily calling `action:target:`-like methods? #561

Open madsmtm opened 9 months ago

madsmtm commented 9 months ago

It is common in AppKit and UIKit to have methods that expect to run a selector on an object. These are usually legacy methods that were introduced before blocks were added to / common in Objective-C, or work this way because the event bubbles upwards.

Examples include NSMenuItem, NSNotificationCenter and UIGestureRecognizer.

It would maybe be nice if we had some object wrapper around closures, such that one didn't have to go through the whole of declare_class! just to handle an event?

I'm thinking something similar to:

// Implementation

unsafe trait Wrapable {
    fn _get_sel() -> Sel;
}
impl Wrapable for Fn() -> R { ... }
impl Wrapable for Fn(T) -> R { ... }
impl Wrapable for Fn(T, U) -> R { ... }
// ...

fn wrap<F: 'static + Wrapable>(f: F) -> (Id<NSObject>, Sel) {
    extern "C" fn method<F: Wrapable>(obj: &NSObject, ...args) -> R {
        (IVAR.load(obj))(...args)
    }

    let sel = F::_get_sel();
    let cls = { // Wrapped in a Once somehow?
        // Unsure if TypeId is unique enough for different generics?
        let builder = ClassBuilder::new(format!("WrappedClosure_{:?}", TypeId::of::<F>())).unwrap();
        builder.add_ivar::<F>("closure");
        builder.add_method(sel, method::<F>);
        builder.register()
    };

    static IVAR = cls.instance_variable("closure").unwrap();

    let obj: Id<NSObject> = msg_send_id![cls, new];
    IVAR.load_ptr(&obj).write(f);

    (obj, sel)
}

// Usage
let center = DistributedNotificationCenter::defaultCenter();
let (obj, sel) = wrap(|notification: &NSNotification| {
    // handle notification
});
center.addObserver_selector_name_object(obj, sel, ns_string!("AppleInterfaceThemeChangedNotification"), None)
eks5115 commented 6 months ago

When leaving this scope, obj will be released and sel will not be called. How to solve.

madsmtm commented 6 months ago

-[NSMenuItem target] is a weak property, which means that it will not by itself prevent the object from being deallocated.

You will have to keep the object that you want to use as the target around manually, probably by storing the Id<...> in a struct somewhere (depends on your setup).

eks5115 commented 6 months ago

Thank you. I know what to do, okay