Scriptico

HP IDOL OnDemand. Query Text Index Example with UI

With the Query Text Index Swift Example article, I demonstrated how to query text indexes in IDOL OnDemand. A simple RESTful client was created to make a call to API and output the result. For that example, I used Xcode Playgrounds and unfortunately, I did not find a way to make UI written in the playground responsive to user’s actions (text inputs, button clicks, etc.). Eventually, I came to the conclusion that for now such features are simply not supported, and decided to make a simple iOS application to demonstrate how to communicate between UI and the client; this article describes how to do so. Let’s begin!

Goals

The main goal is to make a simple iOS application for the HP IDOL OnDemand RESTful client. Nothing fancy, yet Swift and IDOL OnDemand API!

The second goal is to improve the RESTful client and make it a bit flexible. First of all, our client has to communicate with UI only by using notifications. Also, the approach to make calls by using NSURLConnection should be replaced by using Session tasks in order to gain additional flexibility during the response result processing (you will see how and why during the implementation).

That is it about goals. Let’s check the requirements and start the fun!

Requirements

You should have Xcode installed and the IDOL OnDemand account, with at least one working API key.

For the code example, I used Xcode Version 6.1 (6A1030). Since Swift is in beta for now, further Xcode updates may break the code, written with previous versions.  The project for this example is available on GitHub and I will try to keep it updated and compatible with further Xcode releases.

Implementation

Xcode Project

As it has been defined in goals, I am going to create an iOS application, thus, a new iOS project in Xcode should be created. To make it done, open the Xcode and click on the File -> New ->Project… menu item and then select an iOS Single View Application template. The template selection screen in your environment will look similar to the following:

Xcode iOS single view application template

When the appropriate template is selected, click the next button. In the next screen, provide a product name, organization name and identifier, pick Swift as language and Universal for devices. In my environment, the screen looks like the following:

Xcode swift project options

In the next screen, select the appropriate location to save the project and click the create button.

For the example project, I also disabled all device orientations except ‘Portrait‘ as well as size classes and auto layout for the application storyboard to keep things simple. This last step is optional.

IDOL OnDemand RESTful Client

I am going to start from the IDOL OnDemand Client. Let’s create a new class and call it IDOLOnDemandRESTClient.

//  IDOLOnDemandRESTClient.swift

import Foundation

class IDOLOnDemandRESTClient : NSObject {

}

In the previous article, the example client extended NSURLConnectionDataDelegate and was a delegate for the used NSURLConnection. That approach is not flexible since all request responses were processed by one implementation of the connection(connection: NSURLConnection, didReceiveData data: NSData) and connection(connection: NSURLConnection, didFailWithError error: NSError) defined in the protocol and implemented by the client. Sure, it used to work with only one API to call, but in case of different APIs it would not work.

For example, you may have one function to query text indexes on IDOL OnDemand and the second one is to detect faces in the given photo (yeah, IDOL OnDemand can do it as well). The first guy receives a dictionary with found documents and the second one – a dictionary with detected photos. Most likely, the way how you would treat the response will vary from case to case. What is the solution?

Well, a number of delegates for NSURLConnection could be created, appropriately implemented and later, for each call, a correct one would be used. It definitely would work, but for this example, I am going to pick another way to go – URL session tasks.

URL sessions provide three types of tasks: data tasks, upload tasks, and download tasks; for this example, I need data tasks. According to the documentation, data tasks request a resource, returning the server’s response as one or more NSData objects in memory. That’s exactly what I need, so let’s implement this approach.

First of all, let’s create a method which will create and configure a new NSURLSession for each request:

//  IDOLOnDemandRESTClient.swift

import Foundation

class IDOLOnDemandRESTClient : NSObject {

    // Returns properly the initialized session
    func getUrlSession() -> NSURLSession {
        // getting the default session configuration
        var sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
        return NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
    }
}

As you can see, I am using nil as a delegate for the NSURLSession object since later in the code, I am going to use completion handler blocks for session tasks. According to Apple, completion handler blocks are primarily intended as an alternative to using a custom delegate. If you create a task using a method that takes a completion handler block, the delegate methods for response and data delivery are not called.

Secondly, let’s define and properly initialize API_KEY, API_HOST, and API_PATH constants as the following:

//  IDOLOnDemandRESTClient.swift

import Foundation

