The development of Contacts2Web

Introduction

contacts2web_512Part of the online course that I took from Stanford on iPhone app development involved choosing a final project. My top-level criteria for choosing a project included:

  • Incorporate several different iOS technologies.
  • Make use of an external web service and API.
  • Interaction with an external web site.

What I eventually settled on was a concept for allowing a user to view and edit their iPhone Address Book contacts both within an app as well as in a full web browser.

This functionality is already available as a service now at iCloud.com. My plan was to start with this capability and over time begin to add new functionality (social sharing, etc).

The Contacts2Web app is now available in the App Store.  In this document, I will cover some of the details that went into developing the app.

Contents


Back to Contents

Description

Contacts2Web allows you to synchronize your Address Book so that you can view and edit your contacts within this app or in a full Web browser.

Features:

  • Automatic synchronization of your contacts for remote access after login.
  • Duplicate Address Book entries are automatically deleted.
  • Secure https web access provided to access your contacts at www.contacts2web.com
  • Several reliability features built-in including lost network detection, automatic retry and database consistency checks.
  • Automatic detection of changes to your Address Book outside of the Contacts2Web app.


Back to Contents

Online Articles

During creation of Contacts2Web I posted a number of articles related to its development:


Back to Contents

Storyboard

The following image shows the storyboard that I used for Contacts2Web.  The bottom two right view controllers (Log View Controller and second instance of Entry View Controller) are only used in development/debug.

All the view controllers will be covered in the sections that follow.

contacts2web storyboard


Back to Contents

ContactsAppDelegate

In the app delegate we setup the following items in application:didFinishLaunchingWithOptions:

  1. Establish a default value for enable_reporting (more on this in description of Google Analytics preference).  It is important to note that the Settings bundle default value for this identifier only sets the initial switch position – we still need to set the default value as we do here.
  2. Register our Parse API keys. We use 2 sets of keys depending on whether our Scheme is set for development or release work (two distinct Parse app setups).
  3. Setup most of the Google Analytics parameters. We use Google Analytics primarily for logging exceptions and errors for later analysis. The decision on whether to actually enable Google Analytics for this app invocation is determined in RootViewController (see discussion in that section).


Back to Contents

Singletons

Apple describes a singleton as “… the sole allowable instance of a class in the current process”.

We use singletons to share properties and methods that can’t easily be referenced/shared between the classes that use them.

DebugSingleton

We use this singleton for debugging purposes:

  • Track and increment injected errors
  • Maintain log text for log controller. The log controller is only accessible during debug and can be used to view log messages while running app on actual device (without needing to be connected to computer and Xcode).

ManagedDocumentSingleton

Used to return a named Managed Document for Core Data (opening as needed).

CommonStateSingleton

We use this as a general purpose singleton for:

  • Tracking retries (both for main operation and unit testing)
  • Handling coverpoints (our methodology for ensuring that certain key portions of code are reached during unit testing)
  • Locks used with @synchronized and NSConditionLock object for certain Parse and Core Data save operations.
  • Lock related properties for unit testing
  • A pointer to EntryViewController used in unit testing


Back to Contents

RootViewController

This is the initial view controller upon startup.

Google Analytics preference

We use a Settings bundle with a single key called enable_reporting.

When this controller is loaded, or when the application enters the foreground (we use a notification for this), we check the status of enable_reporting key in NSUserDefaults. If the user has opted out of anonymous analytics and error reporting we disable Google Analytics.

Address Book permission

Immediately after startup we need to check to see that we have permission to access the user’s Address Book. For iOS 5 or older, this access is automatically granted.

For iOS6 and later, we first see if we can determine the grant status. If this is available and permission has been granted, we proceed to the login stage. If permission has been denied, we alert the user to the need for permission to use the app.

If we cannot determine our grant status, we request access from the user.

Logged in status

Once we have permission to access the user’s Address Book, we check to see if they are already logged in. If so, we push to the LoginLogoutStagingViewController which is a view controller used only in transitions for login/logout.

If we are not logged in, we display the Login button.

Parse Login

