Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Integrating Parse and React Native for iOS

$
0
0
Combine the power of a Parse backend + React Native iOS frontend!

Combine the power of a Parse backend + React Native iOS frontend!

React Native, introduced at the 2015 Facebook F8 Developer Conference, lets you build native iOS apps using the same concepts found in the popular JavaScript UI library React. The same event also gave us Parse+React, which brings the React view concepts to the data layer.

Parse supports rapid development of your mobile apps by handling your application’s infrastructure needs for you. Parse services include data storage, social integration, push notifications, and analytics, along with client SDKs for various platforms such as iOS, Android, and JavaScript. Parse+React is built on top of the JavaScript SDK and provides hooks into React to make it easy to query and store data on Parse.

In this tutorial, you’ll learn more about React and to use it to build native apps. You’ll build upon the sample PropertyFinder application from our introductory tutorial React Native: Building Apps with JavaScript. Be sure to check out that tutorial for all the React Native basics before continuing on here with integrating Parse.

Getting Started

To get started with the tutorial, download the starter project and unzip it.

This is essentially the same PropertyFinder application with one important difference. Can you spot the difference by looking at some of the JavaScript files?

The original application used ECMAScript 6 (ES6) but the starter project for this tutorial doesn’t. This is because Parse+React relies on mixins to bring Parse functionality into React objects, which isn’t supported for ES6 classes. A future update to React that adds supports for the key observe API will remove the need for using a mixin.

Make sure the React Native pre-requisites are installed. This should be the case if you worked through the previous tutorial.

In v8.0, React Native moved from using Node.js to io.js. If you don’t have io.js installed, set it up via homebrew by executing the following in a Terminal window:

brew unlink node
brew install iojs
brew link iojs --force

This removes node from your path, downloads the latest version of io.js, and tells homebrew to run io.js whenever you run node.

Next, verify that the starter project is set up correctly. Open Terminal, go to the ParseReactNative-PropertyFinder-Starter directory and execute the following command:

npm install

Next, open PropertyFinder.xcodeproj then build and run the project. The simulator will start and display the app’s home page. Test that you’re able to search for UK listings as you did in the previous tutorial:

previous_app

If everything looks good, then you’re ready to integrate Parse+React with your app.

The Parse+React Structure

The Parse+React layer sits on top of both React Native and the Parse JavaScript SDK:

Architecture

It’s available via npm or GitHub.

Parse+React brings the same simplicity to your data management that React brings to your view layers. To understand how this works, consider the React component lifecycle shown below:

Component Life

Parse+React mimics this flow by hooking into your component’s lifecycle. You first set up queries you want to associate with your component. Parse+React then subscribes your component to receive the query results, fetches the data in the background, passes it back to your component and triggers the component’s render cycle like so:

Data Fetch Lifecycle

Co-locating the Parse query in your component with the view makes it much easier to understand your code. You can look at your component code and see exactly how the queries tie into the view.

Modeling Your Property Data

In this tutorial you’ll take out the calls to the Nestoria API and replace them with calls to Parse. In the next few sections, you’ll see how to set up Parse to do this.

Creating Your Parse App

The first thing you should do is sign up for a free Parse account if you haven’t done so. Next, go to the Parse Dashboard and click Create a new App:

Create new Parse app

Name your app PropertyFinder, then click Create. You should see a success note as well as a link to grab your Parse application keys. Click that link and note your Application ID and JavaScript Key. You’ll need these later.

Click Core from the top menu to go to the Data Browser view, where you can see any data stored on Parse for your app. You should see a page stating that you have no classes to display. You’re going to take care of that by creating dummy data to represent property listings that you’ll pull into your app later on in the tutorial.

Defining your Schema

You can use the data displayed in the current PropertyFinder app to figure out what your schema should be.

Open SearchResults.js and examine the renderRow function. Look for the fields from the Nestoria API that display the data. Next, open PropertyView.js and look at the render function to determine if there’s any additional information you’ll need for your schema.

When you’re done, your list of required data elements should match the following:

  • img_url
  • price_formatted
  • title
  • property_type
  • bedroom_number
  • bathroom_number
  • summary

