CHAPTER 10

image

Using SQLite/Core Data with Objective-C (iOS and Mac)

The Core Data stack described in detail in Chapter 9 is front and center in this chapter but in the context of Objective-C. Yes, using Swift and Objective-C with SQLite are very similar processes, but the details of the implementation are just different enough that they’re treated in separate chapters so that you can find what you’re looking for without flipping back and forth. If you’ve already read Chapter 9 (or, conversely, if you’ve read Chapter 10 before you get to Chapter 9), feel free to skip anything that’s redundant as you become bilingual in SQLite and both Apple languages.

Objective-C is the original language for what has become Cocoa and Cocoa Touch. It was designed for NeXTSTEP in the early 1980s. This was the time when object-oriented programming was becoming the primary way of developing software. A variety of languages were designed to implement this new paradigm. Some were built from the ground up, and others were built on top of then-existing languages such as C. (There even is an Object COBOL language.)

Objective-C was the only language for development on NeXTSTEP and later Cocoa and Cocoa Touch. In 2014, Swift was added to the mix. Like Objective-C before it, it is based on the programming styles and languages of the time, but in the case of Swift, those languages are “Objective-C, Rust, Haskell, Ruby, Python, C#, CLU, and far too many others to list,” according to Chris Lattner as quoted in Wikipedia. (Lattner was the original developer of Swift.)

There are important differences in the languages’ implementations of the Core Data stack, but the architecture is the same for both.

Looking at the Core Data Stack

The Core Data stack is a set of three objects that provide functionality. They are as follows:

  • Managed Object Model. This is what you saw built graphically in Chapter 8. It’s very similar to what is often referred to as a database schema. (A schema is described in a formal language rather than graphically.) Remember that relationships are explicitly defined in a Core Data model whereas in SQLite (and in SQL in general) the relationships are implemented in the WHERE clauses that you write.
  • Persistent Store Coordinator. Each SQLite table is usually represented as a persistent store in Core Data. This one-to-one correspondence of persistent store and database table is sufficient in most cases. However, you can have multiple persistent stores to improve performance or provide other optimizations. As far as the Core Data stack is concerned, one of the three key elements is a persistent store coordinator which coordinates all of the persistent stores for a given Core Data app.

    In most cases, the persistent store coordinator handles a single persistent store. However, the full name (persistent store coordinator) is still used because in more complex solutions the persistent store coordinator does actually manage several persistent stores. It is the persistent store coordinator and its persistent stores that do the transformation from the flat SQLite database file to the objects that you work with in your Core Data app. Accordingly, there are options you use when you create a persistent store that specify what the underlying data is (XML on OS X, SQLite on any of the platforms, in-memory stores, and others that may be created in the future).

    The defined store types are

    • NSSQLLiteStoreType
    • NSXMLStoreType (OS X)
    • NSBinaryStoreType (OS X)
    • NSInMemoryStoreType (OS X)
  • Managed Object Context. A managed object context is like scratch pad: it contains data that has been retrieved from a persistent store and that has (perhaps) been modified by your app. When you are finished with modifications, you save the managed object context. If you want to cancel the operation, you simply destroy the managed object context.

The Core Data stack consists of these three objects, and they relate to one another to create a single entity.

Fetching Data to the Core Data Stack

Fetch requests can be created in Xcode Core Data model editor or in your code. They retrieve data from a persistent store (through a persistent store coordinator) and place it in a managed object context. Fetch requests are not part of the Core Data stack, and that fact is reflected in the standard architecture for Core Data (described in the following section).

Objective-C Highlights

Swift looks in many ways like other programming languages you are probably familiar with, but there are some differences. Objective-C looks particularly different to many people. Here are some of the major differences. There is more information on developer.apple.com about Objective-C. Also, the code snippets in this chapter will give you some key examples, but the focus here is on SQLite and its syntax as well as on Core Data.

Using Quoted Strings