/*
*   The API key is removed from the committed project
*
*   To obtain a new API key please sign up at http://www.idolondemand.com/signup.html
*/
let API_KEY = ""

let API_HOST = "https://api.idolondemand.com"
let API_PATH = API_HOST + "/1/api/sync/querytextindex/v1?text=%@:title&indexes=wiki_eng&apikey=" + API_KEY

class IDOLOnDemandRESTClient : NSObject {
    // some code is omitted
}

To get values for API_HOST and API_PATH, I used the IDOL OnDemand Query Text Index API overview. As you can notice, I left the API_KEY constant empty, so please do not forget to provide your key to make it work.

In addition to these constants, I created two more for the success and error notifications as below:

//  IDOLOnDemandRESTClient.swift

import Foundation

let REQUEST_SUCCESS_NOTIFICATION = "RequestSuccessNotification"
let REQUEST_ERROR_NOTIFICATION = "RequestErrorNotification"

// some code is omitted

Basically, if a client makes a request to API and receives the response successfully, I will notify the observers about it by sending REQUEST_SUCCESS_NOTIFICATION with the received data; otherwise, I am going to send a notification with the REQUEST_ERROR_NOTIFICATION identifier and error message. You are step away to see how to do send notifications, and a few more steps away from the explanation how to receive and handle notifications in the code.

Now, let’s write our code to query the text index:

//  IDOLOnDemandRESTClient.swift
// 
func queryTextIndex(query: String) {
     var filteredQuery = query.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.LiteralSearch, range: nil)

     var urlString = String(format: API_PATH, filteredQuery)
     var task:NSURLSessionDataTask = getUrlSession().dataTaskWithRequest(NSURLRequest(URL: NSURL(string: urlString)!), completionHandler: { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
          // the completion handler implementation
     })

     task.resume()
}
//

The first highlighted line shows how the NSURLSessionDataTask task is created, the second highlight shows the completion handler body, and the line #13 demonstrates how to run the created task. How does it work in threads? In the main thread, the UI component adds itself as an observer to the notification center for success and error notifications (you can say, events), fires the queryTextIndex(query: String) function and as soon as the task is created and started, the main thread “returns” to UI. As soon as the completion handler receives a response, it dispatches (you can say also sends or posts) either success or error notification. The component receives either one notification in the other thread and updates its UI, but in the main thread. That’s it. If you’ve just begun to develop UI applications, it may confuse you but there is not that much magic going on – just put break points in the controller where the client is called, in the function being called, in the completion handler and finally, in the component handlers for the notifications. Debug the calls and it should clarify the approach.

Ok, now let’s implement our completion handler:

//  IDOLOnDemandRESTClient.swift

func queryTextIndex(query: String) {
     var filteredQuery = query.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.LiteralSearch, range: nil)

     var urlString = String(format: API_PATH, filteredQuery)
     var task:NSURLSessionDataTask = getUrlSession().dataTaskWithRequest(NSURLRequest(URL: NSURL(string: urlString)!), completionHandler: { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
          // the completion handler implementation
          if nil != error {
                NSNotificationCenter.defaultCenter().postNotificationName(REQUEST_ERROR_NOTIFICATION, object: error.description)
          } else {
               if data == nil {
                    NSNotificationCenter.defaultCenter().postNotificationName(REQUEST_ERROR_NOTIFICATION, object: "no data were recieved")
                } else {
                    var resultAsJSON: AnyObject?  = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: nil)

                    if resultAsJSON is NSDictionary {
                        var result: NSDictionary = resultAsJSON as NSDictionary
                        if nil == result.valueForKey("message") {
                            NSNotificationCenter.defaultCenter().postNotificationName(REQUEST_SUCCESS_NOTIFICATION, object: resultAsJSON)
                        } else {
                            NSNotificationCenter.defaultCenter().postNotificationName(REQUEST_ERROR_NOTIFICATION, object: result.valueForKey("message"))
                        }
                    } else {
                        NSNotificationCenter.defaultCenter().postNotificationName(REQUEST_ERROR_NOTIFICATION, object: " malformed data")
                    }
                }
            }
     })
     task.resume()
}

In case an error occurred, the error notification is posted. The same happens if the received data is nil or data object cannot be serialized as JSON. Also, the line #23 shows how to detect if IDOL OnDemand API returned an error. If the received dictionary has the message key, it means that something went wrong with the request. You can check this case by calling API without the API key. If no errors occurred neither during the call nor serialization, the client posts a success notification in the 24 line. The queryTextIndex(query: String) function is completed.