Now you need to create a class in Parse with these fields to represent a property listing. In your Parse Data Browser, click Add Class and name your class Listing:

Create Parse class

Once you click Create Class, you should see a new Listing class added to the left-hand side. There are other types of classes you can create, such as User, which has some special methods and properties not present in a custom class.

However, your custom class will serve the needs of your app just fine. Think of a Parse class as a database table; the columns you’ll define next are similar in concept to database columns.

Click + Col to add a new column:

Add column to a Parse class

Select File from the type selection, enter img_url, then click Create Column:

Add img column

You should see a new column appear in the header of your class. Parse supports many data types including string, number, boolean, and binary data. Here you’re using the File type to store binary data that represents a property’s image.

Next, add a column to represent the price. To keep things simple, instead of naming the column price_formatted name it price, and select Number for the column type.

Now carry on and create columns for the rest of the fields as follows:

  • title: Type String
  • property_type: Type String
  • bedroom_number: Type Number
  • bathroom_number: Type Number
  • summary: Type String

Verify that all the columns and their types look like the ones shown below:

After columns added

parse_columns_all_added_2

You may have noticed some starter columns were already there when you added the class. Here’s what those are for:

  • objectId: uniquely identifies a row. This ID is auto-generated.
  • createdAt: contains the current timestamp when you add a new row.
  • updatedAt: the time you modified a row.
  • ACL: contains the permission information for a row. ACL stands for “access control list”.

Adding Some Sample Data

You’re almost ready to touch some actual code — oh the anticipation! :] But you’ll need to add some data to work with first.

You can download some sample property photos in this zip file. Download and unzip the file; the photos are contained in the Media directory.

Still within the Data Browser on the Parse site, click + Row or + Add a row. Double-click inside the new row’s img_url column to upload a photo. The label should change from undefined to Upload File as shown below:

parse_upload_file

Click Upload File, browse to house1.jpeg, then click Open. The Data Browser should now show a new row with img_url set:

parse_img_added

You should also see the objectId, createdAt, updatedAt and ACL columns set appropriately. By default, the ACL permission is set to public read and write.

Click Security and change the Listing class permission to public read only:

parse_secure_class

Click Save CLP. Note that the class level permission will supercede an individual row’s permission setting.

Note: There are many options you can use to secure your data. You can learn more from this series of blog posts from Parse.

Continue filling in data for this new row as follows:

  • price: 390000
  • title: Grand mansion
  • property_type: house
  • bedroom_number: 5
  • bathroom_number: 4
  • summary: Luxurious home with lots of acreage.

Armed with this pricely listing, you’re ready to modify your app and test your Parse setup.

Swapping in Parse Calls

It’s finally time to get your hands on the code! You’ll start by retrieving all listings on Parse, without any filtering to begin.

Modifying the Query Logic

Open package.json and add the following two new dependencies:

{
  "name": "PropertyFinder",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node_modules/react-native/packager/packager.sh"
  },
  "dependencies": {
    "react-native": "^0.8.0",
    "parse": "^1.5.0",
    "parse-react": "^0.4.2"
  }
}

Don’t forget to add a comma (,) to the end of the react-native dependency. With this change, you’ve added Parse and Parse+React to your list of dependencies.

Use Terminal to navigate to your project’s main directory and execute the following command:

npm install

This should pull in the dependencies you just added. You should some output similar to the following:

parse-react@0.4.2 node_modules/parse-react
 
parse@1.5.0 node_modules/parse
└── xmlhttprequest@1.7.0

Next, you’ll initialize Parse and add your credentials to the app.

Open index.ios.js and add the following line beneath the other require statements, but before the destructuring assignment of AppRegistry and StyleSheet:

var Parse = require('parse').Parse;

This loads the Parse module and assigns it to Parse.

Add the following code after the destructuring assignment:

Parse.initialize(
  'YOUR_PARSE_APPLICATION_ID',
  'YOUR_PARSE_JAVASCRIPT_KEY'
);

Replace YOUR_PARSE_APPLICATION_ID with your Parse application ID and YOUR_PARSE_JAVASCRIPT_KEY with your Parse JavaScript key. You did write down your Parse application ID and JavaScript key, didn’t you? :] If not, you can always go to the Parse Dashboard and look at the Settings page for your app to find them again.

