The example from the Apple Developer Tools often show interesting techniques that can be successfully used in real projects. One of those is the MultipleNibTabView example (available under /Developer/Examples/InterfaceBuilder/MultipleNibTabView) , showing how you how to build user interfaces in a more modular way. This is the official project description (ReadMe.rtf):

This example application demonstrates how to use views housed in separate nibs. The main window has an NSTabView and an NSTextField. The window controller creates a SubViewController which loads in a nib and returns the nib’s view. The window controller inserts those views into each NSTabViewItem. The SubViewController sends a message to the Window Controller, requesting a pointer to the NSTextField. The widgets in each view applies gets that pointer by sending a message to the SubViewController and then applies changes to the text and color of the NSTextField.

Here it goes its RubyCocoa implementation. Simply create a new RubyCocoa application in XCode, copy over the English.lproj folder and add the source files defined below.

#
#  WindowController.rb
#

require 'osx/cocoa'

class WindowController < OSX::NSWindowController
  
  ib_outlet :displayTextHere
  ib_outlet :multipleNibTabView
  
  attr_accessor :enterTextNibView
  attr_accessor :setColorNibView

  def init
    @enterTextNibView = nil
    @setColorNibView = nil
    super_init
  end

  def awakeFromNib
# Ensure that the first tab is selected.
    @multipleNibTabView.selectTabViewItemWithIdentifier('1')
  end

  def viewFromNibWithName(nibName)
# Creates an instance of SubViewController which loads the specified nib.
    subViewController = SubViewController.alloc.initWithNibName_andOwner(nibName, self)
    subViewController.view
  end

  def tabView_didSelectTabViewItem(tabView, tabViewItem)
    nibName = nil
# The NSTabView will manage the views being displayed but without the NSTabView, you need to use removeSubview: which releases the view and you need to retain it if you want to use it again later.
    
# Based on the tab selected, we load the appropriate nib and set the tabViewItem's view to the view fromt he nib
    if (tabViewItem.identifier.isEqualToString('1'))
      if !(@enterTextNibView)
        @enterTextNibView = self.viewFromNibWithName('EnterText')
      end
      if (@enterTextNibView)
        tabViewItem.setView(@enterTextNibView)
      end
    end

    if (tabViewItem.identifier.isEqualToString('2'))
      if !(@setColorNibView)
        @setColorNibView = self.viewFromNibWithName('SetColor')
      end
      if (@setColorNibView)
        tabViewItem.setView(@setColorNibView)
      end
    end

  end

  def textField
# This method returns a pointer to the NSTextField on the main window.
    @displayTextHere
  end

end

#
#  SubviewController.rb
#

require 'osx/cocoa'

class SubViewController < OSX::NSObject
  
  ib_outlet :view
  attr_accessor :owner

  def init
    super_init
  end

  def initWithNibName_andOwner(nibName, owner)
    @owner = owner
    OSX::NSBundle.loadNibNamed_owner(nibName, self)
    init
  end

  def view
    OSX::NSLog('Getting View %@\n', description)
    @view
  end

  def ownerTextField
    @owner.textField
  end

end

#
#  TextFormattinView.rb
#

require 'osx/cocoa'

class TextFormattingView < OSX::NSView
  
  ib_outlet :backgroundColorWell
  ib_outlet :owner
  ib_outlet :textColorWell

# This method is called when the nib is finished loading.  It makes sure the color wells are set to the colors of the text color and the background color of the NSTextField from the main window.
  def awakeFromNib
    @textColorWell.setColor(@owner.ownerTextField.textColor)
    @backgroundColorWell.setColor(@owner.ownerTextField.backgroundColor)
  end

# This method asks the SubView Controller for a pointer to the NSTextField on the main window and then sets its background color.
  def setBackgroundColor(sender)
    @owner.ownerTextField.setBackgroundColor(sender.color)
  end

# This method asks the SubView Controller for a pointer to the NSTextField on the main window and then sets its text color.
  def setTextColor(sender)
    @owner.ownerTextField.setTextColor(sender.color)
  end

end

#
#  TypeTextHere.rb
#

require 'osx/cocoa'

