Skip to content

Commit d925d78

Browse files
committed
Add Native Components guide.
1 parent 3e8b41f commit d925d78

File tree

2 files changed

+375
-31
lines changed

2 files changed

+375
-31
lines changed

docs/NativeComponentsIOS.md

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
---
2+
id: nativecomponentsios
3+
title: Native UI Components (iOS)
4+
layout: docs
5+
category: Guides
6+
permalink: docs/nativecomponentsios.html
7+
next: linking-libraries
8+
---
9+
10+
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.
11+
12+
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.
13+
14+
## iOS MapView example
15+
16+
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.
17+
18+
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.
19+
20+
Vending a view is simple:
21+
- Create the basic subclass.
22+
- Add the `RCT_EXPORT_MODULE()` marker macro.
23+
- Implement the `-(UIView *)view` method
24+
25+
```objective-c
26+
// RCTMapManager.m
27+
#import <MapKit/MapKit.h>
28+
29+
#import "RCTViewManager.h"
30+
31+
@interface RCTMapManager : RCTViewManager
32+
@end
33+
34+
@implementation RCTMapManager
35+
36+
RCT_EXPORT_MODULE()
37+
38+
- (UIView *)view
39+
{
40+
return [[MKMapView alloc] init];
41+
}
42+
43+
@end
44+
```
45+
46+
Then you just need a little bit of JavaScript to make this a usable React component:
47+
48+
```javascript
49+
// MapView.js
50+
51+
var { requireNativeComponent } = require('react-native');
52+
53+
module.exports = requireNativeComponent('RCTMap', null);
54+
```
55+
56+
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 :(
57+
58+
## Properties
59+
60+
The first thing we can do to make this component more usable is to bridge over some native properties. Let's saw we want to be able to disable pitch control, and be able to specify the visible region. Disabling pitch is a simple boolean, so we just add this to the m-file:
61+
62+
```objective-c
63+
// RCTMapManager.m
64+
RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)
65+
```
66+
67+
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.
68+
69+
Now to actually disable pitch, we just set the property in JS:
70+
71+
```javascript
72+
// MyApp.js
73+
<MapView pitchEnabled={false} />
74+
```
75+
76+
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`:
77+
78+
```javascript
79+
// MapView.js
80+
var React = require('react-native');
81+
var { requireNativeComponent } = React;
82+
83+
class MapView extends React.Component {
84+
render() {
85+
return <RCTMap {...this.props} />;
86+
}
87+
}
88+
89+
var RCTMap= requireNativeComponent('RCTMap', MapView);
90+
91+
MapView.propTypes = {
92+
/**
93+
* When this property is set to `true` and a valid camera is associated
94+
* with the map, the camera’s pitch angle is used to tilt the plane
95+
* of the map. When this property is set to `false`, the camera’s pitch
96+
* angle is ignored and the map is always displayed as if the user
97+
* is looking straight down onto it.
98+
*/
99+
pitchEnabled = React.PropTypes.bool,
100+
};
101+
102+
module.exports = MapView;
103+
```
104+
105+
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.
106+
107+
Next, let's add the more complex `region` prop. We start by adding the native code:
108+
109+
```objective-c
110+
// RCTMapManager.m
111+
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
112+
{
113+
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
114+
}
115+
```
116+
117+
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.
118+
119+
You could of course write any conversion function you want for your view - here is the implementation for `MKCoordinateRegion` via two categories on `RCTConvert`:
120+
121+
```objective-c
122+
@implementation RCTConvert(CoreLocation)
123+
124+
RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue);
125+
RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue);
126+
127+
+ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json
128+
{
129+
json = [self NSDictionary:json];
130+
return (CLLocationCoordinate2D){
131+
[self CLLocationDegrees:json[@"latitude"]],
132+
[self CLLocationDegrees:json[@"longitude"]]
133+
};
134+
}
135+
136+
@end
137+
138+
@implementation RCTConvert(MapKit)
139+
140+
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
141+
{
142+
json = [self NSDictionary:json];
143+
return (MKCoordinateSpan){
144+
[self CLLocationDegrees:json[@"latitudeDelta"]],
145+
[self CLLocationDegrees:json[@"longitudeDelta"]]
146+
};
147+
}
148+
149+
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json
150+
{
151+
return (MKCoordinateRegion){
152+
[self CLLocationCoordinate2D:json],
153+
[self MKCoordinateSpan:json]
154+
};
155+
}
156+
```
157+
158+
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.
159+
160+
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:
161+
162+
```javascript
163+
// MapView.js
164+
165+
MapView.propTypes = {
166+
/**
167+
* When this property is set to `true` and a valid camera is associated
168+
* with the map, the camera’s pitch angle is used to tilt the plane
169+
* of the map. When this property is set to `false`, the camera’s pitch
170+
* angle is ignored and the map is always displayed as if the user
171+
* is looking straight down onto it.
172+
*/
173+
pitchEnabled = React.PropTypes.bool,
174+
175+
/**
176+
* The region to be displayed by the map.
177+
*
178+
* The region is defined by the center coordinates and the span of
179+
* coordinates to display.
180+
*/
181+
region: React.PropTypes.shape({
182+
/**
183+
* Coordinates for the center of the map.
184+
*/
185+
latitude: React.PropTypes.number.isRequired,
186+
longitude: React.PropTypes.number.isRequired,
187+
188+
/**
189+
* Distance between the minimum and the maximum latitude/longitude
190+
* to be displayed.
191+
*/
192+
latitudeDelta: React.PropTypes.number.isRequired,
193+
longitudeDelta: React.PropTypes.number.isRequired,
194+
}),
195+
};
196+
197+
// MyApp.js
198+
199+
render() {
200+
var region = {
201+
latitude: 37.48,
202+
longitude: -122.16,
203+
latitudeDelta: 0.1,
204+
longitudeDelta: 0.1,
205+
};
206+
return <MapView region={region} />;
207+
}
208+
209+
```
210+
211+
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.
212+
213+
## Events
214+
215+
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):
216+
217+
```objective-c
218+
// RCTMapManager.m
219+
220+
#import "RCTMapManager.h"
221+
222+
#import <MapKit/MapKit.h>
223+
224+
#import "RCTBridge.h"
225+
#import "RCTEventDispatcher.h"
226+
#import "UIView+React.h"
227+
228+
@interface RCTMapManager() <MKMapViewDelegate>
229+
@end
230+
231+
@implementation RCTMapManager
232+
233+
RCT_EXPORT_MODULE()
234+
235+
- (UIView *)view
236+
{
237+
MKMapView *map = [[MKMapView alloc] init];
238+
map.delegate = self;
239+
return map;
240+
}
241+
242+
#pragma mark MKMapViewDelegate
243+
244+
- (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated
245+
{
246+
MKCoordinateRegion region = mapView.region;
247+
NSDictionary *event = @{
248+
@"target": [mapView reactTag],
249+
@"region": @{
250+
@"latitude": @(region.center.latitude),
251+
@"longitude": @(region.center.longitude),
252+
@"latitudeDelta": @(region.span.latitudeDelta),
253+
@"longitudeDelta": @(region.span.longitudeDelta),
254+
}
255+
};
256+
[self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event];
257+
}
258+
```
259+
260+
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:
261+
262+
```javascript
263+
// MapView.js
264+
265+
class MapView extends React.Component {
266+
constructor() {
267+
this._onChange = this._onChange.bind(this);
268+
}
269+
_onChange(event: Event) {
270+
if (!this.props.onRegionChange) {
271+
return;
272+
}
273+
this.props.onRegionChange(event.nativeEvent.region);
274+
}
275+
render() {
276+
return <RCTMap {...this.props} onChange={this._onChange} />;
277+
}
278+
}
279+
MapView.propTypes = {
280+
/**
281+
* Callback that is called continuously when the user is dragging the map.
282+
*/
283+
onRegionChange: React.PropTypes.func,
284+
...
285+
};
286+
```
287+
288+
## Styles
289+
290+
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:
291+
292+
```javascript
293+
// DatePickerIOS.ios.js
294+
295+
var RCTDatePickerIOSConsts = require('NativeModules').UIManager.RCTDatePicker.Constants;
296+
...
297+
render: function() {
298+
return (
299+
<View style={this.props.style}>
300+
<RCTDatePickerIOS
301+
ref={DATEPICKER}
302+
style={styles.rkDatePickerIOS}
303+
...
304+
/>
305+
</View>
306+
);
307+
}
308+
});
309+
310+
var styles = StyleSheet.create({
311+
rkDatePickerIOS: {
312+
height: RCTDatePickerIOSConsts.ComponentHeight,
313+
width: RCTDatePickerIOSConsts.ComponentWidth,
314+
},
315+
});
316+
```
317+
318+
The `RCTDatePickerIOSConsts` constants are exported from native by grabbing the actual frame of the native component like so:
319+
320+
```objective-c
321+
// RCTDatePickerManager.m
322+
323+
- (NSDictionary *)constantsToExport
324+
{
325+
UIDatePicker *dp = [[UIDatePicker alloc] init];
326+
[dp layoutIfNeeded];
327+
328+
return @{
329+
@"ComponentHeight": @(CGRectGetHeight(dp.frame)),
330+
@"ComponentWidth": @(CGRectGetWidth(dp.frame)),
331+
@"DatePickerModes": @{
332+
@"time": @(UIDatePickerModeTime),
333+
@"date": @(UIDatePickerModeDate),
334+
@"datetime": @(UIDatePickerModeDateAndTime),
335+
}
336+
};
337+
}
338+
```
339+
340+
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).

0 commit comments

Comments
 (0)