Diary of Building an iOS App with React Native
The story of how I built the first non-Facebook React Native app to be released on the app store
When news broke that React Native was open sourced on Friday, I felt elated. This was the moment I had been waiting for. Ever since the React.js conference videos introduced React Native in late January, I couldn't wait to get my hands on it.
If you don't know React Native, it's a new open source framework by Facebook that allows you to write iOS (and, eventually, Android) apps using HTML-like code (called JSX) and JavaScript. It's based on the popular React.js JavaScript framework, but you can't run normal React.js code directly as an iOS app. Instead, once you are familiar with the basics of React.js, you can use the same knowledge, but slightly different components, to assemble iOS and Android apps. They refer to it as "Learn once, write anywhere", as opposed to the "Write once, run anywhere" approach advocated by other frameworks, like PhoneGap. The paradigm employed by React Native allows it to use native iOS components, and bring about changes to the UI through a JavaScript thread running in the background. In theory, this means the experience should feel native and appear very smooth, even though it's powered by JavaScript. And I was about to find out whether that is true in practice.
I downloaded a copy, followed the instructions for creating an app, and within minutes after learning about the launch, I had my first React Native iOS app running.
If the rest of the process were to be as easy as the start, I was in for a grand time. And React Native did not disappoint.
The first screen
I set out to build a simple app for learning simple Chinese phrases, a very basic spaced-repitition flashcard app. Using Sketch, I quickly came up with what I wanted the study screen to look like:
With only that in hand, I started assembling components in React. First, the card:
var Card = React.createClass({
render: function() {
var card = this.props.card;
return (
<View style={styles.card}>
<Text numberOfLines={2} ref="definition" style={styles.definition}>{card.definition}</Text>
<Text numberOfLines={1} style={styles.chinese}>{this.props.simplified ? card.simplified : card.traditional}</Text>
<Text ref="pinyin" numberOfLines={1} style={styles.pinyin}>{card.pinyin}</Text>
</View>
);
},
});
I had never worked with flexbox before, but I found these tutorials particularly useful. React Native's flexbox implementation departs from the browser-equivalent in some small ways:
- The shorthand form, e.g.
flex: 1 100%
, is not valid in React Native. Instead, specify the properties separately:flex: 1; width: 100%
. Flex values can be integers or doubles (0
,0.3
,1.0
etc), indicating the relative size of the box. So, if you have multiple elements they will fill the relative proportion of the view based on their flex value. If you had two views, one withflex 0.25
and the other withflex 0.75
, it would split into quarters. Or you could also get an identical effect usingflex 25
andflex 75
. - Item-based styling is limited:
flex-basis
,flex-shrink
andflex-grow
are not available. I hope this will change in the future, but as of now the workaround is to use container properties to achieve the same effects.
For the card, this is what my styles looked like at first:
card: {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
shadowColor: "black",
shadowOpacity: 0.3,
shadowRadius: 3,
shadowOffset: {
height: 0,
width: 0
},
width: 290,
height: 290,
},
definition: {
fontSize: 30,
textAlign: 'center',
},
chinese: {
flex: 1,
fontSize: 80,
},
pinyin: {
flex: 1,
fontSize: 40,
},
No big surprises there.
There is a bug in the current version of React Native that causes shadows to not work on iPhone 5 and up, but hopefully that will be fixed soon. I might even take a stab at it myself.
The rest of the items on the screen were similarly created; buttons, progress bar, all were simple React components drawn onto the screen and styled by flexbox. If you are somewhat fimiliar with React.js, this should prove to be very easy.
Adding a ListView
With the study screen cleared, I decided to move on to adding a ListView, for choosing which deck of cards you would like to study. I didn't even design this in Sketch, I had a look at the Movies example to see how they did it there, and used a similar approach.
The important part is the render
function, which returns something that looks like this:
return (
<View style={{flex: 1, flexDirection: 'column'}}>
{header}
<ListView
ref="listview"
dataSource={this.state.dataSource}
renderFooter={this.renderFooter}
renderRow={this.renderRow}
onEndReached={this.onEndReached}
automaticallyAdjustContentInsets={false}
keyboardDismissMode="onDrag"
keyboardShouldPersistTaps={true}
showsVerticalScrollIndicator={true}
style={styles.list}
/>
</View>
);
Notice the this.renderRow
and other functions. These I had to implement, but it was as easy as creating elements on the study screen. Here is what the renderRow
method looks like:
renderRow: function(deck: Object) {
return (
<DeckCell
onSelect={() => this.selectDeck(deck)}
deck={deck}
/>
);
},
DeckCell
is a simple component I defined to display the deck's title and the progress percentage, like this:
Upon selecting the DeckCell
row, we call this.selectDeck(deck)
, which looks like this:
selectDeck: function(deck){
// onPress is what we do when the deck is selected. We bind it to the current scope.
var onPress = function(){
this.setState({loaded: false});
DatabaseManager.loadDecks(this.loadDecks); // we will talk about this in a second
DatabaseManager.loadProgress(this.loadProgress);
}.bind(this);
this.props.navigator.push({
title: deck.title,
component: StudyScreen, // the screen with the cards we created earlier
passProps: {deck: deck, onPress: onPress},
});
}
With this in place, clicking on a deck in the list will load the study screen, sending it a deck
object so that we know which deck is intended to be used. So far so good. The last step that remained was to hook up a database. This was the part I was really curious about!
Hooking up a database
After clearing the first hurdle of drawing some items onto the screen and setting up a ListView with dummy objects, my attention turned to hooking up a database and having it communicate with React Native. I noticed that there is an AsyncStorage
class, but this is intended for simple key-value storage (like LocalStorage
on the web). I needed to do some heavy lifting, so I turned to my trusty friend, SQLite. The question was just, how do I hook SQLite up to the React app? I already alluded to the answer in the previous code snippet.
I made use of the RCTBridgeModule
interface described in the React Native docs. First, this meant defining a DatabaseManager.h
header file:
#ifndef ReactCards_DatabaseManager_h
#define ReactCards_DatabaseManager_h
#import "RCTBridgeModule.h"
#import "DBManager.h"
@interface DatabaseManager : NSObject <RCTBridgeModule>
@property (nonatomic, strong) DBManager *dbManager;
@end
#endif
Next, I implemented an initialization function to load up the SQLite database and save the connection as an instance variable. The SQLite-specific code was kept in another library, SQLiteManager
, which could easily be replaced with a wrapper for Core Data or Realm.
#import <Foundation/Foundation.h>
#import "SQLiteManager.h"
#import "RCTLog.h"
#import "SuperMemo.h"
@implementation DatabaseManager
- (id)init {
return [self initWithDB:@"collections.db"];
}
- (id)initWithDB:(NSString*)databaseName
{
self = [super init];
if (self) {
self.dbManager = [[SQLiteManager alloc] initWithDatabaseFilename:databaseName];
}
return self;
}
Now, I could define a loadDecks
method that takes a callback function, and export it to the JavaScript code by putting a line at the top of the file
RCT_EXPORT_MODULE();
and adding a call to the RCT_EXPORT_METHOD macro (RCT_EXPORT();
on the first lineRCT_EXPORT
is now deprecated in favor of RCT_EXPORT_METHOD
):
RCT_EXPORT_METHOD(loadDecks:(RCTResponseSenderBlock)callback
{
// returns an array of NSDictionary objects, with fields id, title, count and percentage
NSArray *decks = [self.dbManager loadDecks];
callback(@[[NSNull null], decks]);
}
I didn't expect this to work at all, but I'd gone this far, so there was no turning back. In the JavaScript code, I added lines to first import the DataManager class, and then call the loadDecks
function, giving it a callback function the logs the result.
var DatabaseManager = require('NativeModules').DatabaseManager;
...
DatabaseManager.loadDecks(this.loadDecks);
...
loadDecks: function(error, decks){
if (error) {
console.error(error);
} else {
console.log(decks);
}
}
To my own astonishment, it worked! On first try! In the logs, I could see:
RCTJSLog> [{"title":"Descriptions","id":"1","count":"0","percentage":"0"},{"title":"Phrases","id":"2","count":"0","percentage":"0"},{"title":"Time","id":"3","count":"0","percentage":"0"},{"title":"Actions","id":"4","count":"0","percentage":"0"}]
I now had all the basic components necessary to make a React Native app backed by a SQLite database. It was plain sailing up to this point, and it was plain sailing from there on out. React Native is awesome.
Submitting the app
I spent some free hours here and there over the next couple of days to finish the app and bring all the parts together. There were no big surprises, and that's a good thing.
But before I could submit my app to the app store, I needed to turn off developer mode and enable performance optimizations. It took me some digging to find out how to do this, but this turned out to be quite easy, once you know how. First, inside the project AppDelegate.m
, uncomment the line below the OPTION 2
comment:
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
This causes the app to load from pre-bundled file on disk. Next, with the development server running (started with npm start
), you can curl
the URL in the comment, adding an extra dev
query parameter:
curl http://localhost:8081/index.ios.bundle\?dev\=0 -o main.jsbundle
The dev
query parameter may be set to either 0
or false
(or similarly, 1
and true
). When you next build the app, the log should indicate that development mode is now turned off and performance optimizations are turned on:
RCTJSLog> "Running application "HSK1 Chinese" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === false, development-level warning are OFF, performance optimizations are ON"
With this out of the way, I could build an archive and submit it to the app store. Again, easy as pie.
I had some friends beta test the app on different platforms through TestFlight, and all seemed well. And today, one week after the launch of React Native, I submitted the app to be reviewed for the App Store. Wish me luck!
Closing remarks
As a web application developer who was already somewhat familiar with both React.js and iOS, React Native was a total breeze to work with. The things I did not know before I started were easy to learn, thanks to these attributes of React Native:
- Error messages are clear and descriptive. I can't stress the importance of this enough. The React Native error messages would point to specific lines in the JavaScript code, describe the problem exactly, and sometimes even suggest ways to fix it. Once, I did something stupid (I forget what exactly), and it even warned me that what I was doing is an anti-pattern, and should be avoided. Amazing.
- Instant reload with Cmd+R. Yup, no need to compile again and again, wasting precious minutes and breaking your concentration. With React Native you can press
Cmd+R
, and the simulator immediately reloads. This is a huge boon for development. - No more storyboards. Storyboards are great, but they have their downsides. For one, they don't play well with version control systems like Git. For another, I never really liked dragging lines from UI components to lines in the code to indicate a relationship, only to later accidentally remove that line and be faced with an incomprehensible error message. By allowing UIs to be designed with something similar to HTML+CSS, building these just got a whole lot easier and less painful.
- It's smoooothhh. Even on my iPhone 4S, the app runs as smooth as the flight of a dream. By running JavaScript in a background thread that doesn't cause the UI to hang, React Native delivers on its promise to make coding iOS apps as easy as writing JavaScript, without the performance problems associated with running it in a WebView. It totally feels just like a native app.
React Native is truly one of the most exciting developments in technology I have seen in a long time, and the wait was worth it. +1, will use again.
PS If you'd like to check out the app, it's on the app store right now for $0.99. Or if HSK1 is too easy for you, you can sign up to be notified when other levels become available. Update: Since this was published years ago, iOS introduced some breaking changes that wasn't worthwhile for me to address, and I decided to take the app down. It was fun working on this, thanks for all your support and feedback over the years!
Thanks to Spencer Ahrens from the React Native core team for corrections to this post.