The very last step with the client is to make it singleton. Well, the singleton pattern could be an evil thing, yet with UI application it is ok. We are going to call the client only from one thread, yet the following implementation is thread-safe just for any case as well.

//  IDOLOnDemandRESTClient.swift

class IDOLOnDemandRESTClient : NSObject {
    
    class var sharedInstance : IDOLOnDemandRESTClient {
        
        struct Static {
            static var onceToken : dispatch_once_t = 0
            static var instance : IDOLOnDemandRESTClient? = nil
        }
        
        dispatch_once(&Static.onceToken) {
            Static.instance = IDOLOnDemandRESTClient()
        }
        
        return Static.instance!
    }

    // some code is omitted 
}

Now, the client is ready and it could be called as the following:

// using 'Hewlett Packard' as a query
IDOLOnDemandRESTClient.sharedInstance.queryTextIndex("Hewlett Packard")

Extensions

Before I start with the user interface, let me write simple extensions of NSObject, NSArray, and NSDictionary classes. According to Apple,  Extensions add new functionality to the existing class, structure, or enumeration type. This includes the ability to extend types for which you do not have access to the original source code. If you are familiar with Objective-C, Swift extensions are similar to categories yet they have no names. Now, the question is why do I need extensions?

The example application should show the received result in UI to the user. To show the data, I am going to use UITextView and there is no way to add a dictionary (API returns a dictionary with documents for the query index calls) for the view. So, I need to transform the result dictionary to a string. I could make such a thing either in the controller, client, or some helper class but it would not be nice, since I want to keep the code coupling as low as I can.

To begin, let’s create a new swift file, call it AppExtensions.swift and add an extension for NSObject

//  AppExtensions.swift

import Foundation

extension NSObject {
    // returns an indent for the given depth
    func getIndentForDepth(depth: Int) -> String {
        var result = ""
        var currentDepth = depth
        while currentDepth > 0 {
            result += "   "
            --currentDepth
        }
        return result
    }
}

For now, all classes which extend NSObject will have getIndentForDepth(depth: Int) -> String method. This method returns an indent for the given depth.  During the dictionary key/value traversing, a value could be either ‘plain’ or collection object. Thus, the traversing will be recursive and to make the result readable, I want to format it by using indents. The indent is just a number of spaces multiplied by the given depth and the depth shows the current level of object. Huh, I hope I did not confuse you with my writing 🙂

Now let’s define an extension for NSDictionary

//  AppExtensions.swift

extension NSDictionary {
    func toString(depth: Int) -> String {
        var result = ""
        if self.count > 0 {
            
            for (key, value) in self {
                
                // adding the indent for the current depth
                result += getIndentForDepth(depth)
                // adding the key value (in this case keys are strings)
                result += key as String
                // adding a delimiter between key and value
                result += ":"
                
                // processing the current value
                // checking if the current value is a dictionary
                if value is NSDictionary {
                    // starting always from the new string
                    result += "\n"
                    // converting the dictionary to a string
                    var valueAsDictionary = value as NSDictionary
                    result += valueAsDictionary.toString((depth + 1))
                    // adding the new string delimiter
                    result += "\n"
                    // continuing to the next element
                    continue
                }
                
                // checking if the current value is an array
                if value is NSArray {
                    // starting always from the new string
                    result += "\n"
                    // converting an array to a string
                    var valueAsArray = value as NSArray
                    valueAsArray.toString((depth + 1))
                    result += valueAsArray.toString((depth + 1))
                    // adding a new string delimiter
                    result += "\n"
                    // continuing to the next element
                    continue
                }
                
                // treating a value as a string by default
                result += "\(value)"
                // adding a new string delimiter
                result += "\n"
            }
        }
        return result
    }
}

In the loop, I am checking the type of the current value. If the value is a dictionary, I am increasing the depth and call the same method recursively. If the value is NSArray, I am increasing the depth as well and transforming the array to a string by calling toString(depth: Int) -> String (see the NSArray extension below). If the current value is a ‘plain’ object, I am creating a string with it. Now, the NSArray extension.

//  AppExtensions.swift