class TypeTextHere < OSX::NSView
  
  ib_outlet :owner
  ib_outlet :text

  def sendText(sender)
    @owner.ownerTextField.setStringValue(sender.stringValue)
  end

end

As you all know, simple Core Data applications can be built without much coding thanks to XCode Data Modeler and Interface Builder and, for basic things, you seldom need to deal with Core Data API itself. Nevertheless, a Core Data API does exist that allows you to programmatically define your data model, and in some occasions, it could be appropriate to define a data model programmatically.

A basic introduction to the Core Data API is given in the “Low-Level Core Data Tutorial” from Apple, which guides you through the steps necessary to build a simple command line app that stores a log of all its executions and displays then the log content.

The Low-level Core Data tutorial was partially ported some time ago to RubyCocoa by Ernest Prabhakar. In particular, how to execute a fetch request was not covered, so I decided to take Ernest’s sample a little bit further by implementing that part, left out in the first place. As I was at it, I also polished a little bit the original code, taking advantage of various RubyCocoa enhancements since version 0.4.2, current at the time the original post was written.

Core Data Command Line Client


# Port to Ruby/RubyCocoa of the Low-level CoreData Tutorial
# from Apple.
# Apple Inc. © 2005, 2006 Apple Computer, Inc.
#
# The RUbyCocoa version is released under the MIT License

require 'osx/cocoa'
OSX.require_framework 'CoreData'

module CoreDataCLI

  #-- constants
  STORE_TYPE = OSX::NSXMLStoreType
  STORE_FILENAME = "CDCLI.xml"

  #-- module variables
  @@mom = nil
  @@moc = nil

  #-- functions
  def self.managedObjectModel

    if @@mom
      return @@mom
    end

    @@mom = OSX::NSManagedObjectModel.alloc.init
    runEntity = OSX::NSEntityDescription.alloc.init
    runEntity.setName('Run')
    runEntity.setManagedObjectClassName('Run')
    @@mom.setEntities(OSX::NSArray.arrayWithObject(runEntity))

    #-- date attribute
    dateAttribute = OSX::NSAttributeDescription.alloc.init
    dateAttribute.setName('date')
    dateAttribute.setAttributeType(OSX::NSDateAttributeType)
    dateAttribute.setOptional(false)

    #-- processID attribute
    idAttribute = OSX::NSAttributeDescription.alloc.init
    idAttribute.setName('processID')
    idAttribute.setAttributeType(OSX::NSInteger32AttributeType)
    idAttribute.setOptional(false)
    idAttribute.setDefaultValue(OSX::NSNumber.numberWithInt(-1))

    #-- Validation Predicate and Warning
    lhs = OSX::NSExpression.expressionForEvaluatedObject
    rhs = OSX::NSExpression.expressionForConstantValue(OSX::NSNumber.numberWithInt(0))
    validationPredicate = OSX::NSComparisonPredicate.objc_send(
          :predicateWithLeftExpression, lhs,
          :rightExpression, rhs,
          :modifier, OSX::NSDirectPredicateModifier,
          :type, OSX::NSGreaterThanOrEqualToComparison,
          :options, nil)

    validationWarning = OSX::NSLocalizedString("Process ID must not be less than 0.",
          "Process ID must not be less than 0.")

    idAttribute.objc_send(
          :setValidationPredicates, OSX::NSArray.arrayWithObject(validationPredicate),
          :withValidationWarnings, OSX::NSArray.arrayWithObject(validationWarning))

    runEntity.setProperties(OSX::NSArray.arrayWithObjects(dateAttribute, idAttribute, nil))

    return @@mom
  end

  LOG_DIR = "CDCLI"
  def self.applicationLogDirectory
    ald = nil
    if (ald != nil)
      return ald
    end

    paths = OSX::NSSearchPathForDirectoriesInDomains(
          OSX::NSLibraryDirectory,
          OSX::NSUserDomainMask,
          true)

    if (paths.count == 1)
      ald = paths.to_a[0].to_s + "/Logs/" + LOG_DIR
      fileManager = OSX::NSFileManager.defaultManager
      isDirectory = "NO"
      if fileManager.fileExistsAtPath_isDirectory(ald, isDirectory)
        return ald
      end
      if fileManager.createDirectoryAtPath(ald, :attributes, nil)
        return ald
      end
      ald = nil
    end
  end

  def self.managedObjectContext

    if (@@moc)
      return @@moc
    end

    @@moc = OSX::NSManagedObjectContext.alloc.init

    coordinator = OSX::NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(managedObjectModel)

    log_file = applicationLogDirectory() + "/" + STORE_FILENAME
    url = OSX::NSURL.fileURLWithPath(log_file)
    print "url=", url, "\n"