In Objective-C, a quoted string is introduced with @ as in @"myString".

Objective-C Is a Messaging Language

The most important difference is that Objective-C is a message-based language, and it is dynamic. In many object-oriented programming languages today, you call methods or functions of a class with syntax such as

myObject.myFunction ();

Instead of calling a function or method (the terms are used interchangeably here although there are some very subtle differences), with Objective-C, you send a message to an object. The message might actually have a name that makes you think it’s a function call, but deep down it isn’t. In Objective-C, the previous line of code would be written as

[myObject myFunction];

The myFunction message is sent to myObject.

Using Brackets in Objective-C

As you can see in the previous section, Objective-C looks different. Message are enclosed in brackets. The recipient is the first item within the brackets, and the message selector is the second. (A message selector identifies the message to be sent. It often looks like the name of a function or method but it is not a quoted string.) Either the recipient or the message selector can be an expression which is evaluated at runtime—hence it is dynamic.

Brackets have long been an issue for some people when they look at Objective-C. For that reason, there have been a couple of variants. After Apple purchased NeXT, modern syntax was introduced alongside the classical Objective-C brackets. That particular variant is no longer supported, but Objective-C 2.0 (2006) introduced dot syntax in which the brackets are replaced by a more accessible syntax. Both of these variants can function alongside classical Objective-C notation because they do not change what happens inside the compiler. The code in the section “Passing a Managed Object Context to a View Controller in iOS” uses dot syntax for Objective-C.

Chaining Messages

As is the case in Swift and other languages, you can chain statements together if they produce a result. Thus, you can write this code (assuming that myFunction returns an object that can respond to myFunction2).

