Networking

It is time to learn networking! Almost every app has to be connected to the internet for some reason - some of them communicate with their server's API to get or post some data there; some need to download documents or images, others use the internet to send messages, etc.

Networking is pretty easy with Rubymotion, especially if you use afmotion gem. This gem is based on the best iOS networking library AFNetworking and, therefore, is very stable.

Raw AFNetworking may be challenging to learn. afmotion is an awesome and easy to use wrapper than can manage with all main networking tasks.

To install it, it is enough to add it to your Gemfile, and run bundle install. After you need to run rake pod:install as well, because afmotion has AFNetworking as a dependency.

        gem "afmotion"
      

We are going to use afmotion in superapp. Each time user adds new mood object, an app will silently get current weather at his location.

Networking Basics

As you may already know, there are four types of requests:

If you don't have experience with back-end development, all these request types may look weird to you. In fact all requests are very easy - each request has its type (GET/POST/PUT/DELETE), URL, and sometimes requests may have some additional data (for example user data when a user updates his profile). When a server receives some request, it checks whether URL is correct and whether it has some action associated with a given request type to a given URL. If action is available, the server will start processing it. Otherwise, it will return an error.

Four types of requests supposed to make URLs simpler. Your server can have four different actions set to 1 URL. Let's take http://example.com/users/1 URL as an example:

You should know that this is just an example, and all servers have different routes conventions, but this was a most popular one.

Even though there are lots of types of data that can be transferred over HTTP, for communication with servers you will usually use JSON - its structure is very similar to Ruby's Hash, and is very clean straightforward. If you want to create new (or receive existed) user object from the server, JSON will look just like this:

        {
          user: {
            first_name: "John",
            last_name: "Smith",
            email: "mail@example.com",
            # ...
          }
        }
      

AFMotion basics

We know that there are four request types, but we don't know how to send them yet. With afmotion, it is very easy. To send GET to request Google's home page we should use next code:

        AFMotion::HTTP.get("http://google.com") do |result|
          p result.body
        end
      
See that HTTP.get? It means that afmotion is going to send a GET requests and expects to receive an HTTP response. Therefore, result.body will contain an HTML code of a Google's homepage.

If you want to receive JSON, you can specify it instead of HTTP:

        AFMotion::JSON.get("http://jsonip.com") do |result|
          p result.object
        end
      
In this example, we will receive a JSON, and result.object will be a Hash. Also, notice a difference in a place where a response is stored. When asking for an HTTP response, it will be in result.body. If you are expecting to receive JSON, resulting object will be stored in result.object.

HTTP and JSON are not the only allowed operations. You can choose from following types:

Downloading an image may sound complicated, but on practice it is a standard GET request:

        AFMotion::Image.get("https://www.google.com/images/srpr/logo3w.png") do |result|
          # image will be stored in result.object:
          image = result.object

          image.is_a?(UIImage)
          # => true

        end
      

All requests in afmotion are asynchronous, which means a user will have to wait for a result but meanwhile he can do some other fascinating stuff like watching a progress indicator.

Clients

Most apps have one main API to interact with. These API may require some custom headers (for example for authorisation). In this cases you can create a client once, setup it for the API server, and then use it whenever you like. For example:

        client = AFMotion::Client.build("https://example.com/") do
          # set custom headers:
          header "Accept", "application/json"
          header "Accept-Encoding", "gzip,deflate"
          header "Authorization", bearer_header

          # we use JSON to interact with the server
          response_serializer :json
        end

        # And somewhere in your app
        client.get("stream/0/posts/stream/global") do |result|
          ...
        end
      
All requests sent with the client will have the same pattern described during initialization. Each request whether it is GET, POST, PUT or DELETE will be sent with a given set of headers, will be using provided URL and will use provided response serializer.

Superapp

So let's go back to our superapp. We will use forecast.io to retrieve weather information. We know that we need to do a one GET request each time user creates a new mood. But first, we need to register there to get their API key. It is easy - just open their Developers page, check their policies, register, and copy API key that you will be able to found in your dashboard.

I would suggest creating a file that would have all our constants like forecast.io's API key, it's host, maybe some other constants in the future. Let's create /app/app_constants.rb file. We are going to create an AppConstants module that will contain a list of all constants. If we need to use it in another file, we will include it with include AppConstants:

        module AppConstants
          FORECAST_API_KEY = "your_api_key_here"
          FORECAST_API_URL = "https://api.forecast.io/forecast/"
        end
      

Do you remember about MVC pattern? The one with models, views and controllers. Views are showing some info to the user, models are getting this info, and controllers connect view to the correct model. To get weather info, we will use Forecast model. It will be stored in a /app/models/forecast.rb file.

Let's read the forecast.io docs to understand how we can retrieve forecast. It seems like we need to make a GET request to https://api.forecast.io/forecast. To the end of this URL, we will need to add just obtained API key, location in a Latitude and Longitude format, and a date.

We are going to use our new Forecast model. It will have a class method Forecast.for_location(location, on_date: date, &block) that will receive location, date and completion block, and will send a request to forecast.io servers with a provided data. When a response is received, we will check whether it has any errors, and will call a completion block provided by a user.

