Building a Drupal CMS to manage content on iOS - Part 2 (the iOS bit)

Welcome to Part 2. In part one you explored the Drupal side of the system, installed the necessary modules, created a content type, created a node queue and populated it with some content.

You should now be able to check the version of a content package and retrieve a path to download it e.g

http://contentcaptureexample.digitallabsmmu.com/contentpackagerjson/1

We will now turn our attention to the iOS application that downloads and utilises the packaged content from Drupal. The app downloads the content in the Cities nodequeue and uses this content with a TableViewController. The TableViewController displays the different Cities that were added on the Drupal side. These cities can then be selected to view more detail, showing the description and the image.

This article will provide an overview of the approach used, including:

  • Checking if there is already existing content on the phone that can be utilised
  • Accessing Drupal and checking if there is a new or updated group of content
  • If there is new or updated content, then download it to the iOS device and unzip it
  • Once the download is complete, fire off a notification to tell any classes interested that the download of a specific package of content is complete
  • Any classes interested in the notification will use the newly downloaded content, updating their displays

All the code for this tutorial series can be found at https://github.com/CMDT/ContentCaptureExample. You will need to have completed the Drupal set up as described in Part 1 of this series [LINK] and the iOS code needs to be downloaded. Cocoapods will need to be downloaded and installed for you to make use of the third party libraries used within this app. These third party libraries are AFNetworking and ZipArchive. The Podfile needed can be found in the GitHub repo.

The application structure is split into Models, Views, Controllers and Helpers. A break down of the different classes involved in the iOS application and their roles will now be provided.

ContentManager

The role of the Content Manager class, as the name suggested to manage the content. This includes the following tasks:

  • Checking for existing content
  • Checking for content updates
  • Downloading the packaged content
  • Extract the packaged content

Checking for existing content involves initially checking the documents folder on the phone for a particular nodequeue's manifest.json file. If there is not a file on the phone, any content distributed with the application is copied from NSBundle to the phone’s documents folder. If there was content distributed with the application, and it was copied successfully to the phone’s documents, then a version number 0 is saved to NSUserDefaults. By the end of this process if there is not a manifest.json file for a particular nodequeue id in the phone’s documents then there is no existing content for that nodequeue.

As mentioned above a version number is saved to NSUserDefaults. This is done for each nodequeue id within the application, therefore the key used is the nodequeue id. This version number allows us to check whether an update has occurred to the content on the Drupal side. The ContentManager provides methods for both setting and retrieving the version from NSUserDefaults.

The ContentManager checks for updates to content for a particular nodequeue. This is where both the third party libraries are used, AFNetworking and ZipArchive. AFNetworking is used to access the Drupal URL (which is stored in AppNameContentManager and discussed in the next section) and retrieve the results as a dictionary. This URL is accessing the method created in the Drupal Content Packager module, which returns the version of a package of content and the path to the packaged content for a nodequeue id.

Next, a comparison is performed to see if the current version for the nodequeue id, stored in NSUserDefaults is lower than the retrieved version. If the current version is lower this means an update to the content has taken place and this content package now needs to be downloaded again.

  if (currentVersion == nil || versionComparisonResult == NSOrderedAscending) { 
 
    NSString *downloadUrl = [responseObject valueForKey:@"file_path"]:
 
    [self downloadZipFromURL:downloadUrl withNodequeueID:nodequeueID withNewVersion:newVersion];
 
  } else {
 
     NSLog(@"Current version is latest version. No new content for nodequeueid: %@.", nodequeueID);
  }

Content is downloaded from the path stored within the dictionary returned using AFNetworking. The content is downloaded as a zip file to the phone’s documents directory where it is then unzipped. Once the unzipping of the content has been completed a check is performed to see if content already exists for this nodequeue id, within the documents directory. If it does it is deleted the new folder of content is copied over. For example, if content for a nodequeue with an id of 1 is downloaded, its content is unzipped to a folder named download_nqid_1. When the check is performed to see if there is existing content, the check is looking for a folder named content_nqid_1 within the documents directory. If this folder exists it is deleted and the contents of download_nqid_1 are copied to content_nqid_1. Then, download_nqid_1 is deleted and so is the original zipped up file.

