Greetings,
I am not an expert Objective C, Cocoa, or Core Data coder. There, I admitted it. I expect some of the people viewing this will be, and I encourage you to let me know where I’ve gotten things wrong. I’m also sure my MacRuby style is…let’s call it idiosyntactic.

Too long to fit in the margins of this book

My journey starts with Matt Aimonetti’s excellent MacRuby: The Definitive Guide, and it’s Core Data chapter. It’s a really good introduction to Core Data, but I needed something a bit deeper and more advanced. My dream, of course, would be to use Core Data with Active Record’s ease of use. Core Data is really complex, though. Books have been written on the subject; and yet my complete exposure to it has been writing a few Objective C iPhone applications for my own use.

My problem

It’s always best to start with a relatively simple action, and figure out how to build on that. My initial problem to solve was allowing the user to select a Category from an outline view on the left (iTunes style) and pull up a list of the items in that Category in a table view in the main part of the UI, like so:

MacBidwatcher screenshot with an outline view on the left and a table on the right.

I’m sure I could do this a number of ways, and it’s probably even possible to make it work with the standard NSArrayController like in MacRuby: The Definitive Guide, but there are a number of places where it won’t work as well, such as when I want to find the item ending the soonest from all Categories, or load and modify specific Auction items during updates or when the user chooses to perform actions on individual items. At that point, it’ll become necessary to do the Core Data handling ‘by hand’, and so I felt it’d be best not to start with something I was going to have to bypass anyway.

The Data Model

It may be helpful to sketch a simple data model, described using the data modeler in Xcode; some other models in the project have been elided for clarity:

MacBidwatcher's Data Model for Categories and Auctions

Because of the reciprocal relationship between Category and Auction, I could simply load a Category with a particular name, and then refer to category.auctions to get the list (in Core Data form) of Auction objects associated with that category. Adding .allObjects returns the actual objects as an array for easy referencing in a TableView. (This is not efficient, but I’m trying to get the code working right now; I’ll manage efficiency once I’ve gotten it in users hands.)

MacRuby vs. Objective C

The Objective C (that I’ve written) to do a similar load takes about 80 lines of code; I’m not embedding it because it’s too long, and it only loads by name. You can go take a look; it’s pretty straightforward. Unlike the Objective C version, the Ruby code can load by any attribute dynamically, and has additional features like limits and offsets.

This is the core of my code when the user clicks a category/folder on the left:

The key code being referenced is this:
@current_category = Category.find_first(context, by_name:category_name)
It’s roughly equivalent to the Objective C code (extracted from the app that uses my ObjC code above):
NSArray *cats = [Category findByName:category fromContext:managedObjectContext];
The main difference is that the Objective C code requires a different method for each findBy* that you want to do, whereas the MacRuby code uses the fact that its parameters are really just hash entries using the ‘:’ syntax for faux named parameters. So, for example, while this code is doing a find_first, if it were to use find_all it could also pass ..., limit:20, offset:7) if you wanted to get 20 items starting at the 7th.

The Magic

The magic, of course, happens in entity.rb which looks like this:

It references inheritable_attrs.rb which is available as a gist of its own

One important part that any entity needs to get is a ‘context’, which is actually a managed object context. Fortunately when you create a Mac Ruby project using the MacRuby Core Data Application template, a context is available from the AppDelegate class. You may notice that in my AuctionTableDatasource there is: attr_writer app_delegate which doesn’t really do anything obvious. That’s an ‘outlet’ to refer to the AppDelegate; using Interface Builder control-click-and-drag from your data source to the App Delegate, and choose the app_delegate outlet to link it up. The default Core Data AppDelegate class instantiates a Managed Object Context which is essentially a link to your Core Data database. It needs to be passed in to code which is going to interact with data from the database, which is why each of the public methods in entity.rb takes a context as their first parameter.

A MacRuby Caveat

I mildly disagree with the default MacRuby Core Data Application template which creates an XML-based Core Data app; specifically it creates a file with the extension of .xml and passes NSXMLStoreType as the persistent store type. I believe it should pass a .sqlite file extension and use NSSQLiteStoreType as the storage type. For small applications, it may not matter, but if you feel you’re likely to be storing enough data that handling Core Data by hand will be necessary, then you’re going to want the SQL-based storage type. The typical recommendation is to start with an XML type and switch to SQL when you’re going to release, nominally because XML is easier to read. I dispute that, though, because there are slight behavioral differences, such as the XML storage type keeping the entire object graph in memory and reports of subtly different handling of (generally incorrectly specified) relations. If you want to use an SQLLite backed database, you’ll want to fix up AppDelegate.rb once you’ve created your project.

Conventions and Features

I know there’s a lot of disagreement over Active Record’s design, but I happen to like it a lot, and many of the guidelines of Rails creep into my code. I especially like the presumption that there’s an easy way to do whatever you’re doing, that the code handles for you automatically, but if you want to fight the convention, you can; it just won’t be as clean. There’s a lot of ‘convention’ involved in the Entity class, especially in the presumptions that you’ll have model classes which are named identically (including case!) to the core data models you’ve created, i.e. generally with the first letter uppercased. You should be able to override that by setting the entity_name in your subclasses using

require 'entity'
Class OddlyNamedModel < Entity
  self.entity_name = 'oddly_named_model'
.
.
.

but it’s not something you should do lightly, or at all if you’re creating a new project, and embarrassingly, I haven’t tested it.