((myObject.myFunction ()).myFunction2 ()

That is the same as writing

x = myObject.myFunction ();
y = x.myFunction2 ();

And, in Objective-C you can write

[[myObject myFunction] myFunction2]

This is the same as writing

x = [myObject myFunction];
y = [x myFunction2];

Ending Statements with a Semicolon

Objective-C statements end with a semicolon. (You can end a Swift statement with a semicolon, but it’s optional. It is required if you have two Swift statements on one line: the first statement is separated from the second with a semicolon.)

Separating Headers and Bodies in Objective-C

In Swift and many other modern languages, there is a single file for each class. In Objective-C there are two files—a header file with a .h extension and a body file with a .m extension.

Looking at Method Declarations

The syntax for method declarations is different from Swift. The quickest way to see the difference is to look at the examples in the section “Setting Up the Core Data Stack in AppDelegate for iOS.”

Handling nil in Objective-C

In object-oriented languages, there are often references to instances of objects which, in most cases, are stored on the heap (i.e., in an area of memory that can be used as needed by the app). It is different from the stack which is a last in-first out (LIFO) abstract memory structure. (“Abstract” in the sense that it behaves as if it was a single structure but it need not be in some implementations.) Stack data typically belongs to a specific function so that when the function completes, the stack can be cut back with its variables removed from memory.

Instances of objects are allocated in the heap and are not automatically cut back. They need to be removed by one means or another. In Objective-C, several strategies to clean up unused memory have been implemented over time. The current version relies on reference counting: when an instance is allocated, its reference count is set to 1. Every time that instance is used, the using code typically increases the reference count. When each code use of the instance completes, the reference count is reduced by 1. When the reference count reaches 0, the operating system can reuse the memory.

This memory management strategy relies on software engineers to manage the reference counts. Over time, automated and semi-automated tools have been provided. However, you can still find code with the primary memory management tools: retain and release which increase and decrease the reference count by 1.

With automatic reference counting (ARC), a great deal of this is automated.

In Objective-C, instances of objects are accessed with pointers. The traditional C language pointer syntax (an asterisk—*) is used. Thus, you will see declarations in Objective-C such as

NSView *myView;

or

NSView* myView;

The location of the asterisk doesn’t matter. This declares a pointer variable called myView which points to an instance of NSView.

Probably.

Rather than pointing to an instance of NSView, myView could be nil. You’ll find plenty of code in Objective-C to test if a pointer is nil before using it. That’s a good practice.

In the lifetime of an Objective-C object, it can pass through several states. First, it is declared as in

NSView *myView;

Then it is initialized to something. This can happen in a separate statement or in a combined statement.

NSView *myView;
myView = nil;

or

NSView *myView = nil;

If there is a period of time between the declaration and its being set to something valid (such as nil or an actual instance), the pointer is undefined. If you use it, your app’s behavior could be anything, but “crash” is likely to be part of the description. The problem of dangling pointers in Objective-C has existed for many years, and there have been many ways to address it. Today, with Objective-C, ARC is the primary way in which the problem is addressed. With Swift, this is one of the primary reasons for implementing optionals—so that it’s impossible to have these dangling pointers.

If you’re coming from another language, the square brackets and the use of nil (and the necessity of initializing pointers properly) may be the two biggest changes you have to get used to when referring to Core Data objects which may or may not exist (the nature of databases, like that of networks, means that you can’t rely on things being in existence).

Structuring a Core Data App with Objective-C

The basic Core Data stack (persistent store coordinator, data model, and managed object context) typically is placed in a location that is available throughout the app. The most common case (used in Master-Detail Application and Single View Application templates built into Xcode) places the Core Data stack in AppDelegate. AppDelegate typically creates views and other objects within the app. If they will need parts of the Core Data stack, AppDelegate passes them down to the view controllers and views either when they are created or when AppDelegate is managing them after they have been created by others.

Passing a Managed Object Context to a View Controller in iOS

Here is how the Master-Detail Application template for iOS passes the managed object context to the view controller. (This code uses Objective-C dot syntax.)

(BOOL)application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  // Override point for customization after application launch.
  UISplitViewController *splitViewController =
    (UISplitViewController *)self.window.rootViewController;
  UINavigationController *navigationController =
    [splitViewController.viewControllers lastObject];
  navigationController.topViewController.navigationItem.leftBarButtonItem =
    splitViewController.displayModeButtonItem;
  splitViewController.delegate = self;

  UINavigationController *masterNavigationController =
    splitViewController.viewControllers[0];
  MasterViewController *controller = (MasterViewController *)
    masterNavigationController.topViewController;
  controller.managedObjectContext = self.managedObjectContext;
  return YES;
}

The views are created from a storyboard, and this code starts from the basic window and steps down until it finds the navigation controller at the left of the split view controller. From there, it steps down to the MasterViewController inside the navigation controller. Having gotten that controller, it then sets the managedObjectContext property of the masterViewController to the managedObjectContext created in the Core Data stack in AppDelegate. This is the standard way of doing this.

You can also get to the Core Data stack by finding the app delegate and then accessing one of the stack objects. This breaks idea of encapsulation because you’re looking inside the app delegate. However, you’ll find some sample code that does this in various places, and a (weak) argument can be made for doing it this way if you only rarely—like once—need to access the stack. Here’s the code for this other way of getting to the stack.

AppDelegate *appDelegate = (AppDelegate *)
  [[UIApplication sharedApplication] delegate];

// use appDelegate.managedObjectContext or some other stack property

Setting up the Core Data Stack in AppDelegate for iOS

This code is from the Single View Application template that’s built into Xcode. It consists of lazy var declarations for:

  • applicationDocumentsDirectory. This is the directory in which your data model will be placed inside your app.
  • managedObjectModel
  • persistentStoreCoordinator
  • managedObjectContext

By using lazy vars, the initialization code only runs when you actually need it. Thus, this code in the template would never run unless you use Core Data.

The comments from the template code are included here.

Creating the App Delegate Header

This is the code to create AppDelegate in Objective-C. Notice that the import statement is different from Swift. Also, the compiler directives such as @property, @interface, and @end are not in Swift. The method declarations (saveContext and applicationDocumentsDirectory) are good examples of declarations in Objective-C.

