Reusable XIBs in cocoa binding (DRY Views) The Tabs, Part 1

DRY Views

Classes should be reusable, functions should be reusable and any code we write should be highly reusable, a DRY code is a target yes, but what about a DRY views!?

Views also should be reusable, and when it comes to XIBs in MAC OS X app that uses cocoa binding it becomes a little bit tricky.

Our sample app is a master details app which has an inspector window which inspects the selected item whether it’s a master item or detailed item. you can find the source code here.

The master view contains a list of Managers, and yes as you guessed, the detailed view is a list of Employees managed by that Manager.

Untitled 5

Both ‘Manager’ and ‘Employee’ have common properties, and yes again you are right, we will create a super class ‘Person’ to keep the common properties like first name, last name and date of birth. This is good -as far as the model classes are concerned- but what about the inspectors! we then must have two separate views to inspect a Manager and an Employee! but both have common properties, why we should add first name, last name and date of birth fields twice in every inspector view?!!!

Duplicate controls to inspect common properties

 

let’s create a common properties inspector view first which can inspect Person’s properties, another inspector which can inspect Manager’s and one more for Employee’s properties.

One of the advantages of cocoa binding is that we don’t need -almost- any code to for simple CRUD operations on Model classes(using NSController‘s subclasses) nor to glue the UI controls with the model’s properties, so we don’t need Inspector view classes! except one to load the right inspector XIBs into a general proposes NSTabView (for simplicity) and that’s it!

InspectorWindowController.swift

class InspectorWindowController: NSWindowController
{
  var inspectedPerson: Person?
  @IBOutlet var tabView: NSTabView?

  func inspect(person: Person)
  {
  }
}

Inspector xib should be as simple as a tab view item referenced in the InspectorWindowController, it’s the container of the real inspectors that will be loaded later.

inspector
https://amtourky.files.wordpress.com/2016/01/inspector.png

Setup Inspecting flow

We are interested in the selected Person, so an observers on the selection property of the managers and employees will do the job, so on selection change, we will call inspect function of the inspector view controller passing the selected manager/employee.

so inspect function will be something like this, we will get the inspected person, remove any tab items if any to clean the views of old person, then load the new tabs required by the new selected person.

  func inspect(person: Person)
  {
      self.inspectedPerson = person
      self.removeAllTabItems()
      self.loadTabViewItemsForInspectedPerson()
  }

And as elegant the views are, the code behind the scene must be as well, so we are splitting the inspection into three main functions, remove tabs, load the new tab items and then populate the main tab view with the loaded tab items.

  func removeAllTabItems()
  {
      if self.tabView?.tabViewItems.count != 0
      {
          for tabViewItem in self.tabView!.tabViewItems
          {
              self.tabView?.removeTabViewItem(tabViewItem)
          }
      }
  }

 

  func loadTabViewItemsForInspectedPerson()
  {
      guard let theInspectedPerson = self.inspectedPerson
      else { return }

      for tabInfo in theInspectedPerson.inspectorTabsInfo
      {
          if let theNibName = tabInfo, theTabTitle = tabInfo["tabTitle"]
          {
              let newTabViewItem = NSTabViewItem(identifier: "")
              newTabViewItem.label = theTabTitle

              self.populateTabViewItem(newTabViewItem, fromNibName: theNibName)

              self.tabView?.addTabViewItem(newTabViewItem)
          }
      }
  }

 

  func populateTabViewItem(tabViewItem: NSTabViewItem, fromNibName nibName: String)
  {
      var topLevelObjects: NSArray?
      NSBundle.mainBundle().loadNibNamed(nibName, owner: self, topLevelObjects: &topLevelObjects)

      guard let theTopLevelObjects = topLevelObjects
      else { return }

      for object in theTopLevelObjects
      {
          if let theTabView = object as? NSView
          {
              tabViewItem.view = theTabView
          }
          else if let theTabObjectController = object as? NSObjectController
          {
              theTabObjectController.content = self.inspectedPerson
          }
      }
  }

NSBundle.mainBundle().loadNibNamed ??

So what we are loading exactly with the class function loadNibNamed? we are interested in two items:

NSView

The view itself, the NSView instance the xib has to set it as the view of the new tab we just created.

NSObjectController, where views get DRY!!!

The NSObjectController which the views are bound to, remember, those xibs have no owner at all, and the object controllers’s content isn’t bound to anything till now!

Untitled 7 not content object
https://amtourky.files.wordpress.com/2016/01/untitled-7-not-content-object.png

Till the the inspector know what person we are inspecting, then we are feeding the object controller with that person as the content object.

else if let theTabObjectController = object as? NSObjectController
{
  theTabObjectController.content = self.inspectedPerson
}

The top level objects loaded from xib will contain the NSControllers subclasses, pick the one/multiple that needs content object/set/array.. and start feeding them with whatever the normal view owner would do!

But what is person.inspectorTabsInfo!

Every model class will declare the xib files which can inspect it’s properties, a computed property that returns an array of objects describing a tab, contain a xib file name and title of the tab view item, Person declares PersonInspector xib, Employee will return the super inspectors (which is PersonInspector) and add to it EmployeeInspector and Manager also will return PersonInspector and ManagerInspector.

class Person: NSManagedObject
{
    var inspectorTabsInfo: [[String: String]]
    {
        return [["tabTitle": "Basic Info", "tabNibName": "PersonInspector"]]
    }
}

class Employee: Person
{
    override var inspectorTabsInfo: [[String: String]]
    {
        var parentTabsInfo = super.inspectorTabsInfo
        parentTabsInfo.append(["tabTitle": "Employee Info", "tabNibName": "EmployeeInspector"])
        return parentTabsInfo
    }
}

class Manager: Person
{
    override var inspectorTabsInfo: [[String: String]]
    {
        var parentTabsInfo = super.inspectorTabsInfo
        parentTabsInfo.append(["tabTitle": "Manger Info", "tabNibName": "ManagerInspector"])
        return parentTabsInfo
    }
}

And now we have a DRY cocoa binding views that work magically anywhere by just feeding it with the content it’s designed for!
You can also drive from the ObjectController another array controllers for more complex scenarios, like if you want to show the manager’s employees in the ManagerInspector, you would then create a new NSArrayController which content set is bound to the ObjectController.employees
Or even a complete different models by feeding multiple NSControllers at populateTabViewItem function.

 

sample
https://amtourky.files.wordpress.com/2016/01/sample.gif

Here is the source code of the sample app.

One thought on “Reusable XIBs in cocoa binding (DRY Views) The Tabs, Part 1

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s