A blog about iOS dev with RubyMotion

Paul Sturgess

Using MagicalRecord and Core Data in RubyMotion

MagicalRecord is a wrapper around Apple’s Core Data Framework. Written in Objective-C, it’s one of the most popular and mature libraries for working with Core Data.

I really like it as it simplifies a lot of the code, whilst it still allows you to ‘get your hands dirty’ when necessary.

This article details how I’m using it. This is by no means the ‘perfect’ solution, as I am evolving it all the time, but it is working well for me. By all means get in touch if you think I’m missing any obvious tricks.

One thing I have found is that it’s pretty much impossible to hide the fact you are using CoreData. Particularly with the requirement of a different context for each thread. But what I do want to do is make things as simple, consistent and maintainable as possible.

Installation

At the time of writing I’m using the MagicalRecord 3.0 branch, installed via motion-cocoaopds. My Rakefile includes:

1
2
3
app.pods do
  pod 'MagicalRecord', :git => 'https://github.com/magicalpanda/MagicalRecord.git', :branch =>'release/3.0'
end

Wrapping MagicalRecord

First I have a Database class to wrap common MagicalRecord tasks.

The main reason for this class is so that I do not sprinkle MagicalRecord calls all around my code. Having them all in one place will make it easy to update if/when the MagicalRecord api changes. It also encourages consistency in my usage of MagicalRecord as, for example, there are many ways to persist your data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class Database

  def self.filename
    "YourApplicationName.sqlite"
  end

  def self.path
    File.join(
      NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true),
      "YourApplicationName",
      self.filename
    )
  end

  def self.created?
    File.exist?(self.path)
  end

  def self.loadOrCreate
    MagicalRecord.setupAutoMigratingStack
  end

  def self.createTestDB
    MagicalRecord.setupStackWithInMemoryStore
  end

  def self.delete
    if self.created?
      self.cleanUp
      File.delete(self.path)
    end
  end

  def self.reset
    Database.delete
    Database.loadOrCreate
  end

  def self.cleanUp
    MagicalRecord.cleanUp
  end

  def self.defaultLocalContext
    MagicalRecordStack.defaultStack.context
  end

  def self.backgroundLocalContext
    NSManagedObjectContext.MR_confinementContextWithParent(defaultLocalContext)
  end

  def self.save_specific_context(localContext, callback = nil)
    localContext.MR_saveToPersistentStoreWithCompletion(
      lambda { |success, error|
        NSLog("success: %@", success)
        if success
          callback.call if callback
        else
          NSLog "Error saving Seed Data"
          NSLog("description: %@", error.description)
        end
      }
    )
  end

  def self.save_test_db!
    defaultLocalContext.MR_saveToPersistentStoreAndWait
  end

  def self.save_on_main_thread!(callback = nil)
    defaultLocalContext.MR_saveToPersistentStoreWithCompletion(
      lambda { |success, error|
        NSLog("success: %@", success)
        if success
          callback.call(defaultLocalContext) if callback
        else
          NSLog "Error saving Core Data"
          NSLog("description: %@", error.description)
        end
      }
    )
  end

  def self.save_on_background_thread!(callback = nil, completion_callback = nil)
    MagicalRecord.saveWithBlock(
      lambda { |localContext|
        callback.call(localContext) if callback
      },
      completion: lambda { |success, error|
        NSLog("success: %@", success)
        if success
          completion_callback.call if completion_callback
        else
          NSLog "Error saving Core Data"
          NSLog("description: %@", error.description)
        end
      }
    )
  end

end

In AppDelegate didFinishLaunchingWithOptions I call Database.loadOrCreate. As the name implies, this will either load my existing Core Data stack or it will set up a new one.

I also cleanup the database when my app closes via this method in the AppDelegate:

1
2
3
  def applicationWillTerminate application
    Database.cleanUp
  end

Entities

I create my Core Data entities in Xcode (although I do intend to look at the ruby-xcdm gem so I can stop using Xcode).

For now, the ib gem is great for allowing us to fire up Xcode just when we need it. The gem is mostly geared around using Interface Builder but I don’t use it for that.

Once you’ve installed the gem run:

$ rake ib:open