#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;

@end

Synthesizing Properties in AppDelegate.m

Properties are declared in header files, but they must be synthesized in the body files. The @synthesize compiler directive typically involves creating a backing variable with the name of the property and accessing it directly when you’re in the body. Properties that are exposed in the header file are accessed as properties using dot notation, and behind-the-scenes property accessors work with the backing variables (the underscored variables). Except for special cases such as Core Data, a lot of this is done for you automatically if you want the default backing variable names.

@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;

Creating applicationDocumentsDirectory in iOS

This code uses the default file manager to find the document directory for your app. If you want to change the location of your data model’s directory, change it here in the code shown in bold. Either use a different directory or create one of your own (and be careful when you’re not using the default directory).

- (NSURL *)applicationDocumentsDirectory {
  // The directory the application uses to store the Core Data store file.
  // This code uses a directory named "com.champlainarts.SingleViewAppOC" in the
  // application’s documents directory.

  return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
    inDomains:NSUserDomainMask] lastObject];
}

Creating managedObjectModel in iOS and OS X

Here the managedObjectModel code is created as needed. The line shown in bold is created when you use the template. If you change your project’s name, change this line of code. Also note that, by default, the core data model is stored in the app bundle. The xcdatamodeld file that you build with Xcode Core Data model editor is compiled into a momd file during the build process.

- (NSManagedObjectModel *)managedObjectModel {
  // The managed object model for the application. It is a fatal error for the
  // application not to be able to find and load its model.

  if (_managedObjectModel != nil) {
     return _managedObjectModel;
  }

  NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"SingleViewAppOC"
    withExtension:@"momd"];
  _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
  return _managedObjectModel;
}

Creating persistentStoreCoordinator in iOS

This code creates a persistent store coordinator based on your managed object model. The first line shown in bold is created based on your project name, and it contains the location of the SQLite database file. You normally don’t move this file, but this is another place where you might have to change the file name if you have changed the project name.

Note that in using the data model, if self.managedObjectModel does not exist yet, it will be created after the reference here. Notice that this is done differently than it is in Swift.

It’s also worth pointing out the line where the SQLiteStoreType is chosen for the new persistent store. It’s shown in the second bold line.

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
  // The persistent store coordinator for the application. This implementation creates
  // and returns a coordinator, having added the store for the application to it.

  if (_persistentStoreCoordinator != nil) {
    return _persistentStoreCoordinator;
  }

  // Create the coordinator and store

  _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
    initWithManagedObjectModel:[self managedObjectModel]];
  NSURL *storeURL = [[self applicationDocumentsDirectory]
    URLByAppendingPathComponent:@"SingleViewAppOC.sqlite"];
  NSError *error = nil;
  NSString *failureReason = @"There was an error creating or
    loading the application’s saved data.";
  if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
    configuration:nil URL:storeURL options:nil error:&error]) {
    // Report any error we got.
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSLocalizedDescriptionKey] = @"Failed to initialize the
      application’s saved data";
    dict[NSLocalizedFailureReasonErrorKey] = failureReason;
    dict[NSUnderlyingErrorKey] = error;
    error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];

    // Replace this with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You
    // should not use this function in a shipping application, although it may be
    // useful during development.

    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }

  return _persistentStoreCoordinator;
}

Creating managedObjectContext in iOS

Finally, the managed object context is created. It incorporates the persistent store coordinator which, in turn, has a reference to the managed object model. Thus, you have the entire Core Data stack.

- (NSManagedObjectContext *)managedObjectContext {
  // Returns the managed object context for the application (which is already
  // bound to the persistent store coordinator for the application.)

  if (_managedObjectContext != nil) {
    return _managedObjectContext;
  }

  NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
  if (!coordinator) {
    return nil;
  }

  _managedObjectContext = [[NSManagedObjectContext alloc]
    initWithConcurrencyType:NSMainQueueConcurrencyType];
  [_managedObjectContext setPersistentStoreCoordinator:coordinator];

  return _managedObjectContext;
}

