Saving Settings with Core Data Metadata

November 6th, 2017

Filed under: Cocoa, iOS Development, Mac Development | Be the first to comment!

I’m working on a Core Data app where I needed to save some document settings. Creating Core Data entities for these settings was overkill. In my research for a solution I discovered persistent store metadata. I did not find much information online on using persistent store metadata so I’m sharing what I learned in this article.

This article is not for people new to Core Data. If you’re new to Core Data, start by reading Core Data Programming Guide, which is part of Apple’s developer documentation.

Persistent Store Metadata

Core Data uses persistent stores to save data. While you can use an in-memory store, most persistent stores are files. In a document-based Core Data app the document file contains the persistent store.

Persistent stores have a metadata property to store metadata. The metadata is a dictionary. The dictionary keys are strings, and the dictionary values can be any data type. Core Data stores the store type and unique identifier in the metadata. You can also add your own entries to the metadata to store small pieces of data that don’t belong in a Core Data entity.

The most common reason for someone to use persistent store metadata is to save per-document settings in Core Data apps that use NSPersistentDocument. Most people who use Core Data have no need to deal with the metadata. They can stick with Core Data entities and Apple’s UserDefaults class. But there are edge cases where saving data in the persistent store’s metadata makes sense.

Getting the Persistent Store

The persistent store contains the metadata. If you want to access the metadata, you must access the persistent store. The managed object context has a persistent store coordinator. The coordinator contains an array of persistent stores. The store is the first element in the array.

let store = context.persistentStoreCoordinator?.persistentStores.first

Keep in mind the array of persistent stores can be empty. If you create a new document in a document-based Core Data application, a persistent store is not created until you save the document. Check that the persistentStores array is not empty before accessing its elements.

Updating the Metadata

Once you have access to the persistent store, take the following steps to fill the metadata:

  1. Create a variable for the metadata.
  2. Set the variable to the store’s metadata property.
  3. Add dictionary entries for your fields.
  4. Set the store’s metadata property to your variable.

Core Data saves some internal data in the metadata. You should be adding your fields to the existing metadata, not overwriting the existing metadata with your fields. That’s why you create a variable for the metadata and set its initial value to the store’s existing metadata before you add any fields to the metadata.

The following Swift code demonstrates how to update the metadata to store the author of a document:

class Document: NSPersistentDocument {
    // Initial value for author. You would set the author's
    // value from a text field in your app's user interface.
    var author = ""
}

if let store = context.persistentStoreCoordinator?.persistentStores.first {
    let metadata = fillMetadata(store: store)
    store.metadata = metadata
}

func fillMetadata(store: NSPersistentStore) -> [String: Any] {
    var metadata: [String: Any] = store.metadata
    metadata["Author"] = author
    return metadata
}

After updating the metadata, save the managed object context to save the metadata. If you stick with simpler data types like strings and integers, you can save without having to write any more code. If you try to save something more complicated, such as an enum, you will have to write code to conform to the Codable protocol or your app will crash when you save.

NSPersistentStoreCoordinator also has class and instance methods, both named setMetadata, to set the metadata. I went with the simplest solution, setting the store’s metadata property.

When to Update the Metadata?

A good time to update the metadata is when you’re about to save the managed object context. Register for the notification NSManagedObjectContextWillSave.

let center = NotificationCenter.default
center.addObserver(self, selector: #selector(Document.contextWillSave(_:)), 
    name: Notification.Name.NSManagedObjectContextWillSave, object: nil)

Handle the notification by calling your function to update the metadata.

func contextWillSave(_ aNotification: Notification) {
    updateMetadata()
}

Retrieving the Metadata

Call the NSPersistentStoreCoordinator instance method metadataForPersistentStore to retrieve the metadata for your persistent store. Supply the persistent store as the argument to metadataForPersistentStore.

The following Swift code demonstrates how to retrieve the document’s author from the metadata:

if let store = context.persistentStoreCoordinator?.persistentStores.first,
    let metadata = context.persistentStoreCoordinator?.metadata(for: store) {        
        fillSettings(metadata: metadata)
}

func fillSettings(metadata: [String: Any]) {
    if let theAuthor = metadata["Author"] as? String {
        author = theAuthor
    }
}

NSPersistentStoreCoordinator also has a class method to retrieve the metadata, but it requires you to write a do-try-catch block. Calling the instance method is easier.

Facebooktwittergoogle_plusredditmail

UITextView Word Count in Swift

October 24th, 2017

Filed under: iOS Development | Be the first to comment!

Getting the word count of an iOS text view is a little more difficult than a Mac text view because the NSTextStorage class’s words property is not available on iOS. A starting point for getting the word count is to separate the components of the text view’s string by whitespace and newline characters.

let words = textView.text.components(separatedBy: .whitespacesAndNewlines)

The problem is the componentsSeparatedBy function treats empty strings as words. If you have an empty text view, the word count will be 1. Pressing the space bar increments the word count. If you hit the space bar 5 times, the word count goes up by 5 even though you didn’t type any words.

The solution to get an accurate word count is to filter the empty strings out of the array of words.

@IBOutlet weak var textView: UITextView!

let words = textView.text.components(separatedBy: .whitespacesAndNewlines)
let filteredWords = words.filter({ (word) -> Bool in
    word != ""
})
let wordCount = filteredWords.count
Facebooktwittergoogle_plusredditmail

A Note About Swift for Job Recruiters

October 16th, 2017

Filed under: Uncategorized | Be the first to comment!

Apple first showed the Swift language to developers in June 2014. At the time I am writing this, that means Swift has been out for a little over three years. That means no developer can have more than three years of Swift experience. You can’t require 5+ years of Swift experience in your job description when the language hasn’t been out that long.

Facebooktwittergoogle_plusredditmail

Xcode 9: Undefined Behavior Sanitizer

October 5th, 2017

Filed under: Xcode | Be the first to comment!

Xcode 9 adds the Undefined Behavior Sanitizer. Undefined Behavior Sanitizer detects undefined behavior in your code, specializing in checking unsafe constructs in C, C++, and Objective-C code.

Undefined Behavior Sanitizer is turned off initially. You must enable it in your scheme. Select the Run step in the scheme and click the Diagnostics button at the top of the scheme editor. Select the Undefined Behavior Sanitizer checkbox.

Xcode9UndefinedBehaviorSanitizer

After selecting the Undefined Behavior Sanitizer checkbox, build and run your project to run it through Undefined Behavior Sanitizer. You can learn more about Undefined Behavior Sanitizer in the following WWDC video:

Finding Bugs Using Xcode Runtime Tools

Facebooktwittergoogle_plusredditmail

Xcode 9: Main Thread Checker

October 2nd, 2017

Filed under: Xcode | Be the first to comment!

Xcode 9 adds Main Thread Checker, a tool that detects when you make AppKit, UIKit, and WebKit calls on a background thread. Main Thread Checker should be enabled automatically when debugging, but you can check your scheme by selecting the Run step and clicking the Diagnostics button at the top of the scheme editor. Make sure the Main Thread Checker checkbox is selected.

Xcode9UndefinedBehaviorSanitizer

You can learn more about Main Thread Checker in the following WWDC video:

Finding Bugs Using Xcode Runtime Tools

Facebooktwittergoogle_plusredditmail