Open SearchPage.js to make your query logic changes. Add the following code near the top of the file, beneath the React require statement:

var Parse = require('parse').Parse;
var ParseReact = require('parse-react');

This loads the Parse and Parse+React modules and assigns them to Parse and ParseReact respectively.

Next, update the SearchPage declaration to add the Parse+React mixin to the component, just above getInitialState:

var SearchPage = React.createClass({
  mixins: [ParseReact.Mixin],

A React mixin is a way to share functionality across disparate components. It’s especially useful when you want to hook into a component’s lifecycle. For example, a mixin could define a componentDidMount method. If a component adds this mixin, React will call the mixin’s componentDidMount hook as well as the component’s componentDidMount method.

ParseReact.Mixin adds lifecycle hooks into a component when it’s mounted or it’s about to update. The mixin looks for an observe method where the Parse queries of interest are defined.

Add the following method to your component after the getInitialState definition:

observe: function(props, state) {
  var listingQuery = (new Parse.Query('Listing')).ascending('price');
  return state.isLoading ?  { listings: listingQuery } : null;
},

This sets up a Parse.Query for Listing data and adds a query filter to sort the results by least expensive first. This query executes whenever isLoading is true — which is the case whenever you initiate a search.

The results from Parse.Query will be attached to this.data.listings based on the key — listings — that’s paired with the listingQuery query.

Modify _executeQuery as shown below to only set the loading flag for now, rather than perform the call to the server:

_executeQuery: function() {
  this.setState({ isLoading: true });
},

Next, modify onSearchPressed to call _executeQuery:

onSearchPressed: function() {
  this._executeQuery();
},

Right now you’re not using the search term and loading all records instead; you’ll add this later on in the tutorial.

In a similar fashion, modify onLocationPressed to call _executeQuery as follows:

onLocationPressed: function() {
  navigator.geolocation.getCurrentPosition(
    location => {
      this._executeQuery();
    },
    error => {
      this.setState({
        message: 'There was a problem with obtaining your locaton: ' + error
      });
  });
},

Again, you’re calling `_executeQuery()` without using the location information just yet.

To clean up after yourself, delete _handleResponse and urlForQueryAndPage since you have no more need for these response handlers. Ahh, deleting code is so satisfying, isn’t it? :]

This is a good point to test your fetching logic. ParseReact.Mixin forces a re-rendering of your component whenever the results return.

Add the following statement to render just after the point where you set up the spinner:

console.log(this.data.listings);

This logs the listing data each time you render the component — including after you run the query.

Close the React Native packager window if it’s running so you can start afresh.

Open PropertyFinder.xcodeproj and build and run; the simulator will start and display the same UI you know and love from the original app:

reactparse-1

Tap Go and check the Xcode console. You should see some output like this:

2015-06-05 10:14:27.028 [info][tid:com.facebook.React.JavaScript] []
2015-06-05 10:14:27.589 [info][tid:com.facebook.React.JavaScript] [{"id":{"className":"Listing","objectId":"vbHwqDH6n5"},"className":"Listing","objectId":"vbHwqDH6n5",
"createdAt":"2015-06-05T16:23:14.252Z","updatedAt":"2015-06-05T16:24:39.842Z","bathroom_number":4,"bedroom_number":5,
"img_url":{"_name":"tfss-30d28f46-1335-45d7-8d72-e02684c17d25-house1.jpeg",
"_url":"http://files.parsetfss.com/ec34afd8-2b15-4aea-a904-c96e05b4c83a/tfss-30d28f46-1335-45d7-8d72-e02684c17d25-house1.jpeg"},
"price":390000,"property_type":"house","summary":"Luxurious home with lots of acreage.","title":"Grand mansion"}]

The listing data is empty initially, but once you fetch the data it contains the single listing retrieved from Parse. You may also have noticed that the spinner remains once you’ve gotten the search results. This is because you haven’t done anything to properly handle the results. You’ll take care of this later on, but first you’ll take a brief detour into some UI modifications to use Parse data.

Modifying the UI

Open PropertyView.js and remove the following line in render:

var price = property.price_formatted.split(' ')[0];

You no longer have to worry about reformatting price data, since now you have control over the input data.

Modify the related display code, so that instead of accessing the price variable you just deleted, it uses the price property:

<View style={styles.heading}>
  <Text style={styles.price}>${property.price}</Text>

Also notice that you’re now in US territory, so you’ve deftly changed the currency symbol from pounds to dollars. :]