Setting Up the Core Data Stack in AppDelegate for OS X

This code is from the Cocoa Application for OS X template that’s built into Xcode. It consists of declarations that provide similar functionality to lazy var declarations in Swift. The declarations are for the following:

  • applicationDocumentsDirectory. This is the directory in which your data model will be placed inside your app.
  • managedObjectModel
  • persistentStoreCoordinator
  • managedObjectContext

Creating applicationDocumentsDirectory in OS X

This code differs from iOS to reflect the fact that the file structures differ. Its purpose is the same. The code shown in bold is created from your settings when you set up the project. If you change your developer ID or project name, you may have to change this.

- (NSURL *)applicationDocumentsDirectory {
  // The directory the application uses to store the Core Data store file.
  // This code uses a directory named "com.champlainarts.OSXProjectOC" in
  // the user’s Application Support directory.

  NSURL *appSupportURL = [[[NSFileManager defaultManager]
    URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]
    lastObject];

  return [appSupportURL
    URLByAppendingPathComponent:@"com.champlainarts.OSXProjectOC"];
}

Creating managedObjectModel in OS X

The code is the same as it is for iOS except for the name of the project. The code that is shown in bold for iOS would change for OS X to this (assuming your project name is appropriate).

NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"SingleViewAppOC"
  withExtension:@"momd"];

Creating persistentStoreCoordinator in OS X

Xcode creates this for you (or you can create it yourself if you’re building your own Core Data stack). Note the line in bold: it’s taken from your project setup, and you need to change it for your own project.

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
  // The persistent store coordinator for the application. This implementation
  // creates and returns a coordinator, having added the store for the application
  // to it. (The directory for the store is created, if necessary.)

  if (_persistentStoreCoordinator) {
    return _persistentStoreCoordinator;
  }

  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSURL *applicationDocumentsDirectory = [self applicationDocumentsDirectory];
  BOOL shouldFail = NO;
  NSError *error = nil;
  NSString *failureReason = @"There was an error creating or loading
    the application’s saved data.";

  // Make sure the application files directory is there
  NSDictionary *properties = [applicationDocumentsDirectory
    resourceValuesForKeys:@[NSURLIsDirectoryKey] error:&error];
  if (properties) {
    if (![properties[NSURLIsDirectoryKey] boolValue]) {
      failureReason = [NSString stringWithFormat:
      @"Expected a folder to store application data, found a file (%@).",
      [applicationDocumentsDirectory path]];
    shouldFail = YES;
    }
  } else if ([error code] == NSFileReadNoSuchFileError) {
    error = nil;
    [fileManager createDirectoryAtPath:[applicationDocumentsDirectory path]
      withIntermediateDirectories:YES attributes:nil error:&error];
  }

  if (!shouldFail && !error) {
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc]
      initWithManagedObjectModel:[self managedObjectModel]];
    NSURL *url = [applicationDocumentsDirectory
      URLByAppendingPathComponent:@"OSXCoreDataObjC.storedata"];
    if (![coordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil
      URL:url options:nil error:&error]) {
      coordinator = nil;
    }
    _persistentStoreCoordinator = coordinator;
  }

  if (shouldFail || error) {
    // Report any error we got.
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSLocalizedDescriptionKey] = @"Failed to initialize the application’s
      saved data";
    dict[NSLocalizedFailureReasonErrorKey] = failureReason;
    if (error) {
      dict[NSUnderlyingErrorKey] = error;
    }
    error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
    [[NSApplication sharedApplication] presentError:error];
  }

  return _persistentStoreCoordinator;
}

Creating managedObjectContext in OS X

This code is the same as in iOS.

Creating a Fetch Request in iOS