Attributes

Attribute names were something else to deal with; I typically name my attributes in lower case while, as mentioned, my entities have their first letter capitalized. So my Category.find_first(context, by_name:category_name) method will look for an attribute called ‘name‘. I am aware that some people capitalize the first letter of their attributes, so: Category.find_first(context, byName:category_name) would work if you have an attribute called ‘Name‘ and looks very Objective C-ish.

Limits and Offsets

It’s straightforward to index into your results, using Category.find_all(context, by_name:common_name, offset:20, limit:10); this would search for all instances that have common_name in their name attribute, and then starting from the 20th, pull down 10 of them. In SQLLite terms, this translates into something approximately like: SELECT * FROM ZCATEGORY WHERE NAME = ? LIMIT 10 OFFSET 20

Conditions (the escape hatch)

Another useful feature is a conditions key, which provides one entry which is raw predicate logic, essentially. An example for finding all categories that end in ‘nt’ would be: Category.find_all(context, conditions:['name like %@', 'nt']) which translates in SQLLite terms to: SELECT * FROM ZCATEGORY WHERE NSCoreDataLike( ZNAME, ?, 0)

Debugging

One incredibly useful built-in capability (and the source of the above approximations of the commands issued by Core Data) I’ve found for doing debugging of SQLLite-based Core Data code is to pass -com.apple.CoreData.SQLDebug 1 to the application from the command line, in order to view the queries as they’re being done. From the command line, in the root of your app directory, this would look something like this

bash$ build/Release/MyApp.app/Contents/MacOS/MyApp -com.apple.CoreData.SQLDebug 1

Conclusion

I’ve tried to make an extremely simple class that I can subclass for my entities, that allows me to do straightforward find operations in a way that doesn’t feel overly complex, and fits in a Ruby-ish style. It’s working so far for my code, but I can’t promise it’ll work for everybody’s. Especially if you’re facing down historical schemas, or other issues, my defaults are probably not going to be appropriate for you. It’s definitely less code than the Objective C equivalent, and feels…more comfortable for me.

I’m VERY interested in feedback, and improving this code. As I stated at the outset, the best I could be considered is a hobbyist Objective C coder, and I’m only just starting to dig into the power that MacRuby has. Please feel free to provide pointers, comments, ideas, features, or (if necessary) derision. Especially feel free to fork entity.rb and make corrections or additions as you see fit.

– Morgan Schweers, CyberFOX!

Update: A friend and quite excellent Objective C developer David Brown provided his own take on the Entity concept; according to him it’s untested and needs a few tweaks, but the basic concept should work equally well; check it out on his own gist of entity.m. It’s like a Rosetta stone between MacRuby and Objective C!


4 Comments »

  1. On Terry Says:

    Morgan

    Great blog! It expresses a feeling of rubyism(ness) that that macruby needs to embrace. I often get disheartened thinking that macruby is just an objective c wrapper/bridge( like java) but your effort shows that the ruby view is going to make a huge difference.

    Not only fun but practical…. perhaps I’m just slightly ruby biased :)

    Terry


  2. On Saul Says:

    Hi, nice work. I have used Core Data for nearly all my Cocoa apps, and I have a little framework that may be helpful to you. I turns fetching into something more akin to the Rails Active Record model. It’s written in ObjC, but I’ve had success using it for my MacRuby tests. Take a look and I hope it’s as helpful for you as it has been for me: http://github.com/magicalpanda/activerecord-fetching-for-core-data


  3. On George Janczuk Says:

    Hi Morgan,

    I’m a long-time software developer, and my wife is a non-IT person but still fairly computer literate.

    I put her on to JBidWatcher years ago; it just does what it should without fanfare – the way I like my software.

    Anyway… I can see a screenshot for MacBidWatcher on this artice? Are you moving away from Java to Ruby and/or Objective C for MacBidwatcher and/or ?BidWatcher in general? Is this possibly because of Apple lowering the status of the JAVA environment within the MacOS eco-system?

    Will Windows (&/or other platforms) be supported with your new MacBidWatcher project?

    Anyway… Just curious!

    George.


  4. On Cyberfox Says:

    Greetings,
    @George – I’m experimenting with building a Mac-specific version of JBidwatcher, specifically for the Mac OS X App Store. I prefer Ruby as a programming language, so I’ve been exploring how well MacRuby can work as a desktop application development language. I figure the first major app I build, I should have some solid domain knowledge in first. :)

    Multiplatform JBidwatcher is not going away. I will continue to develop it for Mac OS X, Linux and Windows, although once I release MacBidwatcher I’ll have to think about the work that goes into the Mac OS X version.

    There’s also a real possibility that as time goes on I’ll move more of JBidwatcher’s code into Ruby, using JRuby (Ruby under Java) which would let me share the important code base between all the programs.

    It’s less about Apple lowering Java’s profile, than about my own love for Ruby as a programming language. :) Among other things, Ruby has a lot of capabilities ‘built in’ that I’ve had to agonizingly hand-craft over the eleven years I’ve worked on JBidwatcher.

    I’m trying to constantly evolve JBidwatcher, to keep it interesting to me, and useful to as many people as possible. I’ve spent time exploring iPhone apps, web apps, and now a Mac-specific desktop app. Someday hopefully most of those will come to fruition. Or eBay will ban me from their service. :)

    Best of luck with your auctions!

    – Morgan Schweers, CyberFOX!


Leave a Reply