Next, modify the code to access the image’s URI as follows:

<Image style={styles.image}
  source={{uri: property.img_url.url()}} />

You add the call to url() to access the actual image data in Parse. Otherwise, you’d only get the string representation of the URL.

Open SearchResults.js and make a similar change in renderRow by deleting this line:

var price = rowData.price_formatted.split(' ')[0];

You’ll have to modify the related display code like you did before. Since it’s now showing a dollar value, not a pound value, modify the code as follows:

<View style={styles.textContainer}>
  <Text style={styles.price}>${rowData.price}</Text>

Next, update the image access as shown below:

<Image style={styles.thumb}
  source={{ uri: rowData.img_url.url() }} />

Still in SearchResults.js, change rowPressed to check a different property for changes:

rowPressed: function(propertyGuid) {
  var property = this.props.listings
    .filter(prop => prop.id === propertyGuid)[0];

Parse+React identifies unique rows through an id property; therefore you’re using this property instead of the guid.

Similarly, change the implementation of getInitialState to the following:

getInitialState: function() {
  var dataSource = new ListView.DataSource({
    rowHasChanged: (r1, r2) => r1.id !== r2.id
  });
  return {
    dataSource: dataSource.cloneWithRows(this.props.listings)
  };
},

Finally, modify renderRow to use id:

<TouchableHighlight onPress={() => this.rowPressed(rowData.id)}
    underlayColor='#dddddd'>

These changes will use id instead of guid to match up the data records properly.

You’re not quite ready to test your UI code changes. You’ll first need to modify the data fetching logic to transition to your new UI.

Handling the Results

Open SearchPage.js to properly handle your query results. You’ll be camped in this file for the rest of the tutorial, so get comfortable! :]

Earlier on in this tutorial, your data fetch simply logged the results. Remove the debug statement in render as it’s no longer needed:

console.log(this.data.listings);

To properly handle the results, you’ll reset the loading flag in the SearchPage component and navigate to the SearchResults component with the listing data. Keep in mind that ParseReact.Mixin forces a re-rendering of your component whenever the results return.

How can you detect that a fetched result triggered the rendering? Furthermore, where should you check this and trigger the navigation?

ParseReact.Mixin exposes pendingQueries, which returns an array with the names of the in-progress queries. During the search, you can check for a zero length array to indicate the results have returned and hook your completion check in componentDidUpdate that triggers post-render.

Add the following method just above render:

componentDidUpdate: function(prevProps, prevState) {
  if (prevState.isLoading && (this.pendingQueries().length == 0)) {
    this.setState({ isLoading: false });
    this.props.navigator.push({
      title: 'Results',
      component: SearchResults,
      passProps: { listings: this.data.listings }
    });
  }
},

This code first checks isLoading and if true, checks that the query results are in. If these conditions are met, you reset isLoading and push SearchResults with this.data.listings passed to it.

It’s generally frowned upon to change state in componentDidUpdate, since this forces another render call. The reason you can get away with this here is that the first forced render call doesn’t actually change the underlying view.

Keep in mind that React makes use of a virtual DOM and only updates the view if the render call changes any part of that view. The second render call triggered by setting isLoading does update the view. That means you only get a single view change when results come in.

Press Cmd+R in the simulator, then tap Go and view your one lonely, yet very satisfying, result:

reactparse-data1

It’s not much fun returning every listing regardless of the search query. It’s time to fix this!

Adding Search Functionality

You may have noticed that your current data schema doesn’t support a search flow since there’s no way to filter on a place name.

There are many ways to set up a sophisticated search, but for the purposes of this tutorial you’re going to keep it simple: you’ll set up a new column that will contain an array of search query terms. If a text search matches one of the terms, you’ll return that row.