In iOS, the standard practice is to create the Core Data stack in the app delegate as shown previously in this chapter in “ Setting up the Core Data Stack in AppDelegate for iOS” and “Setting Up the Core Data Stack in AppDelegate for OS X.” In addition to the Core Data stack, you typically use fetch requests to fetch data from the persistent store into the managed object context. (On OS X, you use binding instead of view controllers and fetch requests.)

This code is fairly common. Here, it is used to fetch all entities with a given name (Event). The entity description is retrieved from the managed object context (the line is shown in bold) and the fetched results controller is created with a reference to that managed object context. A backing variable for the fetchedResultsController is created with the name _fetchedResultsController. This design pattern is frequently use: if the backing variable (starting with the underscore) exists, it is returned on request. If it does not exist, the fetched results controller is created and the underscore backing variable is set to it for the next time it’s needed.

- (NSFetchedResultsController *)fetchedResultsController
{
  if (_fetchedResultsController != nil) {
    return _fetchedResultsController;
  }

  NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
  // Edit the entity name as appropriate.
  NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event"
    inManagedObjectContext:self.managedObjectContext];
  [fetchRequest setEntity:entity];

  // Set the batch size to a suitable number.
  [fetchRequest setFetchBatchSize:20];

  // Edit the sort key as appropriate.
  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
    initWithKey:@"timeStamp" ascending:NO];

  [fetchRequest setSortDescriptors:@[sortDescriptor]];

  // Edit the section name key path and cache name if appropriate.
  // nil for section name key path means "no sections".
  NSFetchedResultsController *aFetchedResultsController =
    [[NSFetchedResultsController alloc]
    initWithFetchRequest:fetchRequest
      managedObjectContext:self.managedObjectContext
      sectionNameKeyPath:nil cacheName:@"Master"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;
  NSError *error = nil;
  if (![self.fetchedResultsController performFetch:&error]) {
    // Replace this implementation with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You
    // should not use this function in a shipping application, although it may be
    // useful during development.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }

  return _fetchedResultsController;
}

If you have created a fetch request in your data model as described in Chapter 8, you can use it to create a fetchedResultsController.

Saving the Managed Object Context

Although this is basically the same in iOS and OS X, there are some minor differences.

Saving in iOS

- (void)saveContext {
  NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
  if (managedObjectContext != nil) {
    NSError *error = nil;
    if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
      // Replace this implementation with code to handle the error appropriately.
      // abort() causes the application to generate a crash log and terminate. You
      // should not use this function in a shipping application, although it may be
      // useful during development.
      NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
      abort();
    }
  }
}

Saving in OS X

With the menu bar and its commands in OS X, you often are using a Save action to save data. Here is a typical saveAction function from OS X.

- (IBAction)saveAction:(id)sender {
  // 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.

  if (![[self managedObjectContext] commitEditing]) {
    NSLog(@"%@:%@ unable to commit editing before saving", [self class],
      NSStringFromSelector(_cmd));
  }

  NSError *error = nil;
  if ([[self managedObjectContext] hasChanges] && ![[self managedObjectContext]
     save:&error]) {
     [[NSApplication sharedApplication] presentError:error];
  }
}

Working with NSManagedObject

In any app that uses Core Data, you’ll need a Core Data stack, and you create it pretty much the same way each time except that you do customize the name of your project. (If you’re building your app from one of the built-in Xcode templates, there may be a Core Data check box you can use to automatically insert the Core Data stack code as well as your project name.)

But what about your data? That’s going to be managed by your Core Data stack, but surely it requires special coding. The fact is that, as is often the case with Core Data, the SQLite syntax is handled for you behind the scenes. You already have a data model (either from the template as-is or with your modifications) and if you don’t you need to create one using File image New image File.

Image Note  In OS X, view controllers are not generally used; instead, bindings are used. That topic is covered in developer.apple.com.

Each entity in your data model will be transformed into an instance of a class at runtime. Each of those instances is an instance of NSManagedObject or a descendant thereof. This section shows you the basics.

