MacRuby and Core Data Tutorial

Posted on 19 October 2008 by Johannes Fahrenkrug. Tags: Programming Mac Tutorials Ruby Cocoa
Update (10/29/08): I did end up creating an Xcode Template for MacRuby Core Data applications and it has been added to the MacRuby trunk. Laurent Sansonetti has improved it and added some new MacRuby 0.4 features to it, like the use of the Pointer class for NSError** arguments. Just follow these instructions on how to build and install the MacRuby trunk (it's super easy) and you'll be building MacRuby Cocoa Data applications with a spiffy Xcode template in no time!
I love both Ruby and Objective-C, so MacRuby naturally deserves the adjective "flippin' sweet!" I wanted to get up to speed with CoreData for a current project and wanted to do so with MacRuby, so I could take advantage of some or Ruby's string processing power and so on. RubyCocoa has Core Data XCode templates, but MacRuby does not. So I made one (this is a lie, read on). First I'll tell you how stupid-easy it was: I just had to take the ObjC-AppDelegate.m code from the Apple "Core Data Application" template and "translate" the ObjC-code to Ruby (which basically consists of removing square brackets and curly braces). The new AppDelegate.rb file sets up all the Core Data stuff (PersistentStoreCoordinator, ManagedObjectModel, ManagedObjectContext) just like its ObjC-cousin. So how do you create a Core Data application with MacRuby?
  1. Create a new MacRuby Application
  2. Create a new empty file and call it AppDelegate.rb
  3. Paste the following code into that file:
    #
    #  AppDelegate.rb
    #  MyGreatApp
    #
    #  Created by Johannes Fahrenkrug on 10/17/08.
    #  Copyright __MyCompanyName__ 2008 . All rights reserved.
    #
    
    
    class AppDelegate
    attr_writer :window
    
    #    Returns the support folder for the application, used to store the Core Data
    #    store file.  This code uses a folder named "MyGreatApp" for
    #    the content, either in the NSApplicationSupportDirectory location or (if the
    #    former cannot be found), the system's temporary directory.
    def applicationSupportFolder
    paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true)
    basePath = (paths.count > 0) ? paths[0] : NSTemporaryDirectory()
    return basePath.stringByAppendingPathComponent("MacObjTalks")
    end
    
    
    #
    #    Creates and returns the managed object model for the application
    #    by merging all of the models found in the application bundle.
    #
    def managedObjectModel
    if @managedObjectModel
     return @managedObjectModel
    end
    
    @managedObjectModel = NSManagedObjectModel.mergedModelFromBundles(nil)
    return @managedObjectModel
    end
    
    
    #
    #    Returns the persistent store coordinator for the application.  This
    #    implementation will create and return a coordinator, having added the
    #    store for the application to it.  (The folder for the store is created,
    #    if necessary.)
    #
    def persistentStoreCoordinator
    if @persistentStoreCoordinator
     return @persistentStoreCoordinator;
    end
    
    error = nil
    
    fileManager = NSFileManager.defaultManager
    applicationSupportFolder = self.applicationSupportFolder
    
    if !fileManager.fileExistsAtPath(applicationSupportFolder, isDirectory:nil)
     fileManager.createDirectoryAtPath(applicationSupportFolder, attributes:nil)
    end
    
    url = NSURL.fileURLWithPath(applicationSupportFolder.stringByAppendingPathComponent("MyData.xml"))
    @persistentStoreCoordinator = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(self.managedObjectModel)
    if !@persistentStoreCoordinator.addPersistentStoreWithType(NSXMLStoreType, configuration:nil, URL:url, options:nil, error:error)
     NSApplication.sharedApplication.presentError(error)
    end
    
    return @persistentStoreCoordinator
    end
    
    
    #
    #    Returns the managed object context for the application (which is already
    #    bound to the persistent store coordinator for the application.)
    #
    def managedObjectContext
    return @managedObjectContext if @managedObjectContext
    
    coordinator = self.persistentStoreCoordinator
    if coordinator
     @managedObjectContext = NSManagedObjectContext.alloc.init
     @managedObjectContext.setPersistentStoreCoordinator(coordinator)
    end
    
    return @managedObjectContext
    end
    
    
    #
    #    Returns the NSUndoManager for the application.  In this case, the manager
    #    returned is that of the managed object context for the application.
    #
    def windowWillReturnUndoManager(window)
    return self.managedObjectContext.undoManager
    end
    
    
    #
    #    Performs the save action for the application, which is to send the save:
    #    message to the application's managed object context.  Any encountered errors
    #    are presented to the user.
    #
    def saveAction(sender)
    error = nil;
    if !self.managedObjectContext.save(error)
     NSApplication.sharedApplication.presentError(error)
    end
    end
    
    
    #
    #    Implementation of the applicationShouldTerminate: method, used here to
    #    handle the saving of changes in the application managed object context
    #    before the application terminates.
    #
    def applicationShouldTerminate(sender)
    error = nil
    reply = NSTerminateNow
    
    if self.managedObjectContext
     if (self.managedObjectContext.commitEditing)
      if (self.managedObjectContext.hasChanges and !self.managedObjectContext.save(error))
       # This error handling simply presents error information in a panel with an
       # "Ok" button, which does not include any attempt at error recovery (meaning,
       # attempting to fix the error.)  As a result, this implementation will
       # present the information to the user and then follow up with a panel asking
       # if the user wishes to "Quit Anyway", without saving the changes.
    
       # Typically, this process should be altered to include application-specific
       # recovery steps.
    
       errorResult = NSApplication.sharedApplication.presentError(error)
     
       if errorResult
        reply = NSTerminateCancel
       else
        alertReturn = NSRunAlertPanel(nil, "Could not save changes while quitting. Quit anyway?" , "Quit anyway", "Cancel", nil)
        if (alertReturn == NSAlertAlternateReturn)
         reply = NSTerminateCancel
        end
       end
      end
     else
      reply = NSTerminateCancel
     end
    end
    
    return reply
    end
    
    end
    
  4. Save it.
  5. Open MainMenu.nib
  6. Drag a new NSObject to the MainMenu.nib window:
  7. Open the inspector for the new object and open the next-to-last tab. Select "AppDelegate" from the Class popup list.
  8. Control-drag from the File's Owner to the App Delegate and connect it's delegate outlet.
  9. Control-drag from the App Delegate to the Window to connect it's window outlet.
  10. Save the changes to the nib.
  11. Add a Core Data data model (give it a name of your choice and just click Finish in the 3rd step of the wizard).
  12. Double-click on the new data model file to open it with the data model editor.
  13. Add an entity called "Skill" with a string attribute called "name" and an integer 16 attribute called "level".
  14. Save the data model.
  15. Back in Interface Builder, drag a Core Data Entity to the Window.
  16. In the wizard that comes up, select your Xcode Project, your data model and the "Skill" entity.
  17. In the next step, select the Master/Detail view and check all the options.
  18. In the next step, select both attributes and click "Finish".
  19. Your window should look like this:
  20. Disconnect the saveDocument action from the Save menu item by clicking the X on the left of "First Responder".
  21. Control-drag from the Save menu item to the App Delegate and connect it to the saveAction.
  22. Save the NIB.
  23. Click Build & Go.
  24. You should be able to add, remove, edit, and search for skills.
  25. Click File -> Save, close the app and restart it: your data should still be there.
  26. Commence frolicking and rejoicing.