Go to your Data Browser and add a column named place_name of type Array, like so:

parse_add_place_name

Click inside the place_name field of the existing row and add the following data:

["campbell","south bay","bay area"]

Head back to your React Native code. Still in SearchPage.js, modify getInitialState to add a new state variable for the query sent to Parse and also modify the default search string displayed:

getInitialState: function() {
  return {
    searchString: 'Bay Area',
    isLoading: false,
    message: '',
    queryName: null,
  };
},

Next, you’ll need to modify observe to check for the existence of a place name query.

Add the following filter to your Parse.Query to look for the place name:

observe: function(props, state) {
  var listingQuery = (new Parse.Query('Listing')).ascending('price');
  if (state.queryName) {
    listingQuery.equalTo('place_name', state.queryName.toLowerCase());
  }
  return state.isLoading ?  { listings: listingQuery } : null;
},

The equalTo filter looks through the values of an array type and returns objects where a match exists. The filter you’ve defined looks at the place_name array and returns Listing objects where the queryName value is contained in the array.

Now, modify _executeQuery to take in a query argument and set the queryName state variable:

_executeQuery: function(nameSearchQuery) {
  this.setState({
    isLoading: true,
    message: '',
    queryName: nameSearchQuery,
  });
},

Then, modify onSearchPressed to pass the search string from the text input:

onSearchPressed: function() {
  this._executeQuery(this.state.searchString);
},

Finally, modify onLocationPressed to pass in null to _executeQuery:

onLocationPressed: function() {
  navigator.geolocation.getCurrentPosition(
    location => {
      this._executeQuery(null);
    },
    error => {
      this.setState({
        message: 'There was a problem with obtaining your locaton: ' + error
      });
    }
  );
},

You do this as you don’t want to execute a place name search when a location query triggers.

In your simulator, press Cmd+R; your application should refresh and you should see the new default search string.

reactparse-bayarea

Tap Go and verify that you get the same results as before.

Now go back to the home page of your app, enter neverland in the search box and tap Go:

reactparse-blank

Uh-oh. Your app pushed the new view with an empty result set. This would be a great time to add some error handling! :]

Update componentDidUpdate to the following implementation:

componentDidUpdate: function(prevProps, prevState) {
  if (prevState.isLoading && (this.pendingQueries().length == 0)) {
    // 1
    this.setState({ isLoading: false });
    // 2
    if (this.queryErrors() !== null) {
      this.setState({ message: 'There was a problem fetching the results' });
    } else 
      // 3
      if (this.data.listings.length == 0) {
      this.setState({ message: 'No search results found' });
    } else {
      // 4
      this.setState({ message: '' });
      this.props.navigator.push({
        title: 'Results',
        component: SearchResults,
        passProps: {listings: this.data.listings}
      });
    }
  }
},

Taking the code step-by-step you’ll see the following:

  1. Here you turn off isLoading to clear out the loading indicator.
  2. Here you check this.queryErrors, which is another method that ParseReact.Mixin exposes. The method returns a non-null object if there are errors; you’ve updated the message to reflect this.
  3. Here you check if there are no results returned; if so, you set the appropriate message.
  4. If there are no errors and there is data, push the results component.

Press Cmd+R and test the empty results case once again. You should now see the relevant message without the empty results component pushed:

reactparse-narnia

Feel free to add more rows to your Listing class in the Parse Data Browser to test additional search queries; you can make use of the sample photos available in the Media folder you downloaded earlier.

Adding Location Queries

Querying locations is really easy to do with Parse, since Parse supports a GeoPoint data type and provides API methods to peform a variety of geo-based queries, such as searching for locations within a certain radius.

Go to your Data Browser and add a column named location of type GeoPoint:

parse_add_column_location

You’ll need to add some location data for your initial row.

Double-click in the location field and add 37.277455 for latitude area and -121.937503 for the longitude:

parse_add_location

Head back to SearchPage.js and modify getInitialState as follows:

getInitialState: function() {
  return {
    searchString: 'Bay Area',
    isLoading: false,
    message: '',
    queryName: null,
    queryGeo: {},
  };
},

