Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guide on extending React for iOS #114

Closed
jaygarcia opened this issue Mar 2, 2015 · 8 comments
Closed

Guide on extending React for iOS #114

jaygarcia opened this issue Mar 2, 2015 · 8 comments
Labels
Resolution: Locked This issue was locked by the bot.

Comments

@jaygarcia
Copy link
Contributor

I've spent a few hours diving through the source in both the JavaScript side and ObjC side and things are still unclear.

What I'd like to do is extend the basic View class, where I can inject an implementation of EZAudioPlot (Open GL version) in addition to a custom-compiled (soon to be open sourced) iOS wrapped implementation of LibGME.

The goal of this is to demonstrate the extensibility of React Native, going well above and beyond native Cocoa Touch components. The demo application will play chip tunes from popular game systems, powered by LibGME, a wrapper for many popular game system sound emulators.

I did something similar a few years back with Sencha Touch & PhoneGap for the Mod scene; See the following URL and fast forward to 23:18 https://vimeo.com/75277828 if you're interested.

The prototype for this is working perfectly in iOS ObjC Proper: http://screencast.com/t/I28k5OD94

Any guidance for this would be much appreciated.

@joewood
Copy link

joewood commented Mar 13, 2015

👍 If the bridge and view registration could be described in the docs directory that would be really useful. I'm doing some rudimentary opengl tests and some guidance would help. May also be useful to #34.

@sahrens
Copy link
Contributor

sahrens commented Mar 14, 2015

We definitely need some docs about how the bridge works - there is definitely some dark magic going on in there. A simple example/tutorial with a walk through of what actually happens under the covers would be awesome (internally and for the community). We'll definitely wrote something, hopefully soon.

On Mar 13, 2015, at 1:00 PM, Joe Wood notifications@github.com wrote:

If the bridge and view registration could be described in the docs directory that would be really useful. I'm doing some rudimentary opengl tests and some guidance would help. May also be useful to #34.


Reply to this email directly or view it on GitHub.

@jaygarcia
Copy link
Contributor Author

The one thing i'll say, @sahrens is that the idea of RCT_CUSTOM_VIEW_PROPERTY as a macro for managers to do things on views seems a little strange to me. :-\

Maybe i'm too accustomed to being able to execute methods directly on views.

@joewood , here's how i've been able to get things to work (hard to know if this is a hack or not). Keep a keen eye on the usage of RCT_CUSTOM_VIEW_PROPERTY

VIEW header:

#import "RCTView.h"
#import "../../../../EZAudio/EZAudio/EZAudioPlotGL.h"

@interface RCEzPlotGlView : RCTView {
    EZAudioPlotGL *plotter;
}

- (void) update:(float[])data;
- (void) react_updateClippedSubviewsWithClipRect;
@end

VIEW program:


#import "RCEzPlotGlView.h"

@implementation RCEzPlotGlView


- (id)initWithFrame:(CGRect)frame {
    NSLog(@"%@ %@()",  NSStringFromClass([self class]), NSStringFromSelector(_cmd));
    self = [super initWithFrame:frame];


    self.backgroundColor = [UIColor colorWithRed:1 green:1 blue:0 alpha:1];


    return self;
}

// Called via manager
- (void) update:(float[])data {
    RCEzPlotGlView *myView = self;

    CGRect bounds = myView.bounds;

    if (! plotter) {
        NSLog(@"%@ created an EZAudioPlotGL",  NSStringFromClass([self class]));
        plotter = [[EZAudioPlotGL alloc] initWithFrame:self.bounds];
        plotter.backgroundColor = [UIColor colorWithRed:1 green:1 blue:0 alpha:1];
        plotter.plotType = EZPlotTypeBuffer;
        [self addSubview:plotter];

    }
    [plotter updateBuffer:data withBufferSize:512];
}

@end

VIEW MANAGER header:


#import "RCTViewManager.h"
#import "RCEzPlotGlView.h"
#import "RCGmePlayer.h"