This will open Xcode. Inside the Resources folder on the left hand side there will be a .xcdatamodeld file. Select this and you can create your entities.

One thing to remember is to set the Class of each Entity to the corresponding Class in your app. Otherwise it will be a standard NSManagedObject.

So for each Entiy I create a corresponding class like so. Note it inherits from my own CustomNSManagedObject.

1
2
3
class ToDoItem < CustomNSManagedObject
  # more methods go here
end

I give each Entity created_at and id attributes in order to help with querying.

ActiveRecord Style Behaviour

The subclass of NSManagedObject I use looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class CustomNSManagedObject < NSManagedObject

  def self.defaultContext
    CurrentSaveGame.localContext
  end

  def self.all(context = nil)
    localContext = context || defaultContext
    self.MR_findAllSortedBy("created_at", ascending:true, inContext: localContext)
  end

  def self.first(context = nil)
    localContext = context || defaultContext
    self.MR_findFirstOrderedByAttribute("created_at", ascending:true, inContext: localContext)
  end

  def self.last(context = nil)
    localContext = context || defaultContext
    self.MR_findFirstOrderedByAttribute("created_at", ascending:false, inContext: localContext)
  end

  def self.count
    self.MR_numberOfEntities
  end

  def self.find(id, context = nil)
    localContext = context || defaultContext
    self.MR_findFirstByAttribute("id", withValue:id, inContext: localContext)
  end

  def self.new(attributes)
    self.build(attributes)
  end

  def self.build(attributes = {}, context = nil)
    localContext = context || defaultContext
    model = self.MR_createInContext(localContext)
    model.setValuesForKeysWithDictionary(attributes)
    model.created_at = Time.now
    model
  end

  def switch_context(localContext)
    self.MR_inContext(localContext)
  end

  def attributes
    self.entity.attributesByName.keys.each_with_object({}) do |attribute, attributes_hash|
      attributes_hash[attribute] = self.send(attribute)
    end
  end

end

Essentially this class gives me ActiveRecord like behaviour. Also, like my Database class, it ensures I contain some more MagicalRecord calls to a single place.

This means I can do:

ToDoItem.build(
  {
    id: 1,
    action: "Lorem ipsum dolor sit amet"
  }
)

My build method automatically inserts a created_at for every new record.

Data store

By default MagicalRecord.setupAutoMigratingCoreDataStack will use SQLite to persist your data.

Note that I don’t include a save method in CustomNSManagedObject. Saving an individual record isn’t the most efficient way to persist changes in Core Data. Everything is held in memory until the relevant context is told to save. Hence why I’ve put the save method in the Database class.

Persisting data on the main thread

If you’ve used any of the CustomNSManagedObject methods like find, all, or build without passing in a context then using Database.save_on_main_thread! will persist any changes made. For example:

1
2
3
4
5
6
7
new_item = ToDoItem.build(
  {
    id: 2
    action: "Some other todo item"
  }
)
Database.save_on_main_thread!

Persisting data on background threads

However, if updates are made on a background thread, then we need a new (temporary) context which is handled by Database.save_on_background_thread!.

For example:

1
2
3
4
5
6
7
8
9
10
11
Database.save_on_background_thread!(
  lambda { |localContext|
    new_item = ToDoItem.build(
      {
        id: 2
        action: "Some other todo item"
      },
      localContext
    )
  }
)

Alternatively if you don’t want to wrap everything in the block. Create a localContext and hold on to it. Make your changes and then save later down the line.

1
2
3
4
5
6
7
8
9
localContext = Database.backgroundLocalContext
new_item = ToDoItem.build(
  {
    id: 2
    action: "Some other todo item"
  },
  localContext
)
Database.save_specific_context(localContext)

Note most of the class methods in CustomNSManagedObject take an optional context so they can be used on background threads.

Testing

For tests I use the in memory data store and when saving it is synchronous. This ensures the data is there before the assertions!

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
describe "SomeTest" do

  before do
    Database.cleanUp
    Database.createTestDB
    @localContext = Database.defaultLocalContext
  end

  describe "some_method" do

    context "when some scenario" do

      it "should assign something is true" do
        # ...
      end

    end

  end

