Permalink
Browse files

Docs: Updated iOS Component Guide

Summary:
Follow up on #14436

hramos:
>  MapViewIOS was removed a couple of versions ago. No one has touched this guide in a while, so unless someone volunteers to get it back up to date, we'll probably end up removing it from the docs.

I'm volunteering to get it back up to date!

- Fixed broken code examples
- Swapped out `pitchEnabled` for `zoomEnabled`, needs less explaining and it's easier to test on a simulator.
- Renamed RNTMap to RNTMapView for clarity.
- Renamed inconsistent `onChange`s to onRegionChange`.
- Swapped out 'vending' for 'exposing'.

I wasn't really sure about the last 3 points. I think it's more clear like this, but please review.

I'll add some review comments as well.
Closes #14991

Differential Revision: D5416319

Pulled By: hramos

fbshipit-source-id: bc70942a66acc5e3967b245af8271a1d2465ab60
  • Loading branch information...
teameh authored and facebook-github-bot committed Jul 19, 2017
1 parent c2c97ae commit 94b6cda8a7bef244b8e74a9995ccbceeb03dfe05
Showing with 105 additions and 75 deletions.
  1. +105 −75 docs/NativeComponentsIOS.md
View
@@ -17,11 +17,11 @@ Like the native module guide, this too is a more advanced guide that assumes you
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.
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 expose 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:
Exposing a view is simple:
- Create the basic subclass.
- Subclass `RCTViewManager` to create a manager for your component.
- Add the `RCT_EXPORT_MODULE()` marker macro.
- Implement the `-(UIView *)view` method.
@@ -45,7 +45,7 @@ RCT_EXPORT_MODULE()
@end
```
**Note:** Do not attempt to set the `frame` or `backgroundColor` properties on the `UIView` instance that you vend through the `-view` method. React Native will overwrite the values set by your custom class in order to match your JavaScript component's layout props. If you need this granularity of control it might be better to wrap the `UIView` instance you want to style in another `UIView` and return the wrapper `UIView` instead. See [Issue 2948](https://github.com/facebook/react-native/issues/2948) for more context.
**Note:** Do not attempt to set the `frame` or `backgroundColor` properties on the `UIView` instance that you expose through the `-view` method. React Native will overwrite the values set by your custom class in order to match your JavaScript component's layout props. If you need this granularity of control it might be better to wrap the `UIView` instance you want to style in another `UIView` and return the wrapper `UIView` instead. See [Issue 2948](https://github.com/facebook/react-native/issues/2948) for more context.
> In the example above, we prefixed our class name with `RNT`. Prefixes are used to avoid name collisions with other frameworks. Apple frameworks use two-letter prefixes, and React Native uses `RCT` as a prefix. In order to avoid name collisions, we recommend using a three-letter prefix other than `RCT` in your own classes.
@@ -56,31 +56,46 @@ Then you just need a little bit of JavaScript to make this a usable React compon
import { requireNativeComponent } from 'react-native';
// requireNativeComponent automatically resolves this to "RNTMapManager"
// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap', null);
// MyApp.js
import MapView from './MapView.js';
...
render() {
return <MapView style={{ flex: 1 }} />;
}
```
Make sure to use `RNTMap` here. We want to require the manager here, which will expose the view of our manager for use in Javascript.
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 and specify the visible region. Disabling pitch is a simple boolean, so we add this one line:
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 zooming and specify the visible region. Disabling zoom is a simple boolean, so we add this one line:
```objectivec
// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, 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.
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 set the property in JS:
Now to actually disable zooming, we set the property in JS:
```javascript
// MyApp.js
<MapView pitchEnabled={false} />
<MapView
zoomEnabled={false}
style={{ flex: 1 }}
/>;
```
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`:
To document the properties (and which values they accept) of our MapView component we'll add a wrapper component and document the interface with React `PropTypes`:
```javascript
// MapView.js
@@ -96,13 +111,10 @@ class MapView extends React.Component {
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.
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
pitchEnabled: PropTypes.bool,
zoomEnabled: PropTypes.bool,
};
var RNTMap = requireNativeComponent('RNTMap', MapView);
@@ -127,9 +139,16 @@ Ok, this is more complicated than the simple `BOOL` case we had before. Now we
You could write any conversion function you want for your view - here is the implementation for `MKCoordinateRegion` via a category on `RCTConvert`. It uses an already existing category of ReactNative `RCTConvert+CoreLocation`:
```objectivec
// RNTMapManager.m
#import "RCTConvert+Mapkit.m"
// RCTConvert+Mapkit.h
#import <MapKit/MapKit.h>
#import <React/RCTConvert.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>
@interface RCTConvert (Mapkit)
@@ -138,10 +157,6 @@ You could write any conversion function you want for your view - here is the imp
@end
// RCTConvert+Mapkit.m
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>
@implementation RCTConvert(MapKit)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
@@ -173,13 +188,10 @@ To finish up support for the `region` prop, we need to document it in `propTypes
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.
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
pitchEnabled: PropTypes.bool,
zoomEnabled: PropTypes.bool,
/**
* The region to be displayed by the map.
@@ -205,21 +217,26 @@ MapView.propTypes = {
// MyApp.js
render() {
var region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return <MapView region={region} />;
}
render() {
var region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
style={{ flex: 1 }}
/>
);
}
```
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.
Sometimes you'll have some special properties that you need to expose for the native component, but don't actually want them as part of the API for the associated React component. For example, `Switch` has a custom `onChange` handler for the raw native event, and exposes an `onValueChange` handler property that is invoked with just the boolean value rather than the raw event. Since you don't want these native only properties to be part of the API, you don't want to put them in `propTypes`, but if you don't you'll get an error. The solution is simply to call them out via the `nativeOnly` option, e.g.
Sometimes your native component will have some special properties that you don't want to them to be part of the API for the associated React component. For example, `Switch` has a custom `onChange` handler for the raw native event, and exposes an `onValueChange` handler property that is invoked with just the boolean value rather than the raw event. Since you don't want these native only properties to be part of the API, you don't want to put them in `propTypes`, but if you don't you'll get an error. The solution is simply to add them to the `nativeOnly` option, e.g.
```javascript
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
@@ -229,68 +246,75 @@ var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
## 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 declare an event handler property on `RNTMapManager`, make it a delegate for all the views it vends, and forward events to JS by calling the event handler block from the native view. This looks like so (simplified from the full implementation):
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?
Until now we've just returned a `MKMapView` instance from our manager's `-(UIView *)view` method. We can't add new properties to `MKMapView` so we have to create a new subclass from `MKMapView` which we use for our View. We can then add a `onRegionChange` callback on this subclass:
```objectivec
// RNTMap.h
// RNTMapView.h
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface RNTMap: MKMapView
@interface RNTMapView: MKMapView
@property (nonatomic, copy) RCTBubblingEventBlock onChange;
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end
```
```objectivec
// RNTMap.m
// RNTMapView.m
#import "RNTMap.h"
#import "RNTMapView.h"
@implementation RNTMap
@implementation RNTMapView
@end
```
```objectivec
// RNTMapManager.m
Next, declare an event handler property on `RNTMapManager`, make it a delegate for all the views it exposes, and forward events to JS by calling the event handler block from the native view.
#import "RNTMapManager.h"
```objectivec{9,17,31-48}
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
#import "RNTMap.h"
#import <React/UIView+React.h>
#import "RNTMapView.h"
#import "RCTConvert+Mapkit.m"
@interface RNTMapManager() <MKMapViewDelegate>
@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
- (UIView *)view
{
RNTMap *map = [RNTMap new];
RNTMapView *map = [RNTMapView new];
map.delegate = self;
return map;
}
#pragma mark MKMapViewDelegate
- (void)mapView:(RNTMap *)mapView regionDidChangeAnimated:(BOOL)animated
- (void)mapView:(RNTMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
if (!mapView.onChange) {
if (!mapView.onRegionChange) {
return;
}
MKCoordinateRegion region = mapView.region;
mapView.onChange(@{
mapView.onRegionChange(@{
@"region": @{
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
@@ -299,38 +323,44 @@ RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
}
});
}
@end
```
You can see we're adding an event handler property to the view by subclassing `MKMapView`. Then we're exposing the `onChange` event handler property and setting the manager as the delegate for every view that it vends. Finally, in the delegate method `-mapView:regionDidChangeAnimated:` the event handler block is called on the corresponding view with the region data. Calling the `onChange` event handler block results in calling the same callback prop in JavaScript. This callback is invoked with the raw event, which we typically process in the wrapper component to make a simpler API:
In the delegate method `-mapView:regionDidChangeAnimated:` the event handler block is called on the corresponding view with the region data. Calling the `onRegionChange` event handler block results in calling the same callback prop in JavaScript. 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(props) {
super(props)
this._onChange = this._onChange.bind(this);
}
_onChange(event: Event) {
_onRegionChange = (event) => {
if (!this.props.onRegionChange) {
return;
}
// process raw event...
this.props.onRegionChange(event.nativeEvent);
}
render() {
return <RNTMap {...this.props} onChange={this._onChange} />;
return (
<RNTMap
{...this.props}
onRegionChange={this._onRegionChange}
/>
);
}
}
MapView.propTypes = {
/**
* Callback that is called continuously when the user is dragging the map.
*/
onChange: PropTypes.func,
onRegionChange: PropTypes.func,
...
};
class MapViewExample extends React.Component {
onRegionChange(event: Event) {
// MyApp.js
class MyApp extends React.Component {
onRegionChange(event) {
// Do stuff with event.region.latitude, etc.
}
@@ -341,20 +371,20 @@ class MapViewExample extends React.Component {
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView region={region} pitchEnabled={false} style={{flex: 1}} onChange={this.onRegionChange}/>
<MapView
region={region}
zoomEnabled={false}
onRegionChange={this.onRegionChange}
/>
);
}
}
// Module name
AppRegistry.registerComponent('AwesomeProject', () => MapViewExample);
```
## 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:
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
@@ -405,4 +435,4 @@ The `RCTDatePickerIOSConsts` constants are exported from native by grabbing the
}
```
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).
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 [source code](https://github.com/facebook/react-native/blob/master/React/Views) of some of the implemented components.

0 comments on commit 94b6cda

Please sign in to comment.