We use Parse.com as our database service in the cloud in order to provide web access to a user’s Address Book contacts (after a sync with the Contacts2Web app).

We implement the PFLogInViewControllerDelegate and PFSignUpViewControllerDelegate protocols and rely on the view controllers supplied by Parse.com for user signup and login (initiated by Login button).

During a new user signup we perform validation checks on the username to make sure it adheres to our restrictions on length, allowed characters, etc.

After a successful signup, a new Parse user record is created.

KeychainHelper

We store the user’s identifier (username) using Keychain Services for easy access. The use of the Keychain is actually a carryover from before we were using Parse for user management. It is no longer strictly necessary to use the Keychain in this app since the username is easily accessible from the Parse PFUser object.


Back to Contents

LoginLogoutStagingViewController

The logout mechanism turned out to be especially tricky to implement correctly. The issue is that we might be in the middle of a sync operation with Parse that could take many tens of seconds to complete (depending on how many contacts need to be resolved or populated). Imagine initiating a logout while in the middle of a sync and then logging in again to initiate a new sync while the previous sync is still in process.

Our solution was to block a logout until we had reached a steady state (sync completed). While blocking, we wish to message the user that we are waiting for active processes to complete. In retrospect, it may have been easiest to put up this message in the same view controller (EntryViewController) that was controlling the sync processes. This wasn’t done because of the additional view manipulation that would have been required.

Pushing to a new logout pending view controller while we are waiting for a steady state was problematic since there are race conditions to deal with (it is an error to pop to our root controller upon steady state before viewDidAppear occurs in the logout pending view controller that we push to).

The implementation we settled on was to use a staging view controller in the navigation push chain between our RootViewController and EntryViewController (main view controller after login). Upon a login, we always push through this staging view controller without pause on the way to EntryViewController. A logout is a little more involved:

  • If we are in a steady state in EntryViewController when a logout is initiated, we can pop directly to RootViewController bypassing the staging view controller.
  • If we are not in steady state, we pop to the staging view controller on a logout. We sit here and present a message to user about the pending logout with an activity spinner, until we get a message from NSNotificationCenter (initiated by EntryViewController) that a steady state has been reached.


Back to Contents

EntryViewController

Overview and Models

This is the main view controller arrived at after login. It presents a Contacts button which uses the Address Book API to present contacts as in the Apple Contacts app. It also provides a Sync button for manual syncs (needed if a record was edited at www.contacts2web.com).

There are models for Parse (parseConnection) and Core Data (coreDataConnection) interactions. Address Book specific interactions are handled by class method calls to the class AddressBook (which in hindsight could also have been designed as a model).

Development

There are 2 instances of EntryViewController, with the particular instance chosen by a segue in LoginLogoutStagingViewController.m that is dependent on our scheme. In development we use an instance that has extra buttons in the view for debug.

These buttons allow us to perform actions such as populating the Address Book with test data, emptying the Address Book, emptying Core Data and restoring the Address Book from Core Data.

Protocols

This view controller implements protocols required by our Parse and Core Data models, as well as protocols
used by the Apple API for display and edit of contacts.

Reachability