extension NSArray {
    func  toString(depth: Int) -> String {
        var result = ""
        if self.count > 0 {
            for value in self {
                // adding the indent for the current depth
                result += getIndentForDepth(depth)
                
                if value is NSDictionary {
                    // converting the dictionary to a string
                    var valueAsDictionary = value as NSDictionary
                    result += valueAsDictionary.toString((depth + 1))
                    // adding a new string delimiter
                    result += "\n"
                    // continuing to the next element
                    continue
                }
                
                // checking if the current value is an array
                if value is NSArray {
                    // converting an array to a string
                    var valueAsArray = value as NSArray
                    valueAsArray.toString((depth + 1))
                    result += valueAsArray.toString((depth + 1))
                    // adding a new string delimiter
                    result += "\n"
                    // continuing to the next element
                    continue
                }
                
                // treating a value as a string by default
                result += "\(value)"
                // adding a new string delimiter
                result += "\n"
            }
        }
        return result
    }
}

This guy is a bit similar to the NSDictionary extension. It is looping the array elements and processing the current element based on its type. If  the element is either dictionary or array, it increases the current depth and is going deeper to the object.

You are one step away from seeing how it works, so excuse me if this section is confused. Let’s just go to the final step – UI.

User Interface

The application has only one view and as you can see, it is pretty much simple. The following screenshot (sorry for a bit blurry image) shows two important states of the application and there is not that much to describe

IDOL OnDemand Query Text Index Example iOS Application

Now, the UI controller and its important parts. I am going to skip the part how to add new controls to the view and connect them with the controller.

First important thing in the controller is how this guy registers itself in the notification center and defines two handlers for the expected notifications

//  ViewController.swift

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    
    // Handler for REQUEST_SUCCESS_NOTIFICATION
    func onRequestSuccess(notification: NSNotification) {
        // implementation is omitted
    }
    
    // Handler for REQUEST_ERROR_NOTIFICATION
    func onRequestError(notification: NSNotification) {
        // implementation is omitted
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        // adding observers for expected notifications
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "onRequestSuccess:", name: REQUEST_SUCCESS_NOTIFICATION, object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "onRequestError:", name: REQUEST_ERROR_NOTIFICATION, object: nil)
    }
}

In the init method, it adds itself as an observer for notifications, defined in our client, and specifies selectors for each handler. In the handlers, the following shows how to update UI elements in the main thread from another one

//  ViewController.swift

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    
    // Handler for REQUEST_SUCCESS_NOTIFICATION
    func onRequestSuccess(notification: NSNotification) {
        
        // updating view elements in the main thread
        dispatch_async(dispatch_get_main_queue(), {
            // showing result (the received result is always a dictionary in our case)
            var result = notification.object as? NSDictionary
            var resultAsString = result?.toString(0)
            // updating ui, code is omitted
        })
    }
    
    // Handler for REQUEST_ERROR_NOTIFICATION
    func onRequestError(notification: NSNotification) {
        var error: String = notification.object as String
        
        // updating view elements in the main thread
        dispatch_async(dispatch_get_main_queue(), {
            // updating ui, code is omitted 
        })
    }
}

As you can see, for the received dictionary, I am calling the toString(depth: Int) -> String method to transform the dictionary to a string. I hope it clarified why I wrote the extensions described above.

One more important thing is how the controller calls the client

//  ViewController.swift

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    
    // some code is omitted

    @IBAction func sendRequest(sender: AnyObject) {
        // checking if a query is provided
        if !queryTextField.text.isEmpty {
   
            // invoking API
            IDOLOnDemandRESTClient.sharedInstance.queryTextIndex(queryTextField.text)
        } else {
            // showing an alert
        }
    }
}

the sendRequest(sender: AnyObject) function is IBAction for the “query the text” button for the touch up inside actions. The highlighted line is the point where the queryTextIndex(query: String) function is being called.

That’s it. The complete project is available on GitHub. Feel free to write me a line if you have any questions.

Resources

Category: Development, Examples, HP IDOL OnDemand, iOS, iOS, Mobile, Swift, Swift, User Interface

Tagged: , , ,

One Response

  1. […] exited about the topic and ready to slightly embellish our example, please check my other article HP IDOL OnDemand. Query Text Index Example with UI […]

Leave a Reply

ERROR: si-captcha.php plugin: GD image support not detected in PHP!

Contact your web host and ask them to enable GD image support for PHP.

ERROR: si-captcha.php plugin: imagepng function not detected in PHP!

Contact your web host and ask them to enable imagepng for PHP.