Nutrition GPA and Swift

Introduction

Originally written in 2014, the Nutrition GPA (GPA) app was authored in Objective-C and XIB files.  While the app was designed for phones, there were some screen size assumptions baked into the code that made it somewhat fragile to the Plus and X devices that have been introduced in the past few years.

Having experimented with Swift Playgrounds and done some reading, I wanted to try to faithfully port/convert the Objective-C code to Swift while modernizing other aspects of the app as I went.

Motivations

  • Swift has been around since 2014 and is becoming increasingly necessary to iOS development.
  • When the GPA app was written, it didn’t use Storyboards, so layout has suffered with newer devices like the iPhone X.
  • While a relatively simple app – it has just a handful of views – it’d be fun to experiment with WatchKit and add some new capabilities.

Database

The app uses a local SQLite store for quiz results and backing data.  Over the years, I’ve worked quite a bit with SQLite.  Generally, the pattern I’ve followed has been to create an object that represents an individual table row.  This object knows how to insert, update, and delete itself from the SQLite database.  The downside of this approach is that the object needs to know a lot about the underlying database representation.  And, there’s potentially a lot of duplicated code for boiler-plate operations to manage the interactions with the backing store.

Previously to bootstrap a record, each object would have a method similar to this:

+ (id)addObjectToTable(sqlite3*) db
{
   intsuccess = SQLITE_OK;
   if(insert_statement== nil)
   {
      const char*sql = "INSERT INTO some_table 
                 (tID) VALUES ('0')";

      success = sqlite3_prepare_v2(db, sql, -1,
                    &insert_statement, NULL);

      NSAssert1(success == SQLITE_OK, @"add
           object: failed to prepare with message 
           '%s'", sqlite3_errmsg(db));
   }

   success = sqlite3_step(insert_statement);
   sqlite3_reset(insert_statement);
}

With the GPA app, I’ve moved to FMDB, which abstracts much of this repetitive code and provides an opaque interface to the underlying data store by representing rows in a table as dictionaries.

Now, I can generalize the insert, update, and delete operations to three different APIs.

private func insertRecord(_record:
   Dictionary<String, Int>, tableName: String) -> Int

private func updateRecord(_record: 
   Dictionary<String, Int>, tableName: String) -> Int

private func deleteExistingRecords(_table: 
   String, _field: String, value: Any)

Each object that can store data to the database implements the DatabaseObject protocol, which has a single function to provide a dictionary of key/value pairs that the object wants to save or retrieve.

protocolDatabaseObject
{
   func asDictionary() -> Dictionary<String, Int>
}

And, the database insert becomes much more generic:

private func insertRecord(_record: 
    Dictionary<String, Int>, 
    tableName: String) -> Int
{
   var insertedRecordId =   
         Int64(Constants.VALUE_UNSET)
   var insertParams = record;

   insertParams.removeValue(forKey: FIELD_PK)

   var fieldNames = Array<String>()
   var namedParams = Array<String>()

   for key in insertParams.keys
   {
      fieldNames.append(key)
      namedParams.append(
         String.init(format:":%@", key))
   }

   let sql = String.init(format:
     "INSERT INTO %@ (%@) VALUES (%@)", 
        tableName, 
        fieldNames.joined(separator: ","), 
        namedParams.joined(separator: ","))

   databaseQueue.inDatabase{ (database) in
      database?.executeUpdate(sql,   
          withParameterDictionary: insertParams)
   }

   return Int(insertedRecordId)
}

extension Database
{
   func save(_databaseObject: DatabaseObject) 
      -> Int?
   {
      switch databaseObject
      {
      case is ObjA: return 
      insertRecord(databaseObject.asDictionary(), 
         tableName: TABLE_A)

      case is ObjB: return 
      insertRecord(databaseObject.asDictionary(), 
         tableName: TABLE_B)

      default: fatalError("Database: attempt to 
         store an unsupported database object")
   }

   return nil
}

Overall, I’m pleased with decoupling the database functionality from the objects that hold the data.  I experimented with cloud-based storage, like Parse, and having a logical separation of the backing storage from the object was very important.  Rather than use SQLite, I could repurpose the database class to use CloudKit, Parse, or another service without having to also refactor all of the storage objects to “know” about this change.

Gotchas

For the most part, the app port went very smoothly.  I opted to try to use all Swift collection classes, rather than mix-and-match the Objective-C types, like NSMutableArray, etc.  This made things much easier.

Still, there were a few moments where I had to do some Google searches and slight refactoring.

Exclusive Access to Memory

The app provides a 7-day, 30-day, and lifetime average score.  When the view loads, it looks at the recorded history and does some simple math.  To represent these values, I created an array to store the results:

private var avgScore: Array = [0, 0, 0]

During initialization, the app passes references (via inout parameter) to retrieve the respective values:

AppDelegate.pastAverages(avgAll: &avgScore[0], avgWeek: &avgScore[1], avgMonth: &avgScore[2])

Swift didn’t like this and gave me a run-time error.  There’s a build setting to enforce exclusive access to memory at both run-time and compile-time.  Disabling the build option solved the problem but really refactoring the code to use tuples or returning an array would be a better option.

Forced Unwrapping

During the project, my thinking on forced unwrapping (!) evolved – to a stance of: use with caution.  If the forced unwrap fails, the app will crash which is obviously undesirable.

There were times – like with controls, resources, or casting known variables that I felt safe applying the exclamation point – but it wasn’t without some thought of perhaps writing the same piece of code in a different way.

Extensions to Objective-C functions

For whatever reason, some of my favorite Objective-C methods don’t have exact Swift equivalents.  So, extensions to the rescue.  I added a handful for String and FileManager.

And, found myself considering how to use extensions to separate implementation groups more cleanly.  Having a single implementation file (and no-preprocessor) also made me consider applying Swift access controls – just to give myself hints on how I expected various classes to be used.

rawValue for Enums

Swift offers a lot more language flexibility than Objective-C – particularly with enums, tuples, and generics.  In my older code, I relied on ordinal rank of enum members for access to arrays (see the average score example above) – and found myself declaring Swift arrays and using the “raw value” more frequently than I’d expected.

Conclusion

The GPA app port to Swift went very smoothly.  I’m certainly glad to have implemented the UI with a Storyboard – it means a lot less CGMakeRect() and “magic” numbers floating through the codebase. The hiccups were all in all pretty minor.

There are some nice things provided by the language that I still need to come to grips with.  I felt my coding style evolving to fit the language and don’t think it’s finished yet!

Leave a Reply

Your email address will not be published. Required fields are marked *