We use the Reachability class (https://github.com/tonymillion/Reachability) to determine our network state. We allocate a reachability object, start the notifier and listen for notifications on a change in reachability.

When handling an error that could potentially be retried, we check our network status. If it is not reachable, we block a retry. If the network later becomes available, the notification from the reachability object will cause us to automatically retry a Sync that was previously aborted.

Core Data

We use a Core Data document to maintain a copy of the contacts in the Address Book. This is done for a few reasons:

  1. Pulling information from Core Data is much faster than from the Address Book API.
  2. Core Data allows us to perform queries and select only the fields and records of interest.
  3. We can eventually use Core Data to drive an alternative interface to the user’s contacts using a NSFetchedResultsController and expanded capabilities over the native Apple Contacts app interface.

Parse

We use a database in the cloud at Parse.com in order to provide access to a user’s contacts at www.contacts2web.com.

Code Flow

Upon loading the view controller we perform the following functions:

  • Setup some initial state
  • Kickoff our reachability network monitor
  • Open our Core Data document
  • Start some timers (see Sync process section)
  • Prepare to monitor Address Book changes
  • Start our sync process

In a steady state the user has access to a button to access contacts as well as a Sync Now button to manually perform a sync (in the event that contacts were modified at www.contacts2web.com).

Compare state

We maintain a variable to represent the state of the sync process:

  • Assists in handling a change in reachability status
  • Provide a means of determining steady state during a logout request

Each transition for our sync state has an accompanying method which also helps us to control the progress indicators, buttons, labels, etc. depending on where we are in the sync process.

Error handling

Most methods in our models have been designed to accept a pointer to an NSError object and either return a BOOL or an object that can be compared against nil (to check for a successful result). At the point of error detection, the NSError object is allocated and initialized with an error domain, error code and error dictionary (which may include an underlying error).

All of the errors from our models and this controller are eventually handled by the routine handleError:error: This centralization of error handling allows us to easily deal with retries and fatal errors.

Retries

When possible we retry a sync operation that failed due to a cause that may be transitory (such as a network timeout). We use a backoff strategy, increasing the delay between retries by a factor of 2. Once the number of retry attempts has hit a predetermined limit, we cease further retries and alert the user.


Back to Contents

Sync process

A sync is also referred to as a compare in EntryViewController. It is the process by which we keep the Address Book, Core Data and Parse database in sync.

Record ids

Apple’s recommendation for keeping a long-term reference to a particular record is to store the first and last name, or a hash of the first and last name, in addition to the identifier.

We have expanded on this in order to handle our 3 way sync between the Address Book, Core Data and Parse.

One issue to recognize is that the identifier (record id) in a user’s Address Book is unique for a given device. If a user were to sync their contacts between devices using iCloud, the identifiers could be different on different devices for the same contacts.

What we have done is to use 3 sets of identifiers:

  • We store the Address Book record id (addressBookRecordId) as a field in Core Data (each Core Data document being unique for a device). This record id is used only for the sync between the Address Book and Core Data.
  • We calculate an MD5 hash on most of the fields of a record (excluding thumbnail image, etc.) and call this primaryRecordId. It is stored in Core Data and Parse and is the definitive identifier to link these databases. It is guaranteed to be unique since duplicate records are automatically deleted in the Address Book.
  • We calculate an MD5 hash on the first, middle, and last name and call this secondaryRecordId. It is also stored in Core Data and Parse records. It is not necessarily unique. It is used in certain circumstances to optimize the sync process between Core Data and Parse.

Address Book <-> Core Data

This sync occurs first.

Core Data is never updated without also updating the Address Book, so we consider the Address Book to be the golden source of contact information when comparing these two databases.

During this sync phase any differences are resolved by adding, deleting or overwriting records in Core Data – the Address Book is not touched.

Address Book/Core Data <-> Parse

After the Address Book and Core Data are synced, we then sync with the Parse database.

Since records can be edited at www.contacts2web.com, we look at record modification times to determine whether Address Book/Core Data or Parse records need to be updated in the event of a difference. No field merge is done – records are overwritten in their entirety.

More information on this sync process can be found in the section:
ParseConnection model -> Parse Sync details.

Timers

We use a couple timers in EntryViewController:

  1. A timer to automatically perform a sync if one has not occurred for 24 hours in an open app.
  2. A timer that is used to start a sync in one of 3 cases:
    1. We have come out of the background and the Address Book was changed outside of this app.
    2. A contact has changed within this app (using the Address Book People Picker API).
    3. We had previously aborted a sync due to loss of network connection, and the network has been restored.


Back to Contents

Schemas

This section discusses how data is represented in Core Data and Parse.

CoreData

A Core Data document includes a Person entity and To-Many relationships to other entities such as Address, Phone, E-mail, etc.

The following attributes in the Person entity are of  note:

primaryRecordId
Hash of entire record (excluding fields below).

secondaryRecordId
Hash of first, middle, last name.

addressBookRecordId
Correlation to Address Book record.

creationDate
Kept in lockstep with Address Book.

modificationDate
Kept in lockstep with Address Book. Used to identify records that may
need updating.

Parse

Overview

At Parse.com, our app makes use of several classes (can be considered tables):

  • User:  specific information, including login and sync state.
  • Contact:  User’s contacts, including individual record state (i.e. modified/synced).
  • AppRecordId:  Record ids of contacts (used to optimize use of API calls for syncs).

User

The following attributes in the User class are of  note:

userIdentifier
The username.

hashOfHashes
NSString. Set to “” on creation. Set to value of hash of hashes by app.

parseCompareInProgress
Set to true at start of sync and false at completion (set to false on user creation).

recordModified
Set to true to indicate that one or more records have been modified (at www.contacts2web.com).

databaseRevision
We can bump the version number in our Parse Cloud code to cause a mismatch with this field and force a database cleanup mechanism to kick in. Useful if we need to add/remove fields in any of the Parse classes.

AppRecordId

There can be multiple of these objects, 1,000 record ids per object (to allow a fit into 128K max size of a Parse record).

Objects only used by app for reconciling with records that app has in Address Book/Core Data.

We keep this array of record ids (primary, secondary) to be able to determine easily if records have been added or deleted from Address Book and these changes propagated to Parse. We can’t keep track of this in Core Data, since we want to be able to use a sync’d Address Book (via iCloud) to be used on multiple devices. We also want the user to be able to delete app (and Core Data db) without affecting Address Book or Parse db. This only leaves Parse to keep a more permanent view. It isn’t reasonable (also wasteful) to query all contact records (to gather ids) on every sync, so we keep a separate array of them that can be easily downloaded.

The following attributes in the AppRecordId class are of  note:

userIdentifier
The username.

recordIdArr
[{p:primaryRecordId, s:secondaryRecordId}, …]

Contact

A record in this class contains all the information for a specific contact. The To-Many entity relationships from Core Data are represented as arrays in this class.

The following attributes in the Contact class are of  note:

primaryRecordId
Can change as record is modified. Hash of most fields in record.

modifiedPrimaryRecordId
Copy of primaryRecordId before modification. Set to “” once modified records sync’d with app.

creationDate
Kept in lockstep with Address Book. Stored as Number.

modificationDate
Kept in lockstep with Address Book. Used to identify records that may need updating. Stored as Number.

ParseRecordState
A field indicating the state of the record. One of:

  • ParseRecordStateSynced Record has been synced to app.
  • ParseRecordStateModified Record has been modified on web site. Changes not yet synced.
  • ParseRecordStateNew Record has been added on web site. New record not yet synced.

userIdentifier
The username.


Back to Contents

Threads

When the user is given access to the Contacts button in the app, there should be no blocking of the main thread that causes a perceptible delay.

The sync between the Address Book and Core Data is very fast, but not negligible. We don’t display the Contacts button until this sync has completed. This sync is performed exclusively in the main thread. Note that we cannot pass managed objects or managed object contexts across threads, so restricting ourselves to the main thread simplifies things greatly.

The sync with Parse can be slow depending on the number of records to sync and the response time of the network and Parse service. The Parse API is thread safe. We restrict all Parse operations to operating in the global queue to avoid blocking the main thread (the user’s contacts via the Contacts button are thus accessible during the Parse sync).

We do need to be careful when our Parse model interacts via EntryViewController with our Core Data model – we cannot generally pass mutable objects across threads.


Back to Contents

Dictionary representation of contact record

With 3 different databases to keep in sync (Address Book, Core Data, Parse), we needed a format to exchange information. We settled on using an NSDictionary to store this information. Entities that are referenced by the Person entity (Address, E-mail, etc) are stored as an NSArray in the NSDictionary.

Methods to create the dictionary representation or extract information from it can be found in the Address Book class, Person+Create.m (category on Person NSManagedObject subclass) and PFObject+Utils.m (category on Parse object class).


Back to Contents

CoreDataConnection model

Overview

The Core Data model allows us to perform various actions with our Core Data document including:

  • fetch records based on criteria
  • limit properties that are fetched (to limit memory profile)
  • add, overwrite and remove records
  • empty core data document (for debug or in case of a Core Data access error)
  • calculate primary and secondary record ids
  • restore Address Book from Core Data (a debug feature)
  • report progress via delegate method
  • indicate completion via delegate method

Many of these actions are in support of the major function to support a sync between the Address Book and Core Data. Some methods are made available to handle requests from the Parse model (via EntryViewController).

Managed Document

The methods in CoreDataConnection operate on a managed document which is conveyed via initWithCoreDataManagedDocument when this model is initialized from EntryViewController.

Sync process

One of the model’s major functions is to resolve differences between the Address Book and Core Data. We initiate this in the method compareAddressBookToCoreData:error:

This method reads in all the contacts from the Address Book and Core Data (just a few properties including modification date).

We then execute 2 main loops. The first loop goes through all Address Book entries and either adds or overwrites Core Data entries as needed (first fetching the remainder of the Address Book record – we only had modification date to begin with).

The second loop goes through all Core Data entries and removes any entries not also present in the Address Book. This is in keeping with the principle that we follow wherein the Address Book is the golden source for compares against Core Data.

Memory usage management

In order to limit peak memory usage (measured using Xcode Instruments), we need to limit how many records we process at one time before a save to Core Data occurs during the sync process. We do this by breaking up each of the 2 main sync loops into a series of recursive loops, each processing at most 100 records. The goal was to keep our memory footprint under 20 Mbytes.

Removing and adding back the persistent store

Core Data has an issue (bug filed with Apple) where all memory is not released properly in the case of a save followed by faulting managed objects (turn objects into faults to break strong references). A context reset also does not properly release held memory.

The workaround used was to disassociate the persistent store (in our case an SQLite store) from the persistent store coordinator (NSPersistentStoreCoordinator) using the removePersistentStore: method of the coordinator. We then add the persistent store back to the coordinator with addPersistentStoreWithType:

From the docs:
“Instances of NSPersistentStoreCoordinator associate persistent stores (by type) with a model (or more accurately, a configuration of a model) and serve to mediate between the persistent store or stores and the managed object context or contexts. “

This workaround effectively released all memory held by Core Data.

Categories

We added a Create category on the NSManagedObject subclasses. In these categories we provide a method to add a record given a managed object context and a dictionary representation of a contact.

The Person category has some other methods as well:

  • An overwrite method (for the other entities we delete and then recreate).
  • A method to populate a contact’s dictionary representation from a Person object.


Back to Contents

ParseConnection model

Overview

This model is primarily responsible for allowing us to sync the Address Book/Core Data with our Parse database. All Parse interactions occur in the global queue. When we need to call one of our protocol methods (implemented by EntryViewController) to interact with the Address Book or Core Data, we perform a dispatch_sync to the main queue.

Parse Sync details

The sync with Parse is done after the Address Book and Core Data sync has occurred. This sync has been broken down into a number of discreet major parts:

1. Compute hash of hashes on Core Data primaryRecordId.

Each Core Data record has a primaryRecordId which is an MD5 hash on most of the fields of a contact record. We compute an MD5 result on the concatenation of all the primaryRecordIds and call it the hash of hashes. We use this as a quick check to see if anything has changed since our last sync with Parse (Parse User record contains the last result of this hash after a sync).

2. Refresh and Check User record.

  • Refresh our copy of the Parse User record.
  • Check for user record consistency.
  • Determine whether the last sync process completed (repair Parse database as needed)
  • Check the user’s database revision against the master revision.
  • Determine if this is an initial population for user at Parse
  • Check the stored hash of hashes against what was computed in step 1.
  • Check for an indication that one or more records have been modified at www.contacts2web.com

3. Optionally inject an error for testing

4. Reconcile records with Parse (if needed as determined in step 2)

  • Download a list of all record ids
  • Download a list of record ids for modified records
  • Form lists for further processing:
    • Records to add to Parse
    • Records to delete from Parse
    • Records to overwrite at Parse
    • Records modified at www.contacts2web.com. Depending on modification times either Parse record or Address Book/Core Data will be updated.

5. Save some bookkeeping information to Parse user record (new hash of hashes, mark sync as having completed, etc)

It is worth noting that since we use a record id which can change with edits (it is an MD5 hash of record contents), there are some interesting challenges to deal with in reconciling records in step 4 above.

As one example, consider the case where an address has been edited in the Address Book and also at Parse. The primaryRecordId would then be different on both sides. It still may be possible to match up the records using the secondaryRecordId (MD5 on first/middle/last name) if this secondary id is unique. We wish to match records whenever possible to minimize Parse API calls (an overwrite at Parse is cheaper than a delete followed by an add).

A more complex example that we handle is the case where a Parse record is modified so that it becomes identical to another record. Here we need to delete the duplicate records in the Address Book/Core Data and also at Parse.

Parse record saves

The Parse API allows one to save multiple objects (records) per save operation. We take advantage of this in order to minimize the delay of saving possibly thousands of records (for users with large number of contacts).

There are limitations however. The recommendation is to include no more than 20 objects in a save operation to reduce the likelihood of a timeout (Parse imposes a 30 second limit on a save).

In order to increase our throughput, we perform up to 7 concurrent batches of saves in parallel (140 records). Once all batches have completed in one round of saves, we can initiate further concurrent batches as needed.

We arrived at 7 as the max number of concurrent batches based on memory usage analysis using Xcode Instruments. Our goal was to stay under 20 Mbytes of peak app memory usage.

Generic dispatcher

Creating a means of saving records to Parse using the methodology described above is complicated. One can’t simply call a routine directly and pass all the records that need to be saved without possibly exceeding our self-imposed memory usage limit. There may be many different places in the code that might require a save of records to Parse, so we decided to create a generic dispatcher. This dispatcher should not need to know the details of how to obtain records or how many records to save. Instead the dispatcher should receive a pointer to methods that can perform these actions. This was accomplished with a couple of NSInvocation objects.

While saves are performed in parallel, we need to serialize invocation of the NSInvocation objects which we accomplish with a @synchronized block.

Processing of concurrent saves is accomplished with a dispatch group which also allows us to create a block in the code to wait until all the saves have completed.

Other complexity to deal with in this dispatcher had to do with handling errors which can occur at multiple places and in multiple threads in the concurrent save process.

Parse Cloud Code

Parse.com has a feature called Cloud Code that allows an app to make a call to Parse.com and execute some remote JavaScript. One of the advantages to using Cloud Code is that some functions can be offloaded to Parse.com and updated instantly as needed without waiting for an app update.

We use cloud code in several ways:

  • Get a list of record ids for modified records
  • Get a subset of record properties when we don’t need an entire record (in order to minimize network bandwidth)
  • Fetch the master database revision (for comparison against individual user database versions)
  • For unit testing (making specific edits to records, checking records)


Back to Contents

Address Book class

The Address Book class provides a number of utility class methods for interaction with the Address Book database, as well as methods for populating test data.

Utility methods

We developed methods used to create a new record or extract existing contact information using the dictionary format that was discussed in the section “Dictionary representation of contact record”. There is also a method available to remove all entries from the Address Book (for testing purposes).

Null fields

A null field is defined as a field that is not set. How we represent this differs between the native Address Book database and Core Data/Parse.

Address Book:
For single value fields, we set the field value to NULL. For multi-value arrays, only non-null entries are created.

Core Data/Parse (and dictionary representation of contact):

Null text fields are represented by the string “”.

A null date is represented as 1 sec before midnight on Dec. 31, 1969 (-1). When working with dates we use methods that relate values to the Epoch (Jan 1, 1970 GMT).

A null binary field (full sized or thumbnail image) is represented by a single byte = 0.

Key translations for dictionary representation

The Address Book uses either property ids or constant strings to represent various fields. We use our own labels as keys for the dictionary representation of a contact record. Translations are performed either in the method populating or extracting data, or in a helper routine for the case of multi-value array fields (address, e-mail, etc).

Performance

We can fetch all records or a single record from the Address Book (no query capability). When extracting fields from a single field value we use ABRecordCopyValue(). For multi-value arrays we use ABMultiValueCopyLabelAtIndex() and ABMultiValueCopyValueAtIndex(). Image data uses ABPersonCopyImageDataWithFormat().

It can be slow to fetch all contact information, which is one of the reasons we use Core Data as an intermediate database. During the sync process we initially only fetch the modification date, getting the remainder of the contact record only if needed. To facilitate this, the method that fetches information from the Address Book (getPersonRecordDictionaryForRecordId) is passed a variable to indicate which fields to return.

Testing

Methods are provided to populate the native Address Book with pseudo random data for testing purposes. Constants are used to determine how many loops are used for populating, and how many records per loop (to control our memory profile). A constant is also used for the random seed.

When records are populated we can also control other aspects of the data:

  • Whether sparse fields are allowed (can fields of records be left empty)
  • Are duplicates allowed (the main sync loop will delete any duplicates)
  • Are profile images allowed in a record (we have an image data creation method)

Memory usage

We wrap each loop used to populate records within @autoreleasepool { … } to release memory used after each loop iteration. Without this, the memory usage would grow with each iteration. It would eventually be released at the end of the current run loop.

Core Foundation

The Address Book database API uses Core Foundation, so we ended up using many of the constructs from this framework in this class. One of the primary issues we needed to be careful with here was retain counts since Core Foundation doesn’t use ARC. We were careful to release objects when we were done with them. When an object needed to be retained from a method return, we would prefix the method with “new” (a few other prefixes are also available for this purpose).

Data

We created some arrays of labels and values that we could randomly choose from for different contact fields.

If sparse fields were allowed, for certain fields we would randomly populate with data (versus leaving unset) using a specified weighting to choose how often to populate.


Back to Contents

Unit testing

Overview

I created a new target called Contacts2WebBasicUnitTests using the Cocoa Touch Unit Testing Bundle template, with a single test suite of the same name.

I created a new scheme copied from my Coverage scheme and called it UnitTestsAndCoverage. In this scheme I added the target Contacts2WebBasicUnitTests under the Test tab.

In the test suite, I created a set of 8 tests to address some of the major functionality and selected corner cases under the following broad categories:

  • Initial setup
  • Basic modified records
  • Large datasets
  • Sync (Address Book, Core Data, Parse) with various scenarios of modified/added/deleted records.
  • Sync retries

Coverage

I performed some coverage analysis when I first started developing unit tests, to get an idea of some of the sections of code that weren’t getting exercised with casual use of the app.

I created a new configuration called “Debug Coverage” and enabled the two build settings “Generate Test Coverage Files” and “Instrument Program Flow”. I then created a new Scheme called “Coverage” that used the new configuration.

It was necessary to set the key “Application does not run in background” to YES in the .plist file (temporarily for coverage testing only). To generate coverage I needed to click the Home button on the simulator after testing was done (and not just click the Stop button).

Coverage was viewed using the program CoverStory.

Coverpoints

In order to determine whether key portions of code were being exercised during unit testing, I devised a methodology called Coverpoints which used a concept borrowed from a different field (SystemVerilog). I use Coverpoints to assist in functional coverage – trying to determine when code has been exposed to a sufficient enough set of stimulus, including hitting corner cases, that one has a high confidence in its functionality.

I created a set of 16 named coverpoints (in CommonStateSingleton) that different portions of my unit testing was required to hit in order to pass.

I discuss Coverpoints in the article  Functional Coverage and Coverpoints

Class setUp/tearDown

In the class setUp method of my test suite (executed before any tests), I established the set of all coverpoints that needed to be covered by all tests collectively. This was checked in the class tearDown method (executed after all tests completed). Individual test cases would also check various subsets of this set of all coverpoints, sometimes checking that a coverpoint was reached a certain number of times.

Instance setUp

The instance setUp (run per test) is responsible for the following:

  • Get a reference to EntryViewController, polling as needed.
  • Wait for automatic compares to occur.
  • Put the app in a known state with 10 initial records.
  • Check that known state is consistent between Address Book, Core Data and Parse.

Utility methods

The test suite has a number of utility methods for interfacing with the Address Book class, as well as Core Data and Parse via our EntryViewController pointer.

In the case of calls to Parse, which get executed in another thread, we make use of an NSConditionLock to synchronize our code execution.


Back to Contents

Debug

Test Data

What I consider one of the best decisions made during development was to create the infrastructure for test data early on. I discuss this in the section for the Address Book class (subsection Testing). With an ability to create controlled, repeatable, pseudo-random data for small to very large data sets, I was able to quickly flush out issues with methods used to interface with the Address Book.

Creating a parallel path to the EntryViewController for debug, adding lots of test buttons, was also invaluable here. These buttons were tied to actions that allowed me to easily manipulate the test data in the Address Book, Core Data and Parse.

Time Profiling

I used the Time Profiler early in the process of coding the major sync loops in order to perform some first level optimizations (moving code out of loops where possible, etc).

Memory Usage Profiling

During the early stage of development I frequently used the Leaks and Allocations Instruments.

I did find one leak in the Address Book that was reported as a bug to Apple.

The Allocations Instrument was invaluable in helping me to design various portions of my code in loops in order to stay under the memory limit I set for myself of 20 Mbytes peak usage. It was also critical in helping to track down some issues in Core Data that were reported to Apple as bugs (as well as allow me to test and validate my workarounds).

SQLite Manager

I used the SQLite Manager plugin for Firefox to allow me to examine and manipulate the Core Data SQLite database during debug.

Macros

I used macros for several purposes during debug. These macros were included as part of either Global.h (common to this and future projects) and Project.h (project specific macros).

Log messages

I created a macro called FLOG (Formatted Log) from which I derived other log macros. FLOG would call NSLog() in a specific format, including the file and line number in the output. I could optionally also include a pragma call in FLOG to cause information about the FLOG invocation to appear in the Issue Navigator (serving as a bookmark).

More specific log macros were built off of FLOG and included in Project.h. These macros would be used for logging specific functional areas and could be turned on and off as needed. I also added the ability to optionally add logging to our Log View Controller for these macros, so that I could view log messages on the device itself when it was not connected to a computer and Xcode.

Error & Exception reporting

A macro called ERROR_LOG built upon the functionality of FLOG, prefixing a message to include the keyword “ERROR”. A call to this macro also reported an error to Google Analytics, as well as logging the message to our Log Controller (for untethered device logging & log viewing).

Exceptions (for programming errors) were raised with the EXCEPTION_LOG macro which reported the exception to Google Analytics and raised it within iOS.

Injected errors

Our injected error macros were used to test our error handling (INJECT_ERROR_RETURN_NIL, INJECT_ERROR_RETURN_NO).

These macros were inserted into methods where we wanted to temporarily inject an error. In order to work with these macros, the method needed to return nil or NO to indicate an error condition and include NSError **error as an argument.

When the macro was called it would do the following:

  1. Create a pragma message used as a bookmark in the Issue Navigator (so that we remembered to remove the macro when done)
  2. Increment and then return the current error count for the given error domain and code (using our DebugSingleton)
  3. If we had not reached the maximum number of errors to inject:
    1. Output a log message
    2. Create an error dictionary and allocate/initialize an NSError object
    3. Return nil or NO (depending on macro used)

Instrument flag macros

It is possible to put up flag markers while using Instruments in order to visually correlate code execution points of interest with an Instrument trace. We used three special macros to assist with this:

INSTRUMENT_POINT_SIGNAL (mark a point in time)
INSTRUMENT_START_SIGNAL (mark the start of a time period)
INSTRUMENT_END_SIGNAL (mark the end of a time period)

Advertisements

One response to “The development of Contacts2Web

  1. Pingback: Exploring the Contactually API | Finalize.com: My journey with iOS and other code adventures

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s