end

Logging to the Console in RubyMotion

The simplest way to output to the console from your RubyMotion app is to use puts. This works fine when your app is running in the simulator. However, puts does not output to the console when your app is running on the device itself.

You can view the live logs while your app is running on your usb connected device via:

$ motion device:console

You will see messages from your application alongside those from other apps or the system itself.

Instead of using puts, you’ll need to make use of NSLog. For example

NSLog("My debug messge")

Note, however, that the first argument for NSLog is actually a String Format Specifier.

The following are equivalent:

puts "A #{variable} goes here"
NSLog("A %@ goes here", variable)

Just remember that any NSLog messages you leave in your app will be visible to the curious, even after you’ve released to the App Store.

Using LLDB to Debug a RubyMotion App

RubyMotion now uses LLDB for debugging. Previously it used GDB.

If you’re getting random crashes with little or no backtrace it may be down to a memory allocation issue.

Launch your application with the debugger running via:

$ rake debug=1 NSZombieEnabled=YES MallocStackLogging=1

Trigger your bug and you should get output in Terminal like this:

2013-10-16 10:17:19.594 YourAppName[68253:a0b] *** -[UIBarButtonItem isSystemItem]: message sent to deallocated instance 0x9fd56f0
Process 68253 stopped
* thread #1: tid = 0x3127cd, 0x03ac0811 CoreFoundation`___forwarding___ + 769, queue = 'com.apple.main-thread, stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
    frame #0: 0x03ac0811 CoreFoundation`___forwarding___ + 769