if ([self unzipZipFileFromPath: downloadZipFolderFilePath toPath:downloadFolderFilePath]) {
 
  //if a file exists already at the path to copy to then remove it
  if ([fileManager fileExistsAtPath: contentFolderFilePath]) {
 
    [fileManager removeItemAtPath :contentFolderFilePath error:nil]:
  }
 
  //perform copy then delete unnecessary files
  if ([fileManager copyItemAtPath:downloadFolderFilePath toPath:contentFolderFilePath error:nil] ) {
 
    if ([fileManager removeItemAtPath:downloadZipFolderFilePath error:nil] &&
      [fileManager removeItemAtPath:downloadFolderFilePath error:nil] ) {
 
      //unzipping, copying and deleting successful
      return YES:
    }
  }
}

The reason this unzipping process is done in this way with two different file names is because the original content needs to be deleted before the new content can be copied to the phone’s documents folder. If we didn’t use two file names it would require the existing content to be deleted before the new content is even deleted. This means that if there was a problem with the download then there would be no content on the phone anymore. The way we have done it means that if there is a problem downloading the new content, there is still content for the app to use, rather than no content.

The ContentManager is not specific to any application.

AppNameContentManager

A class called AppNameContentManager extends the ContentManager, with AppName being replaced with the name of the application. So, in this example the class is called ContentCaptureExampleContentManager. This class holds the application specific information, such as the web address to the Drupal page created and the nodequeue ids.

For the Cities example, change the web address to the Drupal site that you created. Remember, the end part of the web address is using the Drupal menu system to call the method in the Content Packager module we require.

There was only one nodequeue created, the Cities nodequeue, so the nodequeue id should be 1. However, this can be checked by accessing the Update Content page on the Drupal website. We have placed this nodequeue id in a constant, called NODEQUEUE_CITY, that can be accessed across the application. This is shown below.

ContentCaptureExampleContentManager.h file:

extern NSInteger const NODEQUEUE_CITY

ContentCaptureExampleContentManager.m file:

NSInteger const NODEQUEUE_CITY = 1;

If your application has more nodequeues then each nodequeue would need a corresponding constant. In the initialiser for this class these nodequeue constants are added to an array, which is used by the other methods of this class. These methods include checkForExistingContent and checkForUpdates. Both of these methods act as a wrapper, looping through the array of nodequeue constants and calling a corresponding method in the parent ContentManager class on each one. These methods in the child ContentCaptureExampleContentManager class are called by the AppDelegate.

AppDelegate

Two of the existing methods in AppDelegate have been utilised in this application.

The first, application:didFinishLaunchingWithOptions: calls the checkForExistingContent method in the ContentCaptureExampleContentManager class. This means that when the application finishes launching a check is performed on each nodequeue of content to see if there is already existing content on the phone to be used.

The second method used, applicationDidBecomeActive:, calls the checkForUpdates method in the ContentCaptureExampleContentManager class. This means that when the application comes back from being in the background, a check is performed on each nodequeue of content to see if there has been any updates to that content.

Node

The Node class contains the properties that a basic node will contain. A basic node will usually have a title, content and image(s). This Node should represent the structure of the content type you created on the Drupal side. For example, the City content type has a title which is the name of the city, content which is a description of the city and an image.

If you have many different types of content on Drupal that contain many different fields then your application may need more than one Node, as it may require specific nodes. This could be achieved by using Node as a base class with title and content as all Nodes will usually have these fields. Then extend Node with specific details of the different Nodes needed within the system.

For example, the City application could be extended to include attractions. These attractions may just include a title, content, and an image which means that they can also use the Node object. If the attractions also required a rating then a class called AttractionNode could be created which would extend the existing Node class, adding in a rating property.

NodeDataProvider