Congratulations to your first MacRuby Core Data application. If there are ways to improve this (and I'm sure there are) kindly punch me in the nose by criticizing me in the comments. I'm probably just as new to MacRuby and Core Data as you are, so please ask in-depth questions about these vast topics elsewhere. Don't miss this great MacRuby tutorial on ADC. And read the Core Data Programming Guide. Ok, I mentioned an XCode template for this in the beginning. Well, I haven't really made one yet, but I will. Soon. I hope this was kinda useful to you anyhow.


Comments

Johannes Fahrenkrug said...

Not any that I know of, unfortunately. It should be easy enough to put something like that together in MacRuby, though.

July 31, 2009 06:20 AM

Anonymous said...

Is there a rails fixture-esque feature for CoreData?

July 31, 2009 05:28 AM

Johannes Fahrenkrug said...

Hi Anonymous (if that really IS your real name),

thank you for your friendly comment.
You are absolutely correct. I did this rather quickly, I basically went through the ObjC code line by line and "translated" it to Ruby. I neither optimized nor beautified it. But writing it the way you pointed out would have been much prettier and ruby-ish. Guilty as charged.
Btw, it should be "Very bad ruby style!!!", putting a whitespace before a punctuation mark is very bad writing style. ;-)

- Johannes

February 13, 2009 07:56 AM

Anonymous said...

very bad ruby style !!!

def managedObjectModel
if @managedObjectModel
return @managedObjectModel
end

@managedObjectModel = NSManagedObjectModel.mergedModelFromBundles(nil)
return @managedObjectModel
end

it's

def managedObjectModel
@managedObjectModel ||= NSManagedObjectModel.mergedModelFromBundles(nil)
end

February 13, 2009 07:48 AM

Johannes Fahrenkrug said...

Hey jamesu,

check out my update note on the top of the article!

October 29, 2008 08:25 PM

Johannes Fahrenkrug said...

thank you for your comment, James! I know, the error = nil solution is less then optimal. I saw that they also do that in the RubyCocoa core data template, but that's no excuse. Not the value, but a reference has to be passed as the argument. If anyone has time to find out how to do that in macruby before me, please, by all means, leave a comment!

October 24, 2008 06:49 AM

jamesu said...

Neat, was looking how to do this the other week.

Had an issue where i couldn't figure out what the "error" parameter should be for addPersistentStoreWithType. Looks like you have worked around this by using "nil"... interesting.

(I ended up just implementing this bit in Objective C. Yikes.)

October 23, 2008 11:03 AM

Comments

Please keep it clean, everybody. Comments with profanity will be deleted.

blog comments powered by Disqus