This method will pass a retrieved forecast to the completion block, and a developer will be able to do whatever he wants with it.

Also, we need a method that will transfer forecast summary into an icon - we will show it in a Mood Cell, somewhere near mood description. Since forecast.io returns us an icon parameter that describes the weather very well, we will use it as a weather summary, and as an icon name:

        class Forecast
          include AppConstants
          include MotionRealm

          def self.for_location(location, on_date: date, &block)

            # convert date to a string that forecat.io can understand:
            if date.is_a?(NSDate)
              date_formatter = NSDateFormatter.alloc.init
              date_formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZ"
              date = date_formatter.stringFromDate(date)
            end

            # create a URL from given API key, location and date:
            url = "#{FORECAST_API_URL}#{FORECAST_API_KEY}/" +
              "#{location[:lat]},#{location[:lng]}," +
              "#{date}"

            # send a request to forecast.io
            AFMotion::JSON.get(url) do |result|
              if result.success? && result.object.is_a?(Hash)

                # print result:
                p "Received result":
                p result.object["hourly"]["data"][0]

                # save icon and temperature for a future use:
                icon = result.object["hourly"]["data"][0]["icon"]
                temperature = result.object["hourly"]["data"][0]["temperature"]

                # create and return forecast to a completion block
                forecast = { summary: icon, temperature: temperature }
                block.call(forecast, nil) if block.is_a?(Proc)
              else

                # failed to get weather forecast:
                block.call(nil, result.error) if block.is_a?(Proc)
              end
            end
          end

          # generate an icon for a forecast.
          # check available icons at https://developer.forecast.io/docs/v2
          def icon
            path = "images/weather/" + self.summary + ".png"
            path.uiimage
          end
        end
      
Here we included constants module to the Forecast class to know forecast.io's URL and API key. Inside the method, we created a URL where we will send a GET request. URL schema is well described in the forecast.io docs: it should have API key in the beginning, latitude and longitude, and date in the end. Inside afmotion's completion block we check response status. If it succeeded, we get main forecast data and pass it to user's completion block. You can find the second parameter here which is nil - I reserved it for an error in this example. In cases when forecast weather has not been retrieved, an error will be passed to completion block instead of nil.

You may wonder what is result.object["hourly"]["data"][0]. You can try to add the code p result.object to see how does forecast.io API response looks like. Basically, it is a multi-level JSON that looks similar to this:

        {
          "offset"=>-8,
          "hourly"=>{
            "summary"=>"Partly cloudy starting in the morning, continuing until afternoon.",
            "icon"=>"partly-cloudy-day",
            "data"=>[
              {
                "temperature"=>48.05,
                "windSpeed"=>1.28,
                "humidity"=>0.88,
                "windBearing"=>300,
                "cloudCover"=>0.28,
                "time"=>1447833600,
                "dewPoint"=>44.59,
                "summary"=>"Partly Cloudy",
                "icon"=>"partly-cloudy-night",
                "precipIntensity"=>0,
                "visibility"=>8.16999999999999,
                "ozone"=>269.59,
                "apparentTemperature"=>48.05,
                "pressure"=>1024.89,
                "precipProbability"=>0
              }, {
                "temperature"=>47.64,
                ...
              }
            ]
          }
        }
      
result.object["hourly"]["data"][0] is just a place where data that we are looking for is stored. Firs we access result.object to get all this huge JSON. Then we go one level deeper into result.object["hourly"] where the hourly forecast is stored, and then deeper.

icon method of a forecast instance returns an image that represents weather. I've found some images on IconFinder that we could use - their license allows us to use them here in the tutorial without a link back. You can download them here, or find your own icons. If you are going to add your own icons, make sure they have next names:

Cool, we now have a method that retrieves a forecast. Let's integrate it into the app. Let's add it to the MoodSelectorScreen: when Location Manager retrieved user's location, we will pass it directly to our new Forecast class, and it will get forecast for a given location:

        class MoodSelectorScreen < PM::Screen
          title "Mood Selector"

          # MoodSelectorScreen code ...

          def update_forecast
            return if !@location.is_a?(Hash)

            Forecast.for_location(@location, on_date: NSDate.now) do |forecast, error|
              if error
                UIAlertController.alert self, "Error",
                  message: error.localizedDescription,
                  buttons: ["OK"]
              else
                @forecast = forecast
                p "Got forecast:"
                p forecast
              end
            end
          end

          def locationManager(manager, didUpdateLocations: locations)
            # locations is an array of all user locations. Last is the newest one:
            location = locations.last
            manager.stopUpdatingLocation

            @location = {
              lat: location.coordinate.latitude,
              lng: location.coordinate.longitude,
            }

            update_forecast

            NSLog "Location:"
            NSLog "#{@location}"

          end

          # rest of the code ...
        end
      

You can try to launch the app, and open Mood Selector screen. In a second or two your app will print some logs into console:

Summary

Networking is not that difficult as novice developers usually imagine. Especially if you use afmotion gem - with it, all network requests need just a line or two. For an average request, you just need to know

Then just add some block that will process a response. Check if the request succeeded with result.success? or if it failed with result.failure?. Your response data will be stored in result.object for JSON requests, and in result.body for an HTTP requests.

Book Index | Next