Gear — Service Layer

Gear is an iPad app for managing equipment inventory and scheduling (a more complete description is on a previous blog post).

This post provides a description of the app’s service layer, the mechanism for its modules and view controllers to connect to its data store. Although Gear is a native iOS app, it requires a persistent network connection to load and alter information — the data store exists on a web server using a LAMP stack. This post won’t go into the details of the server side code, it is an overview of the service layer class in the app’s Objective-C code.

EQRWebData

EQRWebData is the app’s service layer class (EQR is the namespace prefix). It is the sole class that makes a network connection to the web server hosting the data store. Here’s what the interface looks like:

It defines a delegate protocol and property:

1
2
3
4
5
6
7
8
@protocol EQRWebDataDelegate <NSObject>
-(void)addAsyncDataItem:(id)currentThing toSelector:(SEL)action;
@end
@interface EQRWebData : NSObject <NSXMLParserDelegate>{
__weak id <EQRWebDataDelegate> delegateDataFeed;
}
@property (nonatomic, weak) id <EQRWebDataDelegate> delegateDataFeed;

And it has three public methods, one of which is the constructor:

1
2
3
4
5
+(EQRWebData *)newWebData;
-(void)query:(NSString *)link parameters:(NSArray *)para class:(NSString *)classString selector:(SEL)action completion:(CompletionBlockWithBool)completeBlock;
-(void)queryForSingle:(NSString *)link parameters:(NSArray *)para completion:(CompletionBlockWithUnknownObject)completeBlock;

The methods query:parameters:class:selector:completion: and queryForSingle:parameters:completion are the two ways that controllers make queries to the data store. query: takes a Selector as an argument and responds by creating a stream of XML data. That data is given to the method’s caller using the method named by the Selector. It’s also passed a completion block so the caller knows when the data stream has ended. Whereas, queryForSingle: is used to get a single object or when the caller is creating or altering an existing model object. queryForSingle: doesn’t create a data stream, instead it responds by returning an object as the parameter to a completion block. Both methods are asynchronous and are usually invoked inside a queue created by Grand Central Dispatch. Here is an example of a query being invoked:

1
2
3
4
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
[webData query:@"getScheduleItemsWithBeginDate" parameters:paramArray class:@"EQRScheduleRequestItem" selector:@"addScheduleItem" completion:^(BOOL success) { ... }];
});

The API endpoints

Any module calling on the service layer will include an endpoint to describe the exchange of data it is requesting. It’s passed as an NSString to the query method as its first argument. Currently I have 90 endpoints (yikes!). Here is a random sampling:

  • alterPhoneInContact
  • alterScheduleEquipJoin
  • alterTransactionDaysForPrice
  • alterTransactionMarkAsPaid
  • deleteScheduleItem
  • getAllContactNames
  • getAllEquipTitleCosts
  • getClassCatalogEquipTitleItemJoins
  • setNewContact
  • setNewMiscJoin
  • setNewScheduleEquipJoin
  • setNewScheduleRequest

Most endpoints require some parameters like an object ID or the property values to be altered or created. The app’s service layer doesn’t need to know about the endpoints. It uses them as the API endpoints when calling on the web server. It’s on the web server that I have 90 different PHP scripts to tackle the business of the queries and send back an XML stream (but I’ll save the server side code for another day).

Parsing the XML data

The web server sends data back as an XML stream. As an NSXMLParserDelegate, EQRWebData implements the methods to form objects from the XML feed and deliver them back to the controllers. NSXMLParser is a Cocoa streaming parser, so it parses in an event-driven manner.

An NSXMLParser notifies its delegate about the items (elements, attributes, CDATA blocks, comments, and so on) that it encounters as it processes an XML document. It does not itself do anything with those parsed items except report them.

As objects are formed from the XML stream, EQRWebData hands them to its delegate, one at a time using the EQRWebDataDelegate method addAsycnDataItem.

1
2
3
4
5
6
7
8
9
10
#pragma mark - webData Delegate methods
-(void)addAsyncDataItem:(id)currentThing toSelector:(SEL)action{
if (![self respondsToSelector:action]){
return;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:action withObject:currentThing];
#pragma clang diagnostic pop
}

performSelector is a Cocoa method that takes advantage of Objective-C’s dynamic binding. addAsyncDataItem is the listener function that disperses objects received from the service layer to the controller’s appropriate methods. When EQRWebData was initially queried, one of the method parameters was a selector which has been returned to the controller and it is now forwarding the data to that method (pragma clang diagnostic is used to suppress Xcode’s caution about an unrecognized selector).

With that, the service layer has done its job and the controller that invoked it has been given the queried data.

Looking Ahead