@interface RCEzPlotGlViewManager : RCTViewManager


@end

VIEW MANAGER program:


#import "RCEzPlotGlViewManager.h"

@implementation RCEzPlotGlViewManager

- (UIView *)view {
   NSLog(@"%@ %@",  NSStringFromClass([self class]),  NSStringFromSelector(_cmd));

  return [[RCEzPlotGlView alloc] init];
}

RCT_CUSTOM_VIEW_PROPERTY(doDraw, RCEzPlotGlView *) {
    NSArray *nsArrayData = [[RCGmePlayer sharedManager] getBufferData:(NSString *)json];

    int i = 0,
        len = 512;

    float data[len];

    for (; i < len; i++) {
        NSNumber *number = nsArrayData[i];

        data[i] = [number floatValue];
    }


    [view update:data];
}

@end

JavaScript view definition:

'use strict';

var NativeMethodsMixin     = require('NativeMethodsMixin'),
    NativeModules          = require('NativeModules'),
    PropTypes              = require('ReactPropTypes'),
    React                  = require('React'),
    ReactIOSViewAttributes = require('ReactIOSViewAttributes'),
    StyleSheetPropType     = require('StyleSheetPropType'),
    ViewStylePropTypes     = require('ViewStylePropTypes'),
    merge = require('merge');


var StyleConstants = NativeModules.RKUIManager.StyleConstants;

var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass');

var stylePropType = StyleSheetPropType(ViewStylePropTypes);



var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, {
    __SOMETHING__ : true // Ignore this. It was for testing.
});

var View = React.createClass({
    statics : {
        pointerEvents: StyleConstants.PointerEventsValues,
        stylePropType
    },

    mixins: [NativeMethodsMixin],

    viewConfig: {
        uiViewClassName: 'RCEzPlotGlView',
        validAttributes: CommonImageViewAttributes
    },

    render: function() {
        return <RCEzPlotGlView {...this.props}/>;
    },
    setDoDraw : function(channel) {        
        this.setNativeProps({doDraw : channel});
    }
});


var RCEzPlotGlView = createReactIOSNativeComponentClass({
    validAttributes: CommonImageViewAttributes,
    uiViewClassName: 'RCEzPlotGlView',
});

var ViewToExport = RCEzPlotGlView;
if (__DEV__) {
  ViewToExport = View;
}

ViewToExport.pointerEvents = View.pointerEvents;
ViewToExport.stylePropType = stylePropType;

module.exports = ViewToExport;

VIEW JavaScript imp (truncated)l:


        return (
            <View style={styles.container}>
                <View style={styles.titleBar}>
                    <Text style={{fontSize: 20}}>{gameObj.game}</Text>
                </View>

                <ScrollView horizontal={true} style={styles.imageContainer} pagingEnabled={true} centerContent={true}>
                    {images}
                </ScrollView>

                <View style={styles.vizContainer}>
                    <RCEzAudioPlotGlView style={styles.vizItem}/>
                    <RCEzAudioPlotGlView style={styles.vizItem}/>
                </View>

                <View style={styles.controlsContainer}>
                    <Text style={styles.timeText}>
                        Current Time : {state.currentTime}
                    </Text>
                    <Text style={styles.trackText}>
                        {state.currentTrack + 1}  of {gameObj.trackCount + 1}
                    </Text>
                    <ControlButton onPress={this.onButtonPress} btnChar={"prev"} btnStyle={"prevButton"}/>
                    <ControlButton onPress={this.onButtonPress} btnChar={centerBtnChar} btnStyle={centerBtnStyle}/>
                    <ControlButton onPress={this.onButtonPress} btnChar={"next"} btnStyle={"nextButton"}/>
                </View>                     
            </View>
        );