NodeDataProvider contains all the specific details relating to Drupal. This means that if something was to change on the Drupal side, for example how the JSON is created, then only this class would need to be changed. NodeDataProvider implements the following tasks:

  • Extracts the JSON for a given nodequeue id
  • Creates Node objects
  • Provides an array of Node objects for a given node queue id

The method nodesWithNodequeueId: is the only publicly accessible method of this class. It returns an array of Node objects, which were created from the manifest.json file. This method calls the extractJSON: method, as the JSON within the manifest.json file needs to be extracted into an array first. Once there is an array of JSON the nodesWithNodequeueId: can loop through and create a Node object from each node within the JSON array. Once the Node object is created it is added to the array containing all the Node objects. This is shown below:

+(NSArray *) nodesWithNodequeueId: (NSNumber *)nodequeueid
{
  NSMutableArray *nodesArray = [[NSMutableArray alloc] init];
  NSArray *nodesFromJSON = [self extractJSON:nodequeueid];
 
  for (NSDictionary *item in nodesFromJSON) {
 
    Node mode = [N0deDataProvider createNodeWithDictionary:item withNodequeueId:nodequeueid];
    [r-odesArray add0bj ect : node] ;
  }
 
  return nodesArray;
}

The createNodeWithDictionary: withNodequeueId: method extracts the values of the required fields from the JSON and stores them in a Node objects properties. For a node’s images, this method calls getNodeImagesWithDictionary: withNodequeueId:, which loops through and looks for any fields that begin with ‘field_image’, as this was the naming convention used on the Drupal side (discussed in part 1). When it finds a field with this naming convention it stores this field name in a dictionary as the key, and extracts the image name, storing it as the value in the dictionary. This allows for different parts of the iOS application to retrieve the image they need by the key. In terms of the City application, the images are all field_image_city, but more images could be used.

The NodeDataProvider would need to be changed if the structure of the Node class changed.

Controllers & Views: TableViewController, DetailViewController, DetailView and TableViewCell

The City application has used the content downloaded from Drupal in a TableViewController. The TableViewController lists the Cities in cells and when they are selected a DetailView is displayed with all of the City’s details.

The TableViewController uses the constant, NODEQUEUE_CITY discussed earlier from ContentCaptureExampleContentManager to store the nodequeue id who’s content will be displayed by the TableViewController.

self.nodequeueID = [NSNumber numberWithInteger: NODEQUEUE_CITY];

The viewWillAppear: method is used to call the checkForUpdateWithNodequeueID: method from ContentManager whenever the view appears. This is because the TableViewController needs to know if the content for its nodequeue has been updated since it was last visible. If it has been updated then the content it is displaying needs to be changed.

This TableViewController class contains an array of Node objects for it’s nodequeue id which it retrieves using the public method nodesWithNodequeueId: from NodeDataProvider. This happens when the view is first loaded and when an update to the content has occurred. This class knows there has been an update to this nodequeue id’s content as it receives a notification from NSNotifcationCenter, when an update has finished in the ContentManager class.

// listen for when content has been updated
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateContent: ) name:ContentUpdateDidCompleteNotification object:nil];

TableViewController uses it’s Node objects, extracting the details needed to display the correct information on the table cells. The table cells are a custom cell, TableViewCell which contain a label to display the City name.

(UITableViewCell *)tab1eView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  TableViewCell *cell = [tableview dequeueReusableCellWithIdentifier:@"NodesTableViewCell"];
  Node *node = [self.node0bjects objectAtIndex:indexPath.row];
  cell.nodeTitleLabel.text = node.title;
  return cell;
}

When a Node, City in this example, is selected within the TableViewController the prepareForSegue: sender: method is called as the DetailViewController is used to display the DetailView for the selected City node. The DetailViewController populates it’s DetailView with the City node's properties, which it retrieves from the Node object.

Summary

To recap, this article has described the iOS application that downloads and uses the content created on Drupal. It takes the Cities nodequeue and uses it’s content to display a list of the cities within a table. When one of the Cities is selected then the details of it are shown.

Find another example system at https://github.com/CMDT/DrupalCMS, which shows the system being used with a MapView, CollectionViewController and PageViewController.