The NSCompoundPredicate weightloss diet for controllers
8th March 2013
Anyone who does iOS or Mac development knows the importance of the Model-View-Controller (MVC) pattern within the Cocoa and Cocoa Touch frameworks. Put simply, the aim of MVC is to separate your data (the model) from the presentation of the data (the view). This is achieved through the use of an intermediate class (the controller) that is responsible for collecting your data from the model, and passing it to an appropriate view.
In theory this is a great system – your model data is completely self-contained and your view does not need intimate knowledge of the model in order to display it because the controller handles the fetching of the data. Likewise your data does not need to be concerned with how it will be presented – the controller is responsible for taking model data and inserting it into the right places in the view, or views. In practice this separation of concerns has a tendency to break down as the lines between model, view and controller get blurred.
Removing Model Logic from Controllers
While there are a number of places that the theory goes awry, one of the most common is in the link between the model and the controller. The relationship usually starts out fine – the model is simple and the controller can make relatively simple calls to get the model data it needs. With time and increasing model complexity the controller will find itself having to make more carefully considered calls to retrieve specific data objects, and possibly perform filtering on those objects before handing them off to the view.
The action of filtering the model data is what we’re focussed on here as we very often use
UITableViews in iOS to present a list of items before tapping to view the details of a specific item. In terms of MVC the controller (often a
UITableViewController) can ask the model for a list of the items, which it can then provide to the view (the
To narrow the focus of the list to items that match specific criteria the controller can either ask that the model only provide a filtered list or it can filter a complete list that is handed to it by the model. We are going to focus on the latter approach in this post, but both approaches are valid and which is used depends on factors that are beyond the scope of this post.
An efficient way of filtering data is to use the
-filteredArrayUsingPredicate collection methods. Allowing the controller to perform its own filtering of the list can introduce a strong coupling between the controller and the model. Creating a predicate requires intimate knowledge of the model object’s properties and how they are interpreted.
Say, for example, your application contained an array of
Users that your controller needed to filter in order to produce a list of active users. Right now an active user is defined as one that has logged into your service in the last 2 weeks. To filter the array of users a controller would need to create a predicate such as the following:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"lastLogin > %@", dateTwoWeeksAgo]; NSArray *activeUsers = [users filteredArrayUsingPredicate:predicate];
The drawback here should be immediately obvious – if the criteria for an active
Userchanges then any controllers that rely on this criteria will need to be updated.
A more manageable alternative is to encapsulate the process for determining an active user into the
User model itself. The predicate can be constructed internally and returned to the controller as follows:
NSPredicate *activeUserPredicate = [User activeUserPredicate]; NSArray *activeUsers = [users filterArrayUsingPredicate:activeUsersPredicate];
We’ve established that keeping the predicate logic in the model class is a good way to reduce the dependency between the controller and the model, but what happens when the model changes and the criteria for a predicate becomes more complex? If you followed the previous advice your model can only return a single predicate so one option is adjust the construction of the predicate to accommodate the extra conditional logic. In our
User example we want to state that an active user has logged in during the last 2 weeks and has made some sort of minimum contribution:
NSPredicate *complexPredicate = [NSPredicate predicateWithFormat: @"lastLogin > %@ AND postCount > %@", dateTwoWeeksAgo, minPostCount];
This can be returned to the controller for determining active users as before, but has become less reusable. If we want to reuse the last login check or the post count checks elsewhere it will require duplicate part of this predicate. Worse still, if we change the last login or post count criteria it may require updating multiple predicates.
Fortunately we can obviate this need by utilising the
NSCompoundPredicate class. This handy class allows us to construct complex predicates by combining simpler predicates with logical conditions such as AND, OR and NOT. In our case we can use an AND condition as follows:
NSPredicate *lastLoginPredicate = [NSPredicate predicateWithFormat:@"lastLogin > %@", dateTwoWeeksAgo]; NSPredicate *postCountPredicate = [NSPredicate predicateWithFormat:@"postCount > %@", minPostCount]; NSPredicate *activeUsersPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[lastLoginPredicate, postCountPredicate]];
postCountPredicate can now be created as class methods and be re-used throughout the model safe in the knowledge that they can be easily maintained. And because
NSCompoundPredicate is a subclass of
NSPredicate it can be safely returned for use anywhere that an
NSPredicate is expected, allowing simple conditions to be increased in complexity without your calling code needing updated to handle it.