This adds a new queryGeo state to hold location data.

Next, modify _executeQuery to take in location data like so:

_executeQuery: function(nameSearchQuery, geoSearchQuery) {
  this.setState({
    isLoading: true,
    message: '',
    queryName: nameSearchQuery,
    queryGeo: geoSearchQuery,
  });
},

Here, you’ve added an additional parameter for the location-based query and then add whatever’s passed in to the current state.

Next, modify onSearchPressed to pass an empty location to _executeQuery:

onSearchPressed: function() {
  this._executeQuery(this.state.searchString, {});
},

The search button is for when you’re searching by place name rather than by location, which means you can just pass in an empty object for the geoSearchQuery.

Modify onLocationPressed to finally make use of this precious location data by passing it on to _executeQuery:

onLocationPressed: function() {
  navigator.geolocation.getCurrentPosition(
    location => {
      this._executeQuery(
        null,
        {
          latitude: location.coords.latitude,
          longitude: location.coords.longitude,
        }
      );
    },
    error => {
      this.setState({
        message: 'There was a problem with obtaining your locaton: ' + error
      });
  });
},

This time, the updated call to _executeQuery passes in null for the search string and actual coordinates for the geoSearchQuery.

Finally, modify observe to add the location-based search filter:

observe: function(props, state) {
  var listingQuery = (new Parse.Query('Listing')).ascending('price');
  if (state.queryName) {
    listingQuery.equalTo('place_name', state.queryName.toLowerCase());
  } else 
  // 1
  if (state.queryGeo && state.queryGeo.latitude &&
      state.queryGeo.longitude) {
    // 2
    var geoPoint = new Parse.GeoPoint({
        latitude: state.queryGeo.latitude,
        longitude: state.queryGeo.longitude,
    });
    // 3
    listingQuery.withinMiles('location', geoPoint, 25);
  }
  return state.isLoading ? { listings: listingQuery } : null;
},

Taking each numbered comment in turn:

  1. Here you check if this is a location query.
  2. Next, you create a Parse.GeoPoint based on the location coordinates.
  3. Finally, you add a filter for locations within 25 miles of the point of interest.

Before you can test the location-based search, you’ll need to specify a location that will yield results.

From the simulator menu, select Debug\Location\Apple to set your simulated location to a spot near Apple headquarters.

In the simulator, press Cmd+R. Tap Location, permit the app to receive location, then verify that you see the expected result:

reactparse-location

Adding More Test Data

The folder you downloaded earlier contains a JSON test data file — Listing.json — that you can import instead of entering your own data.

To import this data go to your Data Browser and perform the following actions:

  1. Click Import.
  2. Drag Listing.json into the upload area.
  3. Make sure the Custom option is selected and click Finish Import.
  4. Dismiss the pop-up.

parse_data_import

You should receive a confirmation email once it’s done; since you’re importing a very small amount of data, this should happen very quickly.

Once the import is complete, you’ll need to fix the image URLs. These will contain incorrect information and you need to upload the photos yourself. Go to all the newly imported rows and, one by one, delete the existing img_url entry, then upload the corresponding photo from the Media folder.

You’ll notice you have a duplicate for the “Grand mansion” property, since you created it manually and it’s also in the import file. Delete one of the copies to keep things clean.

In your simulator, press Cmd+R, click Location and verify that you see the additional results from your imported test data:

reactparse-imported

Where to Go From Here?

You can download the completed project here. Remember to update index.ios.js with your own Parse application and Javascript keys so you connect to your own data set!

You’ve only scratched the surface of what you can do with Parse+React; there’s a whole world out there beyond simply fetching data. You can save data and even use the underlying Parse JavaScript SDK APIs to create users and or add Parse analytics. Check out Parse+React on GitHub for more details.

For more information on Parse itself, check out our Parse Tutorial: Getting Started with Web Backends. If you want to hear more about React Native, have a listen at our podcast episode with Nick Lockwood, who works on the React Native team at Facebook.

If you have comments or questions, feel free to share them in the discussion below!

The post Integrating Parse and React Native for iOS appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>