... Listening to onButton press gives me an opportunity to cache the reference to the two instances of RCEzAudioPlotGlView classes:


     leftViz : null,
     rightViz : null,

    onButtonPress : function(buttonType) {
        // var o = this._renderedComponent._renderedChildren['.3']._renderedChildren['.2'];
        var viewContainer = this._reactInternalInstance._renderedComponent._renderedComponent._renderedChildren['.2'],
            children      = viewContainer._renderedComponent._renderedChildren,
            leftViz       = children['.0'],
            rightViz      = children['.1'];

        // leftViz._instance.something();
        // rightViz._instance.something();

        this.leftViz  = leftViz;
        this.rightViz = rightViz;

        var methodName = this.methodMap[buttonType];
        this[methodName] && this[methodName]();
    },

... Here's where you see the instance setDoDraw methods being called. This is where the manager's RCT_CUSTOM_VIEW_PROPERTY macro comes into play.

    getSampleData : function() {
        var me = this,
            descriptors = this.sampleDataDescriptors;

        this.leftViz._instance.setDoDraw('l');
        this.rightViz._instance.setDoDraw('r');
    },

@nicklockwood
Copy link
Contributor

The idea in React is that views are immutable, and updated only by modifying state, which triggers a re-render of the view. There are numerous ways to circumvent this for stuff like animation, but really that's a hack and not in the spirit of the API, which is why we don't simply expose methods on views, as we do on view managers.

@jaygarcia Instead of calling your property "doDraw" (implying an action), why not call it "bufferData", and set it in the render function of your RCEzPlotGlView.js? The actual native implementation of the property would be the same, but you would update it by triggering a re-render of the view instead of by directly manipulating the property.

@jaygarcia
Copy link
Contributor Author

@nicklockwood Thanks for the feedback. Would you say that a view Manager is synonymous to a "controller"? I ask because it kinda feels that way :)

This was more of an experimental pass and I agree that the name was off and it's been changed. I wasn't happy with the performance of the JavaScript <> ObjC calling 'draw' every 20ms or so. the CPU utilization was way high, so I decided to go to a threaded model where each Oscilloscope has its own thread. I just now need to inject mutex locks in some places because there are a few runloops, iOS, react, audio engine... ;). Oh the joy!

Btw, do you have any feedback on the following pattern? It feels really dirty to have to descend into properties like this to get a reference to a view. :(

 var viewContainer = this._reactInternalInstance._renderedComponent._renderedComponent._renderedChildren['.2'],
            children      = viewContainer._renderedComponent._renderedChildren,
            leftViz       = children['.0'],
            rightViz      = children['.1'];

@nicklockwood
Copy link
Contributor

@jaygarcia yes, a ViewManager is very much a view controller. I considered changing the naming convention to "Controller", but that already has a meaning in iOS and in some cases we're implementing iOS-style ViewControllers as well, so it would have led to name collisions.

Also, unlike iOS view controllers, ViewManagers are effectively singletons, with a single ViewManager instance responsible for all of the views that it creates. Views can easily delegate back to their ViewManager, but it's (deliberately) harder for ViewManagers to call methods directly on the views they create, because they don't retain references to them.

Digging through the whole view hierarchy manually to find a component to call setProps shouldn't be necessary, but I'm not the right person to say what the current recommended approach is for finding a JS component in the hierarchy. Since setProps takes a reactID, I think you'd probably want to make a note of the reactID for the component in questions and then put the setDoDraw() method somewhere more convenient, rather than on the component itself. But I'm not sure about that. Maybe @sahrens can help?

@frantic
Copy link
Contributor

frantic commented Mar 22, 2015

Btw, do you have any feedback on the following pattern? It feels really dirty to have to descend into properties like this to get a reference to a view. :(

Set a ref="leftVis" on the view, then use this.refs.leftVis to get the element. More info http://facebook.github.io/react/docs/more-about-refs.html

@jaygarcia
Copy link
Contributor Author

@frantic dude!! Thanks =D

@vjeux vjeux closed this as completed Apr 1, 2015
@facebook facebook locked as resolved and limited conversation to collaborators May 29, 2018
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Jul 23, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

No branches or pull requests

7 participants