As I continue expanding on this app, these are the issues that I want to work on for the service layer.

  • Separate out the XML parsing. There is no reason for EQRWebData to both make network connections and parse XML data. I should make a separate class to employ the NSXMLParserDelegate methods.
  • Too many endpoints. Most of them are “get” methods and I only have about a dozen different models. But in the interest of keeping the network activity to a minimum, each model has several different “get” endpoints in order for the caller to specify the granularity of the data needed. Instead I should have one endpoint but with query parameters that allow callers to specify how much of the related data it needs to retrieve. Something I’ve heard described as expressive services.
  • Caching and syncing with CloudKit and Core Data. The dependency on a persistent network connection spells disaster when the network goes down. The app needs to cache its data and work from a local data store both for performance reasons and to cut down on the total network activity. I intend to accomplish this and to entirely replace the need for maintaining a web server LAMP stack by switching to CloudKit and Core Data. It my require a change in the data modeling since MySQL is a relational database and CloudKit uses a NoSQL data store. This approach will also allow me to make use of Apple’s remote notifications to ensure that multiple users stay up to date with each other.
  • Service layer protocol. With CloudKit in mind, it’s worth defining a protocol that declares the properties and methods expected of a service layer class. I started experimenting with CloudKit/Core Data by making a subclass of EQRWebData and overriding the query methods but this is a perfect situation to use a protocol instead of class inheritance.

Gear — Overview and Video Demo

Gear is an iPad app for managing the inventory and scheduling of video and audio gear at the Portland non-profit organization, Northwest Film Center. This post is an overview of the app’s purpose and functionality. The goal in making the app was to reduce the time it takes to schedule and check out gear, do so with fewer scheduling mistakes and keep the process simple and intuitive for the sake of training new staff and interns. The app is in use at Northwest Film Center and since introducing it, the time it takes to reserve and check out gear fell to 25% from what it was before. As an iPad app, it allows users the mobility of handling gear while moving from classroom to classroom as they are using it. The app can run on any number of iPads and all users can see and edit the current data (although it is a native iOS app, it requires a persistent network connection). The iPad also makes it possible to capture the signatures of customers and to scan QR codes attached to equipment.

The image above is the daily itinerary view. It displays the scheduled transactions for the day and enables a user to sort and filter the list, update the status of a reservation or make changes to it. The following image shows a page from monthly tracking sheet: a spreadsheet listing the complete inventory of gear, documenting the daily usage for each item. Colors are consistent between the views, they indicate the type of user: student, teacher, staff, etc.

A demo video of the iPad app Gear in action:

00:00 - 00:46 — Creating a new equipment request.
00:46 - 01:21 — Viewing and editing the gear reservations from the ‘day’ view.
01:21 - 01:39 — Marking gear for the request as prepped from the ‘day’ view.
01:39 - 02:08 — Viewing and editing the request from the ‘month’ view.
02:08 - 02:31 — Emailing a reservation confirmation to a customer from the ‘inbox’ view.
02:31 - 03:14 — Marking gear as checked out and capturing a customer signature from the ‘day’ view.
03:14 - 03:36 — Filtering the list of requests from the ‘day’ view.
Note that there is no sound on the video

Future posts will describe the design patterns and coding approaches I am using in the app. Gear was made to suit the needs of a specific organization. An updated version meant for wide release in the App Store is in progress.

Aggregation in MongoDB and Mongoose

For a recent project, I worked with my classmates from Code Fellows to create a web app to display movie box office analytics. This post demonstrates what I learned when I wanted to use aggregated data from MongoDB to display average box office grosses on a chart.

Our query to the server will (or will not) include some key/value pairs for filtering the result and the data returned will show the average income per movie screening over a six month period. The result will also have data for an alternate view that provides an overall summary of the query with averages and totals for admissions and attendance.

Although our source data came from MongoDB, a NoSQL database, we modeled our data similar to how we would with a relational database. Our Screenings collection contained instances of individual movie screenings, recording the day and time of the screening, attendance and admissions totals, and then a reference to an entry in the Movie collection that had details about the movie like title and genre.

Aggregation was introduced in Mongo version 2.2. Aggregate operations form a data processing pipeline. The documentation description reads as follows:

Aggregation operations group values from multiple documents together, and can perform a variety of operations on the grouped data to return a single result.

Mongoose’s implementation is basically a wrapper around the MongoDB methods, forming JavaScript methods. It acts as a pipeline by creating a promise which can in turn be acted on by any number of aggregation operations.

Let’s begin in the Screenings model. The following is a a static method on the Screenings collection Schema. Rather than writing out the entire function, I’ll break it up into chunks with some commentary.

1
screeningSchema.statics.getAggregateData = function aggMatchingCompany(title, genre) { ... }

The following five code blocks are the body of the above function.

I begin the aggregation with the project method to define the fields I want from the Screenings collection. Here I’m including all the fields in the Screenings schema, and even adding a few computed values that I’ll need. The movie field is a reference to a record in another collection. Project returns all of the records in the collection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const aggregateResult = this.aggregate([
{
$project:
{
movie: true,
attendanceTotal: true,
admissionsTotal: true,
concessionsTotal: true,
dateTime: { $subtract: ['$dateTime', 1000 * 60 * 60 * 7] },
seats: true,
format: true,
dayOfWeek: { $dayOfWeek: { $subtract: ['$dateTime', 1000 * 60 * 60 * 7] } },
hourOfDay: { $hour: { $subtract: ['$dateTime', 1000 * 60 * 60 * 7] } },
month: { $month: '$dateTime' },
},
},
]);