CoreFoundation`___forwarding___ + 769:
-> 0x3ac0811:  jmp    0x3ac090c                 ; ___forwarding___ + 1020
   0x3ac0816:  movl   %edi, (%esp)
   0x3ac0819:  calll  0x3bb840e                 ; symbol stub for: class_getSuperclass
   0x3ac081e:  movl   %eax, %edi
(lldb)

Leave your application running and open a separate tab in Terminal. You can inspect the malloc_history of the object that has been deallocated via:

/usr/bin/malloc_history 68253 0x9fd56f0

Note the first argument is the Process ID as indicated where it says Process 68253 stopped. Note that the Ruby process that performs the build and launches your app (i.e. rake) is not the same as your app process.

The last argument is the Object Reference as indicated by message sent to deallocated instance 0x9fd56f0.

This (hex) number is the address of a piece of memory where the object we are interested in (was) located. The other traces in the malloc history are cases where that same memory location was used.

Running the above command will give you a dump of information from the stack logs. I’ve truncated the following example, as the information is plentiful, but it shows the kind of output you can expect:

malloc_history Report Version:  2.0
Process:         YourAppName [68253]
Path:            /path/to/YourAppName
Load Address:    0x1000
Identifier:      YourAppName
Version:         ??? (???)
Code Type:       X86 (Native)
Parent Process:  debugserver [68922]

Date/Time:       2013-10-16 11:14:24.104 +0100
OS Version:      Mac OS X 10.8.5 (12F45)
Report Version:  7

ALLOC 0x9fd56f0-0xa64da98 [size=425]: thread_4925a28 |start | main | UIApplicationMain | -[UIApplication _run] | CFRunLoopRunInMode | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoSource1 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ | PurpleEventCallback | _PurpleEventCallback | _UIApplicationHandleEvent | -[UIApplication sendEvent:] | ...

FREE  0x9fd56f0-0xa64da98 [size=425]: thread_4925a28 |start | main | UIApplicationMain | -[UIApplication _run] | CFRunLoopRunInMode | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoSource1 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ | PurpleEventCallback | _PurpleEventCallback | _UIApplicationHandleEvent | -[UIApplication sendEvent:] | ...

ALLOC 0x9fd56f0-0xa64dacf [size=112]: thread_4925a28 |start | main | UIApplicationMain | GSEventRun | GSEventRunModal | CFRunLoopRunInMode | CFRunLoopRunSpecific | ... | rb_scope__initWithFillColor:__ | vm_dispatch | rb_vm_dispatch | builtin_ostub1(objc_object* (*)(objc_object*, objc_selector*, ...), objc_selector*, objc_object*, unsigned char, int, unsigned long*) | _objc_rootAlloc | class_createInstance | calloc | malloc_zone_calloc

The output shows there had been two mallocs in the life span of the app and only one was freed.

If I was not expecting the first ALLOC to be freed, then I would investigate the trace of the FREE to figure out why it was freed. Alternatively if I was expecting the second ALLOC to be FREEd as well, then I would know that did not happen.

I knew I was expecting the memory to be freed and it was towards the end of the second ALLOC block I could see my own method initWithFillColor being called.

I was subclassing UIBarButtonItem with my method initWithFillColor like so:

  class MenuButton < UIBarButtonItem

    def initWithFillColor(fillColor)
      UIBarButtonItem.alloc.initWithCustomView(button(fillColor))
    end

    private

    def showMenu
      # ...
    end

    def button(fillColor)
      UIMenuButtonIcon.alloc.initWithFrame(CGRectMake(0.0, 0.0, 25, 15), fillColor:fillColor).tap do |button|
        button.addTarget(self, action:"showMenu", forControlEvents:UIControlEventTouchUpInside)
      end
    end

  end

My initWithFillColor method obviously doesn’t need to create another instance of UIBarButtonItem. So I updated it to the following and my memory bug was gone:

def initWithFillColor(fillColor)
  initWithCustomView(button(fillColor))
end

Hopefully this is a good start for debugging any memory bugs in your RubyMotion apps.

Thanks to the latest HipByte employee, Eloy DurĂ¡n, for the tips!

How to Debug Core Data Output to the Console in RubyMotion

Core Data can show you the underlying queries and execution times whilst you are running your app. Just fire it up with:

rake args="-com.apple.CoreData.SQLDebug 1"

Note you can bump the number up to 2 or 3 depending on how much information you want. 3 goes as far as to show you the result set Core Data has fetched.

Setting the Status Bar Colour for iOS7 in RubyMotion

First things first you need to set UIViewControllerBasedStatusBarAppearance in your info.plist.

This is done by updating your Rakefile like so:

app.info_plist['UIViewControllerBasedStatusBarAppearance'] = true

Now you need to implement the method preferredStatusBarStyle in your subclassed UINavigationController:

class MyCustomNavigationController < UINavigationController

  def preferredStatusBarStyle
    UIStatusBarStyleLightContent
  end

end

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.makeKeyAndVisible
    controller = YourFirstController.alloc.init
    @window.rootViewController = MyCustomNavigationController.alloc.initWithRootController(controller)
    return true
  end
end

Note that UIStatusBarStyleLightContent will turn your status bar text color to white.

How to Use the Crittercism Cocoapod in a RubyMotion App

Crittercism is a great way of collecting data and crash reports from your iOS application whilst it’s being used in realtime.

The easiest way to get it up and running in your RubyMotion app is to use the Coacoapod.

If you haven’t setup Coacoapods in your app yet then check out my article where I install the ViewDeck Cocoapod.

Update your Rakefile with:

Motion::Project::App.setup do |app|
  # ...

  app.frameworks += [
    'SystemConfiguration'
  ]

  app.pods do
    pod 'CrittercismSDK'
  end
end

Locate your Crittercism App id under Settings on the Crittercism website. Update your app_delegate.rb with the following just before you call makeKeyAndVisible:

crittercism_app_id = "51d1421f97c8f273e0000007"
Crittercism.enableWithAppID(crittercism_app_id)

Now when you build your app and errors get raised, you’ll start receiving crash reports.

How to Memory Profile a RubyMotion Application Using Instruments

Instruments, to quote Apple, “is a performance, analysis, and testing tool for dynamically tracing and profiling OS X and iOS code”.

Essentially it gives you live data while your application is running to help track down memory leaks and performance issues.

Profiling in the simulator

Simply fire your application up and then open Xcode. Open Instruments via the menu:

Xcode > Open Developer Tool > Instruments

Once Instruments had opened, I elected to keep the icon in the dock. This just saves having to open Xcode each time I want to run Instruments.

Once you’ve chosen the type of profiling you want to perform, from the Choose Target dropdown select, select Attach to Process. Under the System heading you should see the name of your app to select.

Finally hit the record button and you’re good to go.

Profiling whilst your app is running on your device

First up make sure your device is recognised by Xcode to be “used for development”.

If you haven’t done this, connect your device to your computer and open Xcode and open the organiser via:

Xcode > Window > Organizer

Find your device and select “Use for development”

Now in Instruments you should be able to select your iPhone as a target. Then under Choose Target you should get a list of your apps to choose from.

Note that your application must have been provisioned using a development profile.

How to View the RubyMotion Release Notes

Presuming you have RubyMotion installed. This one-liner should open the release notes in your favourite editor:

$ open /Library/RubyMotion/NEWS

How to Setup TestFlight for a RubyMotion App

The official RubyMotion guide for installing TestFlight got me most of the way there, but there were a few additional steps I needed to get everything working.

First and foremost I got my app running on my own device via usb.

However, for TestFlight, an Adhoc Provisioning Profile is required, as opposed to a Development Provisioning Profile. Essentially this is an Apple approved license to distribute your app to a limited number of devices without using the App Store.

Create a distribution certificate

Go to the Apple iOS developer website and create a Distribution certficate in a similar way to how you created your Developer certificate.

Create a fixed App ID

This is optional, but it felt right to me to use a fixed App ID instead of a wildcard.

Create an adhoc distribution profile

Finally, still on the Apple iOS Developer website, create an Adhoc distribution profile, download it and double click it to install it.

Setting up your Rakefile for multiple environments

I wanted to use both my Developer and Adhoc Provisioning Profiles at the same time. Without having to worry about switching them when required.

My Rakefile looks like this:

Motion::Project::App.setup do |app|
  env = if ENV['adhoc'] == '1'
          'adhoc'
        else
          'dev'
        end

  app.name = 'MyApp'

  # Provisioning profiles
  if env == 'adhoc'
    app.identifier = 'com.mydomain.myapp'
    app.codesign_certificate = 'iPhone Distribution: Company (XXXXXXXXXX)'
    app.provisioning_profile = '/path/to/adhoc/provisioing/profile/example.mobileprovision'
    app.entitlements['get-task-allow'] = false
  else
    app.identifier = 'com.mydomain.myapp'
    app.codesign_certificate = 'iPhone Developer: My Name (XXXXXXXXXX)'
    app.provisioning_profile = '/path/to/development/provisioing/profile/example.mobileprovision'
  end

  # TestFlight config
  app.testflight.sdk = 'vendor/TestFlight'
  app.testflight.api_token = 'sometoken'
  app.testflight.team_token = 'anothertoken'
end

desc "Set the env to 'adhoc'"
task :set_adhoc do
  ENV['adhoc'] = '1'
end

desc "Run Testflight with the adhoc provisioning profile"
# e.g. rake tf notes="My release notes"
task :tf => [
  :set_adhoc,
  :testflight
]

Credit to Hwee-Boon Yar for writing up how to setup the Rakefile for multiple environments

Your API token is under your account settings on TestFlight at: https://testflightapp.com/account/#api.

You can find your team API token at the following url: https://testflightapp.com/dashboard/team/edit/

One key bit of information I was initially missing was setting get-task-allow to false. This is only required for the Adhoc Provisioning Profile.

Use a custom rake task to deploy to TestFlight

Notice the rake task at the bottom sets the adhoc variable to ensure the Adhoc provisioning profile is used before firing off the TestFlight task added by motion-testflight.

So from now on I release to TestFlight via:

$ rake tf notes="My release notes"

How to Setup a Test Only Group in Bundler for RubyMotion

Coming from a Ruby on Rails background, it’s common place to see the Gemfile declarations in logical groups. This ensures your gems only get required in the environment they are needed.

A similar setup can be achieved in RubyMotion by tweaking your Gemfile and Rakefile.

In your Gemfile:

source "https://rubygems.org"
gem 'rake'
gem 'motion-cocoapods'

group :spec do
  gem 'motion-redgreen'
end

In your Rakefile:

if ARGV.join(' ') =~ /spec/
  Bundler.require :default, :spec
else
  Bundler.require
end

Thanks to the TinyMon application where I first saw this setup.