Sunday, June 28, 2015

Customer Support

I currently have two apps in the Apple App store (one free and the other paid).  I currently am working on my third one. If you have read some of my previous posts it's obvious this new app will have some type of sync option.  So I am really jazzed about learning and applying that solution.

So last week I was diligently working on this sync problem when I got an email from a customer about an update I had just done on my paid app.

This old app is written in Objective-C and was done as I learned iOS development.  Needless to say the code is UGLY.  I wouldn’t write it that way today.  Additionally it was first deployed on iOS 6, so it had that look and feel.  

Over the years I have upgraded and changed some of the visuals to be a bit more modern, but it still needs a good overhaul.  To be honest, I don’t have a lot of desire to go back and rewrite it.  Additionally it doesn’t bring in a lot of revenue.  I do get some, but not enough to make it worth supporting.

Anyway, as I was saying, I got an email from a customer telling me about a critical bug that I had missed in my last update (only 4 weeks ago) and it basically made the app useless.  I was appalled.  How could I have missed that bug?  

I knew I was rushed at the time I was doing the update but I never even touched that area of the code.  It wasn’t like the app was crashing or anything, but it still was a critical bug.  So I stewed all day while I was at my day job (earning real money) about what could have gone wrong and how I might fix it.

I dreaded getting home. I knew my whole evening would be lost trying to find and fix the bug and resubmit to the App store.  To make it worse, I knew it would take an additional eight days to actually get the app through the review process and on the store.  Which meant the one customer email I had was going to be a flood of emails soon, (or I would get no other emails, which would tell me no one was using my app).

I got home and quickly found the problem.  Turns out to be I had refactored one line of code where the problem was occurring, but that line should have been guarded by an “if” block.  It wasn’t which meant that line of code was run every time the view opened regardless of the state of app.  That was a very bad thing. It meant the user’s input would never be honored.

I fixed the bug and retested everything.  I found myself looking at code that needed to be refactored and I had to resist the urge as I wanted to get the update out that night.  Which I did.

That left me with the dilemma of do I wait eight days to get this fix out?  My app is hardly used right now, but what would my customers say who upgraded and found out they could no longer use the app?  So I decided the best thing I could do was request an expedited review.

I REALLY hated to do that for this app, but in the end I decided it was the right thing to do.  I knew Apple had very strict rules on when and how often an expedited review could be requested.  With the limited distribution my app has I really hated to use what I saw as my one time use of this feature for this app.  But I did it.

As expected, I got a very sternly worded email from Apple that I should not abuse the system and these types of requests were approved on a limited basis.  But by the time I got home from work the next day the update had been approved and was on the store.

So in the end I guess I learned a few things:
  1. The system worked as designed
  2. When working with an old app, just fix what needs to be fixed, don’t refactor
  3. And most of all I need to test more before releasing

I can’t wait for a released version of XCode 7 with it’s new UI testing capabilities.  I will definitely want to set that up for all my apps going forward.  

So why did I title this post “Customer Support”?  Well, for a single developer, customer support is hard.  You end up having to drop everything you are doing to address the concerns of your customer.  Programmers aren’t always the best front line support people.  But when it is only you, thats all you can do.

In the end this turned out ok.  The customer was happy that I had listened and was able to fix the problem so quickly. I learned I need to do a better job of testing, and I actually sold a couple of more units as well.

Next week, I hope to get back to my sync solution.  I have refactored the code a good bit since my test app and am now moving it into production.  So I hope I will talk about that in my next post.   

Sunday, June 21, 2015

Back From Vacation

I’ve been on vacation the last two weeks.  It was an experience.  Let’s just say that a seven day cruise, the open Pacific Ocean, two cross country flights and an inner ear infection DO NOT MIX!!.  My family had a great time, I, however, don’t remember much :-)


My app updates of a couple of weeks ago were 50% successful.  One app sailed through review, but the other was rejected.  The rejected app crashed on startup. Fortunately, when an app is reviewed because of a crash issue, Apple sends you the crash log.  I found out they have made a lot of progress with XCode and how it deploys to the App store.  I was able to symbolicate the crash log and figure out that I had two .xib files with the same name in the bundle I submitted for review.  The wrong one was getting picked up on the iPad version.