The examples in this section use the same two entities that have been used previously in this book: User and Score. Figure 10-1 shows them in the Xcode Core Data model editor graph view

9781484217658_Fig10-01.jpg

Figure 10-1. User and Score entities in a data model

Creating a New NSManagedObject Instance

There are several ways to create new Core Data instances. One of the most common is found in the Master-Detail Application template for Objective-C. Here’s the code that is used there. It is connected to a + in the  MasterViewController view, but any object in your interface can be connected to an action such as the following. (This happens in MasterViewController.ViewDidLoad).

- (void)insertNewObject:(id)sender {
  NSManagedObjectContext *context = [self.fetchedResultsController
    managedObjectContext];
  NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];

  NSManagedObject *newManagedObject = [NSEntityDescription
    insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];

  NSError *error = nil;
  if (![context save:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
      abort();
  }
}

You use this code (or code very much like it) whenever you create a new NSManagedObject. The beginning of this code just locates the managed object context. It might be a property in the class you’re working with. If it isn’t you may need to add a local property which is created (or passed through) when your class is instantiated or when the instance is loaded from a storyboard.

Next, you create an entity description, a subclass of NSEntityDescription. This encapsulates the entity information that you manage in your data model. In the code shown here, the entity description (called entity) is retrieved from a fetched results controller.

The fourth line of the code actually created a new instance called newManagedObject. That line of code is worth examining in detail. It’s really quite simple, but it’s the heart of Core Data.

NSManagedObject *newManagedObject = [NSEntityDescriprtion
  insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];

You use a class method of NSEntityDescription to create the new object—insertNewObjectForEntityForName. You need the name of the new object to be created and you need the managed object context into which to put it.

If you know the name of the object you want to create, you can omit the line creating entity (the second line of this code) and change this line of code to refer to it by name.

NSManagedObject *newManagedObject = [NSEntityDescription
    insertNewObjectForEntityForName: "User" inManagedObjectContext:context];

Obviously, this code is less reusable, but it works.

After you have created that new managed object you save the managed object context into which you inserted it:

NSError *error = nil;
  if (![context save:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
      abort();
  }

As noted in comments through the template code as well as elsewhere in this book, don’t use abort() in a shipping app. Instead, catch the error and log it, let the user know that there’s a problem (if the user can do something about it), or just fix the problem in your code.

That’s all it takes to create a new managed object.

If you want to set a value for an attribute defined for the entity in your data model, you can use key-value coding to do so with a line of code such as the following:

  [newManagedObject setValue:["New User" forKey:@"name"];

If the value to which you set the property is invalid, the save of the managed object context will fail. This will happen particularly if you’ve declared validation rules in the Xcode Data Model editor. Until you shake down the validation rules, you may have to deal with errors appropriately. (And, of course, if you are allowing user input, a whole host of user-generated errors may occur.)

Image Tip  If you really want to gain an appreciation of Core Data, work through the SQLite syntax that must be generated behind the scenes to implement the code shown here. It definitely is being executed, but you don’t have to type it in.

Working with a Subclass of NSManagedObject

You can create a subclass of NSManagedObject instead of using an instance of NSManagedObject itself. If you create a subclass (the process is described next), the chief benefit is that instead of using key-value coding with setValue:forKey: as shown in the previous section, you can set a value for newManagedObject using code such as the following:

newManagedObject.name = "New User"

Creating a new subclass of NSManagedObject is easy with Xcode Core Data model editor. Here are the steps involved. Begin with your data model open as shown in Figure 10-1. (It doesn’t matter if you’re looking at the graph or table view.) Choose Editor image Create NSManagedObject Subclass to open the window shown in Figure 10-2. Select the data model you want to use as shown in Figure 10-2. (Typically there’s only one. There may be several if you have been modifying your data model.)

9781484217658_Fig10-02.jpg

Figure 10-2. Select your data model

After you click Next, choose the entity or entities you want to subclass as shown in Figure 10-3. By default, the entity or entities that you have selected in the graph or table view (as shown in Figure 10-1) are selected here. You don’t have to subclass everything. Sometimes, you choose to subclass the more complex entities and leave the others as NSManagedObject instances.

9781484217658_Fig10-03.jpg

Figure 10-3. Choose the entities to subclass

Click Next and choose where to save your new files as shown in Figure 10-4. Also choose the language you want for the subclass files. (You can mix and match Swift and Objective-C.)

9781484217658_Fig10-04.jpg

Figure 10-4. Choose where to save the subclass files and what language you want to use

You should double-check the group (for the Navigator) and the target, but usually they’re correct. The option for scalar properties requires a little explanation. By default, your entity attributes are converted into Swift or Objective-C properties using the native Cocoa or Cocoa Touch types. Thus, a Double is converted to NSNumber. NSNumber is a much more powerful construct than Double (for one thing, it’s a class). If you’re working a lot with such a property, sometimes the power of NSNumber gets in the way. Choosing to use scalar types will use the basic platform (non-object) types which may make your code simpler.

If your project is basically written in Swift and you choose to create your subclass in Objective-C, you may be asked if you want to create a bridging header between the two languages as shown in Figure 10-5. Yes, you do want to do so.

9781484217658_Fig10-05.jpg

Figure 10-5. You can choose to create a bridging header

In a mixed-language project, don’t worry if you’re not asked about a bridging header. It only need to be created once, so after the first time, you won’t be asked.

The bridging header is created for you in the Build Settings of your project as shown in Figure 10-6. You don’t need to do anything further.

9781484217658_Fig10-06.jpg

Figure 10-6. Building settings with a bridging header

If you’re creating an Objective-C subclass, Xcode will create two files for each entity you have selected. As opposed to Swift, these will be the header (.h) and body (.m) files.

The first is the interface file, and it looks as follows:

#import "User.h"

NS_ASSUME_NONNULL_BEGIN

@interface User (CoreDataProperties)

@property (nullable, nonatomic, retain) NSString *email;
@property (nullable, nonatomic, retain) NSString *name;
@property (nullable, nonatomic, retain) NSSet<NSManagedObject *> *scores;

@end

@interface User (CoreDataGeneratedAccessors)

- (void)addScoresObject:(NSManagedObject *)value;
- (void)removeScoresObject:(NSManagedObject *)value;
- (void)addScores:(NSSet<NSManagedObject *> *)values;
- (void)removeScores:(NSSet<NSManagedObject *> *)values;

@end

NS_ASSUME_NONNULL_END
}

This is different from the extension file in Swift. First of all it is bracketed in NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END. These implement a feature in Objective-C that comes back from Swift. Inside a section with these brackets, all pointers are assumed to be non-null so it’s safe to rely on that in your code. (When used from Swift, these properties will be converted as forced-unwrapped properties with !.)

Inside the .h file you’ll see the property declaration of the category (CoreDataProperties) which declares the two attributes of the User entity (email and name). The relationship (scores) comes across as an NSSet. Note that because this section is bracketed as nonnull, the properties need to explicitly be marked as being nullable. This may seem as self-defeating, but as your code grows, you’ll see that making nullability an exception rather than a default makes your code cleaner and more robust.

The companion .m file looks as follows:

#import "User+CoreDataProperties.h"

@implementation User (CoreDataProperties)

@dynamic email;
@dynamic name;
@dynamic scores;

@end

@dynamic represents a promise that the property will be filled in at runtime (when it is retrieved from the persistent store).

That’s all it takes to create an NSManagedObject subclass in Objective-C.

Image Note  To see how to use your subclasses, see Chapter 12.

Summary

This chapter shows you how to work with SQLite/Core Data on OS X and iOS using Objective-C—the original of language for Cocoa and Cocoa Touch.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.226.255