# newStore, error = coordinator.addPersistentStoreWithType_configuration_URL_options_error(STORE_TYPE, nil, url, nil, nil)

    newStore, error = coordinator.objc_send(:addPersistentStoreWithType, STORE_TYPE,
          :configuration, nil,
          :URL, url,
          :options, nil,
          :error, error)

    if (newStore == nil)
      OSX::NSLog("Store configuration Failure\n%@", error.localizedDescription)
    end

    @@moc.setPersistentStoreCoordinator(coordinator)
    @@moc
  end

  class Run 1000) AND (processID < 8580)")
request.setPredicate(predicate)

result, err = moc.executeFetchRequest_error(request, err)

 if (result == 0 || err)
   OSX::NSLog("Error while fetching\n%@", err.localizedDescription)
   exit -3
 end

enumerator = result.objectEnumerator
while (run = enumerator.nextObject) != nil
  if (run)
    print "On ", run.date, " as process ID ", run.processID, "\n"
  end
end

Remarks

If you copy/paste the code above in a text file, let’s call it coredata_cli.rb for reference, each time you execute it in a shell terminal, it will log the execution date/time and process ID and then list the whole log content. You can play around with the predicateWithFormat argument so to filter the log by process ID or date, if you like. This code has been tested with RubyCocoa 0.11.0.

In comparison to the ObjectiveC implementation, look at how much cleaner is the Ruby code given the absence of memory management (retain/release) and the power of the kvc_wrapper directive provided by the RubyCocoa bridge.

PS: The cdcli.rb file linked from Ernest Prabhakar’s blog is not available anymore at that location. I could retrieve it thanks to the great web WayBack Machine accessible here and you can found the ruby file here.

This post is a follow-up to another post I wrote on the very same subject. I am showing here the full implementation of a NSPersistentDocument based class that allows to use package documents embedding a Core Data store.

I short, what this post adds to the previous one is:

  • improved encapsulation;
  • NSDocumentController subclass to correctly handle the Recent Document menus;
  • fixed a problem with NSError handling, though still not doing any proper error management.

Those improvements originated from a discussion with Tim Perrett in the cocoa-dev mailing list and from a comment by Laurent Sansonetti to my original post. Thanks to both.

PersistentPackageDocument Class

The PersistentPackageDocument class can be used as a base class for your document classes whenever you want them use a document package embedding the actual Core Data data store. PersistentPackageDocument derives from NSPersistentDocument and overrides four methods: initWithContentsOfURL_ofType_error, writeToURL_ofType_forSaveOperation_originalContentsURL_error, readFromURL_ofType_error, displayname. Here’s the code:


class PersistentPackageDocument < OSX::NSPersistentDocument

   #-- returns the document name to display in the window title
   def displayName
     if (fileURL)
       documentNameFromDataStoreURL(fileURL)
     else
       'Untitled'
     end
   end

   #-- returns the package document path by stripping the dataStoreName component
   #-- from the data store URL; used in displayName
   def documentNameFromDataStoreURL(url)
     /([^\/]+)\/?$/ =~ url.relativePath.gsub(/#{dataStoreName}$/, '')
     $1 + " - View"
   end

   def dataStoreURLFromPackageURL(url)
     dataStorePath = url.relativePath.stringByAppendingPathComponent(dataStoreName)
     OSX::NSURL.fileURLWithPath(dataStorePath)
   end

   def readFromURL_ofType_error(url, type, errorPtr)
     path=url.relativePath
     if (!OSX::NSFileManager.defaultManager.fileExistsAtPath_isDirectory(path, nil))
#-- YOUR ERROR MANAGEMENT HERE
     end
     result = super_readFromURL_ofType_error(url, type, nil)
     if (!result)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
     end
     result
   end

   def writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, errorPtr)