I should have found this in testing, there really is no excuse.  I figure what happened was my testing wasn’t deep enough to expose the problem.  Even though it wasn’t a section of code I was changing I still somehow introduced this duplicate file while adding new features.  I’ll be glad to move these apps to iOS9 so I can use the new UI Testing support.


Progress

Last time I went through the create method of my CloudKit DataService.  Today, I’ll talk about the Update method.  Here’s the code:


   func updateEntity(entity:LocalEntity) {  
     privateDB.fetchRecordWithID(EntityHelper.remoteRecordID(entity,zoneID: zoneID)){  
       record, error in  
       if (error != nil) {  
         println("error updating entity: \(error)")  
       }  
       else {  
         // Update the record with the changes  
         EntityHelper.updateRecordWithLocalChanges(record, entity: entity)  
         // Now save it  
         self.privateDB.saveRecord(record) {  
           record, error in  
           if (error != nil) {  
             // If we get an error here, I have no idea what to do  
             println("error updating entity: \(error)")  
             if let delegate = self.delegate {  
               dispatch_async(dispatch_get_main_queue()) {  
                 delegate.handleRemoteError(error)  
               }  
             }  
           }  
           else {  
             // Now update the local cache  
             let updatedEntity = EntityHelper.localEntity(record as CKRecord)  
             if let delegate = self.delegate {  
               dispatch_async(dispatch_get_main_queue()) {  
                 delegate.entityUpdated(updatedEntity)  
               }  
             }  
           }  
           // Finally get any other updates we missed  
           DataService.sharedInstance.getDeltaDownloads()  
         }  
       }  
     }  
   }  
This method takes a LocalEntity and updates the record in the private cloud store.  The method accomplishes this by first fetching the record from the private cloud store.  In the result block we use the passed in LocalEntity to update the data of the record we just retrieved, using the before mentioned EntityHelper.  We next save the record back to the private cloud store.  Next we notify the local store delegate the entity has been updated.  Finally we retrieve any other updates that have occurred.


The final CRUD method is the delete method.  Here is the code:


 func deleteEntityWithID(localRecordID: LocalRecordID) {  
     let recordIDToDelete = CKRecordID(recordName: localRecordID.id, zoneID: zoneID)  
     privateDB.deleteRecordWithID(recordIDToDelete) {  
       recordID, error in  
       if (error != nil) {  
         println("error saving entity: \(error)")  
         if let delegate = self.delegate {  
           dispatch_async(dispatch_get_main_queue()) {  
             delegate.handleRemoteError(error)  
           }  
         }  
       }  
       else {  
         if let delegate = self.delegate {  
           dispatch_async(dispatch_get_main_queue()) {  
             delegate.entityDeleted(localRecordID)  
           }  
         }  
       }  
       DataService.sharedInstance.getDeltaDownloads()  
     }  
   }  

In this method I create the CKRecordID of the record to be deleted.  I ask the private cloud store to  delete the record and notify the local store delegate that the entity has been deleted.  As in the other methods I take the opportunity to get any other updates that may have occurred.

Reading
I’m in between books right now and determining what to read next.  Right now I think it is either going to be “AngularJS In Action” or “Spring Boot In Action”, both from Manning Publishing.  I’ll probably start one or both these books this week. It will all depend on what interests me at the moment, web UI, or middle tier.

News
So far, I’ve watched about ten videos from this year's WWDC.  I am so impressed at how Apple brings so much to the table each year.  Things I’m really excited about are UI testing, code coverage , GameplayKit, CloudKitJS, and of course Swift 2.0.  It will be fun diving into all the new changes and technologies.  

One thing that continues to strike me as I watched all the new tools and demo’s of iOS9 is how mature the Apple ecosystem is.  Each year the integration between the Mac and iOS devices ramps up even more.  This year they also amped up the integration between apps with each of the OS’s. Apple has a very compelling user story (if you are willing to live solely in the Apple ecosystem)

Friday, June 5, 2015

More CloudKit

I had to take a break from my sync discoveries this week to respond to some customer support questions on two of my apps.  I submitted updates to both of them over the past few days.  As anyone knows who has done this, even getting app updates ready for the store can be hectic.  Anyway, I’m glad that’s over, now I just hope they make it through the review process soon, but with WWDC next week I’m thinking it will be a while.  Anyway, since the fixes were mostly "nice to haves" it’s no big worry.