Next, I need to the title and genre information that is stored on the related entry in the Movies collection. Lookup is a method that effectively performs an outer left join between two collections (in RDBS terms). I’m making the connection between the movie field in my current data to the _id field in the Movies collection and the data is returned as a property on Screenings object titled movie_data.

1
2
3
4
5
6
7
aggregateResult.lookup({
from: 'movies',
localField: 'movie',
foreignField: '_id',
as: 'movie_data',
});
aggregateResult.unwind('$movie_data');

The result of lookup is an array. Unwind is called immediately afterwards to extract the object from the array. With a one-to-one relationship between the instance of a screening to a related Movie entry, unwind is just that simple. If it were a one-to-many relationship, we’d have more data processing options in the unwind.

Next, I’m testing to see if any filter queries were passed into the aggregation pipeline and I’m acting on those using match to constrain the set of all screenings to just the filtered set of screenings.

1
2
3
4
5
6
7
if (title) {
aggregateResult.match({ 'movie_data.title': title });
}
if (genre) {
aggregateResult.match({ 'movie_data.genres': { $in: [genre] } });
}

Grouping will pivot the data into the time sequence I need for a chart. Instead of returning all the raw screening data, this method will use the month field as the basis for grouping the data and returning a sum on admissions and attendance and also an average for those fields. With count, I’m also providing a count of the number of Screening records that were used to form the aggregated data.

1
2
3
4
5
6
7
8
aggregateResult.group({
_id: '$month',
count: { $sum: 1 },
admissions: { $sum: '$admissionsTotal' },
attendance: { $sum: '$attendanceTotal' },
avgAdm: { $avg: '$admissionsTotal' },
avgAtt: { $avg: '$attendanceTotal' },
});

Since this is just a plain old JavaScript promise, we can do any additional work with the data to shape it into the result we want. Here, I’m totaling and averaging the aggregated data to provide an optional grand summary object. And after that, a function to fill in any missing months and then sorting the months in sequential order.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
return aggregateResult.then(data => {
// Add summary totals
const totals = data.reduce((previous, current) => {
previous.admissions += current.admissions;
previous.attendance += current.attendance;
previous.count += current.count;
return previous;
}, { admissions: 0, attendance: 0, count: 0 });
totals.avgAdm = (totals.admissions / totals.count) || 0;
totals.avgAtt = (totals.attendance / totals.count) || 0;
// Polyfill any missing months
for (let i = 1; i < 8; i++) {
const index = data.findIndex((e) => e._id === i);
if (index === -1) {
data.push({
_id: i,
count: 0,
admissions: 0,
attendance: 0,
avgAdm: 0,
avgAtt: 0,
});
}
}
// Sort in ascending order
data.sort((a, b) => a._id > b._id);
return {
sequence: data,
totals,
};
});

The promise will resolve into an object with two properties: sequence and totals. The following is the server side route (using Express) that invokes the aggregation function.

1
2
3
4
5
.get('/aggregate', (req, res, next) => {
const { title, genre } = req.query;
Screening.getAggregateData(title, genre)
.then(data => res.json(data));
})

This is what it looks like from a client side service.

1
2
3
4
5
6
7
aggregate(params) {
return $http
.get(`${apiUrl}/screenings/aggregate`, params)
.then(r => {
return r.data;
});
},

The sequence object in the final data is passed into an instance of ChartJS as a dataset. ChartJS can even overlay datasets to easily compare multiple queries as lines or bar charts.

A demo of the finished project with mock data is online at ahbo.firebaseapp.com.
Here’s the project on GitHub.
MongoDB has some good documentation for its aggregation operations.
And here is the documentation for Mongoose.

Game Piece Collection View

Here is an example of UICollectionViews used as armies on a board game.

Each grouping of armies is a collection view – here are two collection views:

Generally collection views contain unique objects defined by some meaningful text. In this case, the data source method collectionView:cellForItemAtIndexPath: simply adds a UIView (SPYArmyView) to the cell’s content view and gives it a color.

The action happens in the custom view layout – SPYBrigadeViewLayout.

For each item returned from the data source, a single army unit is added to the stack. The data source only needs to know the total quantity of armies and the color.

The layout uses these private properties:

View Layout Properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@property int maxColumnStack;
@property int minColumnStack;
@property int unitPieceHeight;
@property int unitPieceWidth;
@property int extraHeight;
@property int extraWidth;
@property float halfUnitPieceHeight;
@property float halfUnitPieceWidth;
@property float columnHeightOffset;
@property float aisleWidthOffset;
@property int maxColumnCount;
@property int zIndexDirection;
@property float transformScaleFactor;

In this case, maxColumnStack = 4 and maxColumnCount = 3.

Here is a GitHub Gist link to the UICollectionViewController file, another for the UICollectionViewLayout, and another for the UIView that represents the content of an individual cell.