From c5d367ad39e6c748816b7a42b2a76d1a54a9ac33 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 20 Apr 2015 22:42:02 -0700 Subject: [PATCH] Add Native Components guide. --- docs/NativeComponentsIOS.md | 340 ++++++++++++++++++++++++++++++++++++ docs/NativeModulesIOS.md | 66 +++---- 2 files changed, 375 insertions(+), 31 deletions(-) create mode 100644 docs/NativeComponentsIOS.md diff --git a/docs/NativeComponentsIOS.md b/docs/NativeComponentsIOS.md new file mode 100644 index 00000000000000..14a186fe4829fa --- /dev/null +++ b/docs/NativeComponentsIOS.md @@ -0,0 +1,340 @@ +--- +id: nativecomponentsios +title: Native UI Components (iOS) +layout: docs +category: Guides +permalink: docs/nativecomponentsios.html +next: linking-libraries +--- + +There are tons of native UI widgets out there ready to be used in the latest apps - some of them are part of the platform, others are available as third-party libraries, and still more might be in use in your very own portfolio. React Native has several of the most critical platform components already wrapped, like `ScrollView` and `TextInput`, but not all of them, and certainly not ones you might have written yourself for a previous app. Fortunately, it's quite easy to wrap up these existing components for seamless integration with your React Native application. + +Like the native module guide, this too is a more advanced guide that assumes you are somewhat familiar with iOS programming. This guide will show you how to build a native UI component, walking you through the implementation of a subset of the existing `MapView` component available in the core React Native library. + +## iOS MapView example + +Let's say we want to add an interactive Map to our app - might as well use [`MKMapView`](https://developer.apple.com/library/prerelease/mac/documentation/MapKit/Reference/MKMapView_Class/index.html), we just need to make it usable from JavaScript. + +Native views are created and manipulated by subclasses of `RCTViewManager`. These subclasses are similar in function to view controllers, but are essentially singletons - only one instance of each is created by the bridge. They vend native views to the `RCTUIManager`, which delegates back to them to set and update the properties of the views as necessary. The `RCTViewManager`s are also typically the delegates for the views, sending events back to JavaScript via the bridge. + +Vending a view is simple: +- Create the basic subclass. +- Add the `RCT_EXPORT_MODULE()` marker macro. +- Implement the `-(UIView *)view` method + +```objective-c +// RCTMapManager.m +#import + +#import "RCTViewManager.h" + +@interface RCTMapManager : RCTViewManager +@end + +@implementation RCTMapManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[MKMapView alloc] init]; +} + +@end +``` + +Then you just need a little bit of JavaScript to make this a usable React component: + +```javascript +// MapView.js + +var { requireNativeComponent } = require('react-native'); + +module.exports = requireNativeComponent('RCTMap', null); +``` + +This is now a fully-functioning native map view component in JavaScript, complete with pinch-zoom and other native gesture support. We can't really control it from JavaScript yet, though :( + +## Properties + +The first thing we can do to make this component more usable is to bridge over some native properties. Let's say we want to be able to disable pitch control specify the visible region. Disabling pitch is a simple boolean, so we just add this one line: + +```objective-c +// RCTMapManager.m +RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL) +``` + +Note that we explicitly specify the type as `BOOL` - React Native uses `RCTConvert` under the hood to convert all sorts of different data types when talking over the bridge, and bad values will show convenient "RedBox" errors to let you know there is an issue ASAP. When things are straightforward like this, the whole implementation is taken care of for you by this macro. + +Now to actually disable pitch, we just set the property in JS: + +```javascript +// MyApp.js + +``` + +This isn't very well documented though - in order to know what properties are available and what values they accept, the client of your new component needs to dig through the Objective-C code. To make this better, let's make a wrapper component and document the interface with React `PropTypes`: + +```javascript +// MapView.js +var React = require('react-native'); +var { requireNativeComponent } = React; + +class MapView extends React.Component { + render() { + return ; + } +} + +var RCTMap= requireNativeComponent('RCTMap', MapView); + +MapView.propTypes = { + /** + * When this property is set to `true` and a valid camera is associated + * with the map, the camera’s pitch angle is used to tilt the plane + * of the map. When this property is set to `false`, the camera’s pitch + * angle is ignored and the map is always displayed as if the user + * is looking straight down onto it. + */ + pitchEnabled = React.PropTypes.bool, +}; + +module.exports = MapView; +``` + +Now we have a nicely documented wrapper component that is easy to work with. Note that we changed the second argument to `requireNativeComponent` from `null` to the new `MapView` wrapper component. This allows the infrastructure to verify that the propTypes match the native props to reduce the chances of mismatches between the ObjC and JS code. + +Next, let's add the more complex `region` prop. We start by adding the native code: + +```objective-c +// RCTMapManager.m +RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) +{ + [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES]; +} +``` + +Ok, this is clearly more complicated than the simple `BOOL` case we had before. Now we have a `MKCoordinateRegion` type that needs a conversion function, and we have custom code so that the view will animate when we set the region from JS. There is also a `defaultView` that we use to reset the property back to the default value if JS sends us a null sentinel. + +You could of course write any conversion function you want for your view - here is the implementation for `MKCoordinateRegion` via two categories on `RCTConvert`: + +```objective-c +@implementation RCTConvert(CoreLocation) + +RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue); +RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue); + ++ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json +{ + json = [self NSDictionary:json]; + return (CLLocationCoordinate2D){ + [self CLLocationDegrees:json[@"latitude"]], + [self CLLocationDegrees:json[@"longitude"]] + }; +} + +@end + +@implementation RCTConvert(MapKit) + ++ (MKCoordinateSpan)MKCoordinateSpan:(id)json +{ + json = [self NSDictionary:json]; + return (MKCoordinateSpan){ + [self CLLocationDegrees:json[@"latitudeDelta"]], + [self CLLocationDegrees:json[@"longitudeDelta"]] + }; +} + ++ (MKCoordinateRegion)MKCoordinateRegion:(id)json +{ + return (MKCoordinateRegion){ + [self CLLocationCoordinate2D:json], + [self MKCoordinateSpan:json] + }; +} +``` + +These conversion functions are designed to safely process any JSON that the JS might throw at them by displaying "RedBox" errors and returning standard initialization values when missing keys or other developer errors are encountered. + +To finish up support for the `region` prop, we need to document it in `propTypes` (or we'll get an error that the native prop is undocumented), then we can set it just like any other prop: + +```javascript +// MapView.js + +MapView.propTypes = { + /** + * When this property is set to `true` and a valid camera is associated + * with the map, the camera’s pitch angle is used to tilt the plane + * of the map. When this property is set to `false`, the camera’s pitch + * angle is ignored and the map is always displayed as if the user + * is looking straight down onto it. + */ + pitchEnabled = React.PropTypes.bool, + + /** + * The region to be displayed by the map. + * + * The region is defined by the center coordinates and the span of + * coordinates to display. + */ + region: React.PropTypes.shape({ + /** + * Coordinates for the center of the map. + */ + latitude: React.PropTypes.number.isRequired, + longitude: React.PropTypes.number.isRequired, + + /** + * Distance between the minimum and the maximum latitude/longitude + * to be displayed. + */ + latitudeDelta: React.PropTypes.number.isRequired, + longitudeDelta: React.PropTypes.number.isRequired, + }), +}; + +// MyApp.js + + render() { + var region = { + latitude: 37.48, + longitude: -122.16, + latitudeDelta: 0.1, + longitudeDelta: 0.1, + }; + return ; + } + +``` + +Here you can see that the shape of the region is explicit in the JS documentation - ideally we could codegen some of this stuff, but that's not happening yet. + +## Events + +So now we have a native map component that we can control easily from JS, but how do we deal with events from the user, like pinch-zooms or panning to change the visible region? The key is to make the `RCTMapManager` a delegate for all the views it vends, and forward the events to JS via the event dispatcher. This looks like so (simplified from the full implementation): + +```objective-c +// RCTMapManager.m + +#import "RCTMapManager.h" + +#import + +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" + +@interface RCTMapManager() +@end + +@implementation RCTMapManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + MKMapView *map = [[MKMapView alloc] init]; + map.delegate = self; + return map; +} + +#pragma mark MKMapViewDelegate + +- (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated +{ + MKCoordinateRegion region = mapView.region; + NSDictionary *event = @{ + @"target": [mapView reactTag], + @"region": @{ + @"latitude": @(region.center.latitude), + @"longitude": @(region.center.longitude), + @"latitudeDelta": @(region.span.latitudeDelta), + @"longitudeDelta": @(region.span.longitudeDelta), + } + }; + [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} +``` + +You can see we're setting the manager as the delegate for every view that it vends, then in the delegate method `-mapView:regionDidChangeAnimated:` the region is combined with the `reactTag` target to make an event that is dispatched to the corresponding React component instance in your application via `sendInputEventWithName:body:`. The event name `@"topChange"` maps to the `onChange` callback prop in JavaScript (mappings are [here](https://github.com/facebook/react-native/blob/master/React/Modules/RCTUIManager.m#L1146)). This callback is invoked with the raw event, which we typically process in the wrapper component to make a simpler API: + +```javascript +// MapView.js + +class MapView extends React.Component { + constructor() { + this._onChange = this._onChange.bind(this); + } + _onChange(event: Event) { + if (!this.props.onRegionChange) { + return; + } + this.props.onRegionChange(event.nativeEvent.region); + } + render() { + return ; + } +} +MapView.propTypes = { + /** + * Callback that is called continuously when the user is dragging the map. + */ + onRegionChange: React.PropTypes.func, + ... +}; +``` + +## Styles + +Since all our native react views are subclasses of `UIView`, most style attributes will work like you would expect out of the box. Some components will want a default style, however, for example `UIDatePicker` which is a fixed size. This default style is important for the layout algorithm to work as expected, but we also want to be able to override the default style when using the component. `DatePickerIOS` does this by wrapping the native component in an extra view, which has flexible styling, and using a fixed style (which is generated with constants passed in from native) on the inner native component: + +```javascript +// DatePickerIOS.ios.js + +var RCTDatePickerIOSConsts = require('NativeModules').UIManager.RCTDatePicker.Constants; +... + render: function() { + return ( + + + + ); + } +}); + +var styles = StyleSheet.create({ + rkDatePickerIOS: { + height: RCTDatePickerIOSConsts.ComponentHeight, + width: RCTDatePickerIOSConsts.ComponentWidth, + }, +}); +``` + +The `RCTDatePickerIOSConsts` constants are exported from native by grabbing the actual frame of the native component like so: + +```objective-c +// RCTDatePickerManager.m + +- (NSDictionary *)constantsToExport +{ + UIDatePicker *dp = [[UIDatePicker alloc] init]; + [dp layoutIfNeeded]; + + return @{ + @"ComponentHeight": @(CGRectGetHeight(dp.frame)), + @"ComponentWidth": @(CGRectGetWidth(dp.frame)), + @"DatePickerModes": @{ + @"time": @(UIDatePickerModeTime), + @"date": @(UIDatePickerModeDate), + @"datetime": @(UIDatePickerModeDateAndTime), + } + }; +} +``` + +This guide covered many of the aspects of bridging over custom native components, but there is even more you might need to consider, such as custom hooks for inserting and laying out subviews. If you want to go even deeper, check out the actual `RCTMapManager` and other components in the [source code](https://github.com/facebook/react-native/blob/master/React/Views). diff --git a/docs/NativeModulesIOS.md b/docs/NativeModulesIOS.md index 399d63440bbd43..51fbb22a30403b 100644 --- a/docs/NativeModulesIOS.md +++ b/docs/NativeModulesIOS.md @@ -4,20 +4,20 @@ title: Native Modules (iOS) layout: docs category: Guides permalink: docs/nativemodulesios.html -next: linking-libraries +next: nativecomponentsios --- -Sometimes an app needs access to platform API, and React Native doesn't have a corresponding wrapper yet. Maybe you want to reuse some existing Objective-C or C++ code without having to reimplement it in JavaScript. Or write some high performance, multi-threaded code such as image processing, network stack, database or rendering. +Sometimes an app needs access to platform API, and React Native doesn't have a corresponding module yet. Maybe you want to reuse some existing Objective-C or C++ code without having to reimplement it in JavaScript, or write some high performance, multi-threaded code such as for image processing, a database, or any number of advanced extensions. We designed React Native such that it is possible for you to write real native code and have access to the full power of the platform. This is a more advanced feature and we don't expect it to be part of the usual development process, however it is essential that it exists. If React Native doesn't support a native feature that you need, you should be able to build it yourself. This is a more advanced guide that shows how to build a native module. It assumes the reader knows Objective-C (Swift is not supported yet) and core libraries (Foundation, UIKit). -## iOS Calendar module example +## iOS Calendar Module Example This guide will use the [iOS Calendar API](https://developer.apple.com/library/mac/documentation/DataManagement/Conceptual/EventKitProgGuide/Introduction/Introduction.html) example. Let's say we would like to be able to access the iOS calendar from JavaScript. -A native module is just an Objective-C class that implements the `RCTBridgeModule` protocol. If you are wondering, RCT is a shorthand for ReaCT. +A native module is just an Objective-C class that implements the `RCTBridgeModule` protocol. If you are wondering, RCT is an abbreviation of ReaCT. ```objective-c // CalendarManager.h @@ -27,7 +27,7 @@ A native module is just an Objective-C class that implements the `RCTBridgeModul @end ``` -In addition to implementing the `RCTBridgeModule` protocol, your class must also include the `RCT_EXPORT_MODULE()` macro. This takes an optional argument that specifies the name that the module will accessed by in your JavaScript code (more on this later). If you do not specify a name, the JavaScript module name will match the Objective-C class name. +In addition to implementing the `RCTBridgeModule` protocol, your class must also include the `RCT_EXPORT_MODULE()` macro. This takes an optional argument that specifies the name that the module will be accessible as in your JavaScript code (more on this later). If you do not specify a name, the JavaScript module name will match the Objective-C class name. ```objective-c // CalendarManager.m @@ -60,7 +60,7 @@ CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey'); The return type of bridge methods is always `void`. React Native bridge is asynchronous, so the only way to pass a result to JavaScript is by using callbacks or emitting events (see below). -## Argument types +## Argument Types `RCT_EXPORT_METHOD` supports all standard JSON object types, such as: @@ -71,9 +71,9 @@ The return type of bridge methods is always `void`. React Native bridge is async - map (`NSDictionary`) with string keys and values of any type from this list - function (`RCTResponseSenderBlock`) -But it also works with any type that is supported by the `RCTConvert` class (see [`RCTConvert`](https://github.com/facebook/react-native/blob/master/React/Base/RCTConvert.h) for details). The `RCTConvert` helper functions all accept a JSON value as input and map it to a native Objective-C type or class. +But it also works with any type that is supported by the `RCTConvert` class (see [`RCTConvert`](https://github.com/facebook/react-native/blob/master/React/Base/RCTConvert.h) for details). The `RCTConvert` helper functions all accept a JSON value as input and map it to a native Objective-C type or class. -In our `CalendarManager` example, we to pass the event date to the native method. We can't send JavaScript Date objects over the bridge, so we need to convert the date to a string or number. We could write our native function like this: +In our `CalendarManager` example, we need to pass the event date to the native method. We can't send JavaScript Date objects over the bridge, so we need to convert the date to a string or number. We could write our native function like this: ```objective-c RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSNumber *)secondsSinceUnixEpoch) @@ -96,11 +96,11 @@ But by using the automatic type conversion feature, we can skip the manual conve ```objective-c RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date) { - // Date it ready to use! + // Date is ready to use! } ``` -You would then call this from JavaScript by using: +You would then call this from JavaScript by using either: ```javascript CalendarManager.addEvent('Birthday Party', date.toTime()); // passing date as number of seconds since Unix epoch @@ -112,6 +112,8 @@ or CalendarManager.addEvent('Birthday Party', date.toISOString()); // passing date as ISO-8601 string ``` +And both values would get converted correctly to the native `NSDate`. A bad value, like an `Array`, would generate a helpful "RedBox" error message. + As `CalendarManager.addEvent` method gets more and more complex, the number of arguments will grow. Some of them might be optional. In this case it's worth considering changing the API a little bit to accept a dictionary of event attributes, like this: ```objective-c @@ -143,9 +145,9 @@ CalendarManager.addEvent('Birthday Party', { > **WARNING** > -> This section is more experimental than others, we don't have a set of best practices around callbacks yet. +> This section is more experimental than others because we don't have a solid set of best practices around callbacks yet. -Native module also supports a special kind of argument- a callback. In most cases it is used to provide the function call result to JavaScript. +Native modules also supports a special kind of argument- a callback. In most cases it is used to provide the function call result to JavaScript. ```objective-c RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback) @@ -155,7 +157,7 @@ RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback) } ``` -`RCTResponseSenderBlock` accepts only one argument - an array of arguments to pass to the JavaScript callback. In this case we use node's convention to set first argument to error and the rest - to the result of the function. +`RCTResponseSenderBlock` accepts only one argument - an array of parameters to pass to the JavaScript callback. In this case we use node's convention to make the first parameter an error object (usually `null` when there is no error) and the rest are the results of the function. ```javascript CalendarManager.findEvents((error, events) => { @@ -167,32 +169,34 @@ CalendarManager.findEvents((error, events) => { }) ``` -Native module is supposed to invoke its callback only once. It can, however, store the callback as an ivar and invoke it later. This pattern is often used to wrap iOS APIs that require delegate. See [`RCTAlertManager`](https://github.com/facebook/react-native/blob/master/React/Modules/RCTAlertManager.m). +A native module is supposed to invoke its callback only once. It can, however, store the callback and invoke it later. This pattern is often used to wrap iOS APIs that require delegates. See [`RCTAlertManager`](https://github.com/facebook/react-native/blob/master/React/Modules/RCTAlertManager.m) for an example. -If you want to pass error-like object to JavaScript, use `RCTMakeError` from [`RCTUtils.h`](https://github.com/facebook/react-native/blob/master/React/Base/RCTUtils.h). +If you want to pass error-like objects to JavaScript, use `RCTMakeError` from [`RCTUtils.h`](https://github.com/facebook/react-native/blob/master/React/Base/RCTUtils.h). Right now this just passes an Error-shaped dictionary to JavaScript, but we would like to automatically generate real JavaScript `Error` objects in the future. -## Implementing a native module +## Threading -The native module should not have any assumptions about what thread it is being called on. React Native invokes native modules methods on a separate serial GCD queue, but this is an implementation detail and might change. If the native module needs to call main-thread-only iOS API, it should schedule the operation on the main queue: +The native module should not have any assumptions about what thread it is being called on. React Native invokes native modules methods on a separate serial GCD queue, but this is an implementation detail and might change. The `- (dispatch_queue_t)methodQueue` method allows the native module to specify which queue its methods should be run on. For example, if it needs to use a main-thread-only iOS API, it should specify this via: ```objective-c -RCT_EXPORT_METHOD(addEvent:(NSString *)name callback:(RCTResponseSenderBlock)callback) +- (dispatch_queue_t)methodQueue { - dispatch_async(dispatch_get_main_queue(), ^{ - // Call iOS API on main thread - ... - // You can invoke callback from any thread/queue - callback(@[...]); - }); + return dispatch_get_main_queue(); } ``` -The same way if the operation can take a long time to complete, the native module should not block. It is a good idea to use `dispatch_async` to schedule expensive work on background queue. +Similarly, if an operation may take a long time to complete, the native module should not block and can specify it's own queue to run operations on. For example, the `RCTAsyncLocalStorage` module creates it's own queue so the React queue isn't blocked waiting on potentially slow disk access: + +```objective-c +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); +} +``` -## Exporting constants +## Exporting Constants -Native module can export constants that are instantly available to JavaScript at runtime. This is useful to export some initial data that would otherwise require a bridge round-trip. +A native module can export constants that are immediately available to JavaScript at runtime. This is useful for communicating static data that would otherwise require a round-trip through the bridge. ```objective-c - (NSDictionary *)constantsToExport @@ -201,16 +205,16 @@ Native module can export constants that are instantly available to JavaScript at } ``` -JavaScript can use this value right away: +JavaScript can use this value right away, synchronously: ```javascript console.log(CalendarManager.firstDayOfTheWeek); ``` -Note that the constants are exported only at initialization time, so if you change `constantsToExport` value at runtime it won't affect JavaScript environment. +Note that the constants are exported only at initialization time, so if you change `constantsToExport` values at runtime it won't affect the JavaScript environment. -## Sending events to JavaScript +## Sending Events to JavaScript The native module can signal events to JavaScript without being invoked directly. The easiest way to do this is to use `eventDispatcher`: @@ -240,7 +244,7 @@ var subscription = DeviceEventEmitter.addListener( (reminder) => console.log(reminder.name) ); ... -// Don't forget to unsubscribe +// Don't forget to unsubscribe, typically in componentWillUnmount subscription.remove(); ``` For more examples of sending events to JavaScript, see [`RCTLocationObserver`](https://github.com/facebook/react-native/blob/master/Libraries/Geolocation/RCTLocationObserver.m).