#-- if content is not nil, then we are saving a newly created document
#-- in this case, initWithURL is not called, so we had no chance to fix the url,
#-- let's do it here.
     if (content == nil)
       path = url.relativePath
       url = dataStoreURLFromPackageURL(url)
       isDirectory = false
       if (!OSX::NSFileManager.defaultManager.createDirectoryAtPath_attributes(path, nil))
#-- YOUR ERROR MANAGEMENT HERE, set errorPtr
         return false
       end
     end

     ok = super_writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, nil)

     if (!ok)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
     end
     ok
   end

   def initWithContentsOfURL_ofType_error(url, type, errPtr)
     url = dataStoreURLFromPackageURL(url)
     ok, err = super_initWithContentsOfURL_ofType_error(url, type, nil)
     if (!ok)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
     end
     ok
   end

end

For a more detailed discussion of the rationale behind this implementation, see my previous post.

You should then change your MyDocument class (the one produced by XCode templates) so that it derives from PersistentPackageDocument instead of NSPersistentDocument and it adds a dataStoreName method that returns the data store file name for that specific document. Here an example:


class MyDocument < PersistentPackageDocument

  def dataStoreName
    'data.xml'
  end

#-- default RubyCocoa implementation: managedObjectModel, setManagedObjectContext, windowNibName, etc.

end

Supporting Recent Documents

The PersistentPackageDocumentClass as given above is fully capable of dealing with package documents embedding a Core Data data store. Unfortunately, it alone cannot ensure that the Recent Documents menu is correctly handled in your application. To that aim, you need to override your NSDocumentController noteNewRecentDocumentURL method so that it does some juggling with the path that is stored with the recent document menus.

If your package document is enough rich, chances are that you are already subclassing NSDocumentController, so overriding noteNewRecentDocumentURL is a snap. Otherwise, here is a sample subclass:


class PersistentPackageDocumentController < OSX::NSDocumentController

  def init
    super_init
  end

  def packageURLFromDataStoreURL(url)
    dataStoreName = currentDocument.dataStoreName
    OSX::NSURL.fileURLWithPath(url.relativePath.gsub(/#{dataStoreName}$/, ''))
  end

  def noteNewRecentDocumentURL(url)
    if (currentDocument)
     super_noteNewRecentDocumentURL(packageURLFromDataStoreURL(url))
    end
  end

end

As already mentioned, the key point is the method noteNewRecentDocumentURL, while packageURLFromDataStoreURL is just responsible for string manipulation. Note also that packageURLFromDataStoreURL accesses the current document to retrieve its dataStoreName and this forces to guard against the case when there is no current document. There are many alternative implementation of this behaviour, in particular you could define the method dataStoreName in the document controller class and let PersistentPackageDocument access it there. This approach has the adavantage that a “current” document controller is always there, but for presentation reasons it is not taken here.

Sublassing NSDocumentController has its own particularities. The easiest way to do it is in Interface Builder MainMenu.nib file. Just subclass and instantiate it in the nib and the above code will be used for your document shared controller. Read this FAQ for more information.

Summing up

The two classes defined above will allow you to easily integrate package documents in your Core Data application.

One final note: make sure you define your document classes as packages in your target properties.

A way to start…

July 15, 2007

RubyCocoa is definitely a great way to build Cocoa apps. Here I will collect resources about RubyCocoa and tell my experiences using it. But first of all, I would like to point you to a few online resources about RubyCocoa that I find really useful:

  • Homepage to the RubyCocoa Project
    The place to go when you want to be informed about releases, changes, details about RubyCocoa status and so on.
  • RubyCocoa Resources
    A collection of articles about RubyCocoa. Definitely worth a look, you will surely find something interesting to you. Includes a very good introduction to RubyCocoa, if you are new to it.
Follow

Get every new post delivered to your Inbox.