Progress
So last week I walked through the initialization of my CloudKit DataService. This week the plan is to talk about the CRUD methods it exposes to the LocalDataService.


First let me talk about what is being stored.  The object being stored is called a LocalEntity.  It is very basic.  I have it extending a class called LocalPersistentObject.  My thinking is that any object I want to store in CloudKit will extend that class.  So I'll start with LocalPersistentObject:


1:  let CLOUD_STATUS_SHOULD_ADD:Int = 0  
2:  let CLOUD_STATUS_UP_TO_DATE:Int = 1  
3:  let CLOUD_STATUS_SHOULD_DELETE:Int = 2  
4:  let CLOUD_STATUS_SHOULD_UPDATE:Int = 3  
5:  let KEY_DATE_UPDATED = "DateUpdated"  
6:  let KEY_DATE_CREATED = "DateCreated"  
7:  let KEY_RECORD_NAME = "recordName"  
8:  let KEY_CLOUD_STATUS = "cloudStatus"  
9:  let KEY_TEMPORARY_ID = "temporaryID"  
10:  class LocalPersistentObject: NSObject {  
11:    var _cloudStatus = CLOUD_STATUS_SHOULD_ADD  
12:    var _tempID:String?  
13:    var dateUpdated:NSDate = NSDate()  
14:    var dateCreated:NSDate = NSDate()  
15:    var recordID:LocalRecordID?  
16:    override init() {  
17:      super.init()  
18:      self.tempID = "\(NSUUID().UUIDString)-Temp"  
19:    }  
The first part of the file shows the state flags I use to track the state of the object in the local storage.  I use this state to know what to do with the object when the client syncs.  This is stored in the _cloudStatus field.  I also have a _tempID field that is used to give the object a unique id when it is first created but not yet sync’d with the cloud.  This allows offline caching.


For this listing I took out the accessor methods for the tempID and cloudStatus fields just because they are not that interesting.  For local storage I overrode the initWithCoder and encodeWithCoder methods.  That’s how I am caching the objects locally.  As you will see though this really isn’t a requirement, it could be a NSManagedObject.  The design here is to separate the local persistent cache from the CloudKit implementation.  The key is the _cloudStatus field and the _tempID field.  You can think of LocalPersistentObject as being  an abstract class in other languages.  My intent is to never cache one of these locally.


The subclass, LocalEntity, is very simple and it represents the client's business object.  It only has two fields which are both strings, “name” and “desctext”.  The “desctext” holds text used to describe the entity in which I store a string describing when the object was last updated.  The “name” field is, well, the name of the Entity.  That literally is all there is to this object.  The “persistenty” stuff is all in the super class.


Now let’s go back and look at how these objects are used in the DataService CRUD methods.  I’ll start with the method to get all entites. This method is used when the client first connects to the CloudKit zone to get all the objects from the zone.  I expect this would only be called when the app is launched for the first time on a given device.

1:   // Retrieves all entities from CloudKit  
2:    func getEntities() {  
3:      // Build a predicate that is always true  
4:      let predicate = NSPredicate(format: "TRUEPREDICATE")  
5:      // Sort by the name field  
6:      let sortDescriptor = NSSortDescriptor(key: KEY_NAME, ascending: true)  
7:      // Set up the query  
8:      let query = CKQuery(recordType: "Entity", predicate: predicate)  
9:      query.sortDescriptors = [sortDescriptor]  
10:      // Perform the query on the correct zone  
11:      privateDB.performQuery(query, inZoneWithID: zoneID) {  
12:        (results, error) in  
13:          if (error != nil) {  
14:            println("error loading: \(error)")  
15:            // Notify the delegate an error occurred  
16:            if let delegate = self.delegate {  
17:              dispatch_async(dispatch_get_main_queue()) {  
18:                delegate.handleRemoteError(error)  
19:              }  
20:            }  
21:          }  
22:          else {  
23:            var entities = [LocalEntity]()  
24:            // Process the results we got and marshall the CKRecord objects into LocalEntity objects  
25:            for record in results {  
26:             let entity = EntityHelper.localEntity(record as! CKRecord)  
27:             entities.append(entity)  
28:            }  
29:            // Notify the delegate the entites were loaded.  
30:            if let delegate = self.delegate {  
31:              dispatch_async(dispatch_get_main_queue()) {  
32:                delegate.entitiesLoaded(entities)  
33:              }  
34:            }  
35:          }  
36:      }  
37:    }  

The code is pretty straight forward.  The only interesting part is line 26 where the results returned from the query are used to create an array of LocalEntity objects.  To help with this I created a EntityHelper class that can take a CKRecord and turn it into a LocalEntity.  It can also turn a LocalEntity into a CKRecord object.  I’ll go over that in a future post.


Here is the “addEntity” method:


1:  /**  
2:      Adds a LocalEntity to the CloudKit database  
3:      :param: entity The LocalEntity to be persisted.  
4:    */  
5:    func addEntity(entity:LocalEntity) {  
6:      // Get the record for the entity  
7:      let record = EntityHelper.recordForEntity(entity,zoneID: zoneID)  
8:      // Save the record  
9:      privateDB.saveRecord(record) {  
10:        record, error in  
11:        if (error != nil) {  
12:          // If there is an error notify the delegate  
13:          println("error saving entity: \(error)")  
14:          if let delegate = self.delegate {  
15:            dispatch_async(dispatch_get_main_queue()) {  
16:              delegate.handleRemoteError(error)  
17:            }  
18:          }  
19:        }  
20:        else {  
21:          // Convert the returned record from the save into a new LocalEntity  
22:          let entityToAdd = EntityHelper.localEntity(record as CKRecord)  
23:          // Set the tempID on the entity we just created to the tempID of the  
24:          // entity we had when we began the save. That way the delegate can  
25:          // update the correct entity in the local cache.  
26:          entityToAdd.tempID = entity.tempID  
27:          if let delegate = self.delegate {  
28:            dispatch_async(dispatch_get_main_queue()) {  
29:              delegate.entityAdded(entityToAdd)  
30:            }  
31:          }  
32:        }  
33:        // Pull down any other changes that might have occurred  
34:        DataService.sharedInstance.getDeltaDownloads()  
35:      }  
36:    }  

Essentially all this code does is convert the passed in LocalEntity into a CKRecord and save it to the CloudKit database.  One interesting part starts on line 23.  When the client first created the LocalEntity object it attached a temporary id, held in tempID, to the LocalEntity.  After the save we still have access to that original LocalEntity object, so we get the original tempID and place it on the record that was just saved to the cloud.


At this point we have a LocalEntity object that represents a successfully stored CKRecord.  It has a CKRecordID and a tempID.  Why?  

Remember the design, the client code only talks to the LocalDataService, so before the LocalDataService told the DataService to store the LocalEntity in the cloud it first persisted it into it’s local cache.  

But it needed an id so it gave it this temporary one. Now on the successful return trip from the CloudKit database it uses the tempID to find the LocalEntity in it’s local cache and update it.  

I’ll go over how the LocalDataService works in a future post, but suffice it to say, for a newly added object that has been successfully stored in CloudKit, the LocalDataService, after finding it in the local cache, updates the locally cached object’s data, marks its state as successfully stored in the cloud and removes the tempID as it is not needed for subsequent updates.  We can use the real recordID from here on out.


Next time I'll finish up the CRUD methods (Update and Delete)


Reading
I finished reading “Spring In Action” from Manning Publishing.  My overall opinion of the book is, if you want to learn Spring, here is where you go.  I always got the feeling the book wasn’t quite up-to-date for Spring 4.0 (although it said it was).  It was hard to tell when the author was discussing a Spring 4.0 only feature.  But overall I really liked the book and thought it was well put together and logically arranged.


News

Someone did post to my question on the CoreData forums about CoreData sync loosing sync records.  Basically they said “I’m having the same problem”. I'm rolling my eyes now.  A lot of good that did to help solve the problem.  Anyway, WWDC is next week, I’m not going, but I suspect there will be just too much goodness to consume that I’ll skip writing a post next week and bask in the holiday us geeks call WWDC Week!