Manual Core Data Migrations – Lessons Learned

So, I’ll try to keep this all brief.  In short, as everyone, including Core Data Ninja Master Marcus Zarra, has said:  Avoid heavy Core Data Migrations!

The tools are buggy, the crash bugs difficult to interpret.  I spent 4-5 hours already just sort of shooting at a black box.   So, I hope to share with you the things I learned and what caught me up.  If this is all too brief or out of context in places, it’s because I’m not a tutorial writer, and keep my blog posts quite often so that I can refer back to them.  So please feel free to write me if you need clarification.

I’ll also give the disclaimer that I’m not the most ‘canonical’ of programmers.  So, these are my findings.  I don’t know if they follow best practices, but I don’t think there are any best practices when it comes to this stuff…. otherwise I probably wouldn’t have made this lengthy blog post!

Scenario

Migration of old data store using an old Core Data model to a new data store with a brand new Core Data model

This documents my personal experience trying to migrate a Data Model I want to retire (i.e. completely separate Core Data Model file) to a brand new data model.

I wanted to move away from my old Objective-C pipeline which involves NSManagedObject subclasses using mogenerator that all have an Objective-C style Namespace and codegen, to a pure Swift version which uses the codegen feature of the Latest Xcode Core Data editor.

I kept these models separate because in a few releases when I know everyone has upgraded, I can eventually just remove the legacy model from the bundle and all the associated classes.

What I learned / experienced:

  • How to set up incremental migrations using a handy little helper from http://www.objc.io
  • How to migrate old data to new using Mapping Models
  • How to write and use custom NSEntityMappingPolicy objects
  • How to use the Mapping Model editor to call functions on these custom policies
  • All the various Pitfalls, Gotchas, and things that Apple really needs to improve on.

Useful Pre-requisite Reading:

How to set up incremental migrations

This is really what you want to do.  It makes your life easy in that you only need to make mapping models for each change to your data model.  There is a lot of code floating around the internet that helps you make an incremental migration manager.  I took one from objc.io and then modified it so it could support my use case where I am migrating a completely different store to a new one, and that new one should have its own URL on disk that may not follow from the original store.

Code for that migration manager can be found in a gist here.  Don’t just copy-paste:  You have to copy-paste into the correct files!  Also, it’s in objective-c so you’ll want to make a bridging header… It’s really not hard.

What is different in my version of MHWMigrationManager than from on objc.io:

  • It has a hack fix for a dubious migration error that has minimal debug help: ‘Mismatch between mapping and source/destination models’
  • It allows you to specify via the delegate a “Final Destination URL” once the Migration has successfully completed.  This can be useful as in my case where I have 2 different Stores and Models and want them to stay that way.

Set up your Core Data Stack

Basically, you start with Apple’s boilerplate code for Core Data and a persistent container, then you modify in a way that I’ve done here.  These 2 gists I’ve provided are all just starting points.  We haven’t even got to the annoying stuff yet.

If you create a brand new data model like I have done, be sure that it uses a different name than your old one.  You can’t rename the old one because you want the new one to have the same nice name.  Leave everything about the old one in place.

Pro Tip:  If you didn’t do this but already did the work of creating this new model version, if you create a new model either via duplication or copy-paste of entities, keep in mind that delete rules are not copied over, NOR are the inverse relationships.  So you’ll have to go through and set these by hand again.

Now Create Mapping Models

You can now proceed to create a mapping model in the ways you’ve probably read about on the internet already.   Pro Tips:

  • Only write your mapping model, once your Core Data Object Model is complete.  If you make a modification to your Core Object Model (.xcdatamodeld), any mappings that reference it will no longer be found by the Migration Manager.
  • Due to the nature of the migration process, you won’t have access to any subclass of NSManagedObject, and so this can make setting up NEW relationships difficult.  It is therefore a good idea to keep ‘foreign keys’ on your models with relationships.  (i.e. you extend your Post model to have an Attachment.  Attachment should have an attribute postId, or something identifiable)
  • If you use a subclass of NSEntityMigrationPolicy and you specify it in the Mapping Model editor, make sure that you also provide the Module name.  So, instead of LegacyToModernPolicy, I would put SongbookSimple.LegacyToModernPolicy, because my app’s module name is SongbookSimple.

Mapping Models are useful in that they allow a lot of customization, and you can sort of elect what you want to be done “automagically” and what you’d like to have more control over.  You get more control over these by using custom subclasses of NSEntityMigrationPolicy.

In general, for property or relationship mappings, the auto-generated functionality can get you pretty far.  Otherwise, when you have specified a custom Policy, you can make method calls on that policy.

When to use custom policy methods

If you have a somewhat strange migration for a specific entity, you can just remove all Attribute mappings in for an Entity Mapping, then in your subclass, override the method below.  The method signature is misleading because you are actually asked to create the equivalent version of the old object in the new object.  So you’re manually specifying how one object “becomes” the new object, and in my case where a Song started to store its data in a separate SongData object, you have to create this SongData object and find a way to link the two (i.e. songFilename) because you create the actual relationships later.  EDIT:  What I’ve learned however is that ideally this createDestinationInstances(…) method should really be about 1:1 mapping.  If your migration ends up taking one object and splitting it into two, you should write to Policy Subclasses and have 2 Entity Mappings:  i.e OldSong -> Song and OldSong -> SongData, then in the 2nd pass of the migration where the relationships are re-established, that’s where you can associate them again.

    override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
        
        let dInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext)
        
        let text = sInstance.value(forKey: "text") as! String?
        
        dInstance.setValue(sInstance.value(forKey: "lastModified"), forKey: #keyPath(Song.createdAt))
        dInstance.setValue(sInstance.value(forKey: "lastModified"), forKey: #keyPath(Song.updatedAt))
        dInstance.setValue("txt",                                   forKey: #keyPath(Song.fileExtension))
        dInstance.setValue(sInstance.value(forKey: "filename"), forKey: #keyPath(Song.filename))
        dInstance.setValue(sInstance.value(forKey: "firstLetterUppercase"), forKey: #keyPath(Song.firstLetterUppercase))
        dInstance.setValue(sInstance.value(forKey: "name"), forKey: #keyPath(Song.title))
        dInstance.setValue(sInstance.value(forKey: "oldFilename"), forKey: #keyPath(Song.oldFilename))
        dInstance.setValue(self.songDataLengthOfText(text), forKey: #keyPath(Song.songDataLength))
        dInstance.setValue(text, forKey: #keyPath(Song.text))
        dInstance.setValue(SongDataType.plainText.rawValue, forKey: #keyPath(Song.typeRaw))
        dInstance.setValue(nil, forKey: #keyPath(Song.userInfoData))
        
        // NOTE: DON'T DO THIS.  Set up a OldSong -> SongData entity mapping!
        // Then fetch this instance later via its songFilename
        //let songData = NSEntityDescription.insertNewObject(forEntityName: SongData.entity().name!, into: manager.destinationContext)
        //songData.setValue(sInstance.value(forKey: "filename"), forKey: #keyPath(SongData.songFilename))
        //let structure = SongDataStructure()
        //structure.set(text: text)
        //songData.setValue(structure.serialize(), forKey: #keyPath(SongData.nsdata))
        
        // NOTE: DON'T DO THIS.  Set up a OldSong -> SongViewPreferences entity mapping!
        //let viewPreferences = NSEntityDescription.insertNewObject(forEntityName: SongViewPreferences.entity().name!, into: manager.destinationContext)
        //viewPreferences.setValue(sInstance.value(forKey: "filename"), forKey: #keyPath(SongViewPreferences.songFilename))
        
        manager.associate(sourceInstance: sInstance, withDestinationInstance: dInstance, for: mapping)
        return
    }

I will say this:  this method above is used to create instances.  You don’t associate them yet!  That’s why you can see I set songFilename on these 2 new data types (in my new model), so that I can create their relationship later…  this is for the second pass in the migration, where relationship mappings are created (more on that).

Quick Reference for Mapping Editor FUNCTION arguments:

NSMigrationManagerKey: $manager
NSMigrationSourceObjectKey: $source
NSMigrationDestinationObjectKey: $destination
NSMigrationEntityMappingKey: $entityMapping
NSMigrationPropertyMappingKey: $propertyMapping
NSMigrationEntityPolicyKey: $entityPolicy

If I don’t need such a detailed mapping of an object, I can also use custom methods to set a property in the editor for any attribute.  Pro Tip: Make sure you change the the “Source Fetch” on a Relationship mapping to “Use Custom Value Expression” if you do this.

FUNCTION($entityPolicy, "filenameForLibraryWith:" , $manager)

This is syntax in the editor.  It’s saying “to get the value for this destination attribute, call a method on the (custom) policy called

func filename(forLibraryWith manager: NSMigrationManager) -> String

Remember you can use the arguments listed about in the Quick Reference section for knowing what kind of arguments you can pass into these methods!

You can also use these custom methods when performing relationship mappings.  Now, I personally haven’t had to override the method:

func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws

I’ve got by with the Relationship mappings in the Mapping Model editor and custom methods.

Do not reference subclasses of NSManagedObject during migration!

It would seem that this is how Core Data ensures that the migration can work reliably.  It sucks because you need to remember:

  • awakeToInsert is not called, because no instances of your subclass are used in migration, so whatever you would set in awakeToInsert, you should do here too.
  • The relationship accessors aren’t present either.  So be safe and specify the relationship and its inverse.

I created a handy helper method to find NSManagedObject instances:

class LegacyToModernPolicy: NSEntityMigrationPolicy {
    
    static func find(entityName: String,
                     in context: NSManagedObjectContext,
                     sortDescriptors: [NSSortDescriptor],
                     with predicate: NSPredicate? = nil,
                     limit: Int? = nil) throws -> [NSManagedObject] {
        
        let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: entityName)
        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = sortDescriptors
        if let limit = limit {
            fetchRequest.fetchLimit = limit
        }
        
        do {
            let results = try context.fetch(fetchRequest)
            return results
        } catch {
            log.error("Error fetching: \(error.localizedDescription)")
            throw error
        }
    }
  }

Pitfalls, Gotchas, Other Tips, Things I’ve already said

Here’s a list of things I might have mentioned already, but in this modern age where attention spans are low, and we need headings to actually read something, here is a list of things that I discovered in this long journey over 2 days:

    1. If you created your new Data Model and it started with the same name as your legacy one (i.e. SongbookSimple), it’s better to just create a new Core Data Model file with a new name (e.g. SongbookSimpleDataModel), then rebuild your data model by hand. This in practice means copy-pasting all the entities from the one to the other. Note! The Delete rules are not copied over, NOR are the inverse relationships. So you’ll have to go in and for each Entity, connect these up again.
    2. If you get an error during Migration such as:
      Couldn’t create mapping policy for class named (LegacyToModernArtistPolicy)
      It’s probably because you have Swift module-related errors. See Sandeep’s comment in this Question:
      So LegacyToModernArtistPolicy becomes SongbookSimple.LegacyToModernArtistPolicy and it works
    3. If you change your current model to accommodate the migration process, AND you’ve already run the app, you’ve altered the data model in a bad way. You don’t know if this is a typical state to be in, so better play it safe. You’ve got to remove the app from the simulator, re-install an old version so to populate an old data store, then run your new version in development.
    4. If you are doing custom fetch requests so to satisfy creating relationships (in your NSEntityMigrationPolicy), it would seem that fetch requests are returning NSManagedObject objects and not the subclasses. Nor can they be casted. For example, you might have seen a crash error:Could not cast value of type 'NSManagedObject_Library_' (0x6100000504d0) to 'SongbookSimple.Library' (0x101679180).This is why the helper above can be useful.  I fixed the bug I mention here with:
          func libraryForManager(_ manager: NSMigrationManager) -> NSManagedObject {
              
              do {
                  var library: NSManagedObject? = try LegacyToModernPolicy.find(entityName: Library.entity().name!,
                                                          in: manager.destinationContext,
                                                          sortDescriptors: [NSSortDescriptor(key: "filename", ascending: true)],
                                                          with: nil,
                                                          limit: 1).first
                  if library == nil {
                      let dInstance = NSEntityDescription.insertNewObject(forEntityName: Library.entity().name!, into: manager.destinationContext)
                      
                      // awakeFromInsert is not called, so I have to do the things I did there, here:
                      dInstance.setValue(Library.libraryFilename, forKey: #keyPath(Library.filename))
                      dInstance.setValue(NSDate(timeIntervalSince1970: 0), forKey: #keyPath(Library.updatedAt))
                      library = dInstance
                  }
                  
                  return library!
                  
              } catch {
                  fatalError("Not sure why this is failing!")
              }
          }
      
    5. You think you are far along because all of your NSEntityMigrationPolicy objects are having their custom methods called, nothing is failing, but at the end of the migration process you’re still getting errors. SpecificallyCocoa error 1570. This implies that data validations are failing.It could easily be that it’s because you made a false assumption that awakeFromInsert will get called on your new instances during the migration process. For example you added a non-optional attribute to your model who gets his default value set in awakeFromInsert. You should specifically make sure these are getting set either via the mapping editor and your custom policy (with a custom method), or try to handle the errors in performCustomValidationForEntityMapping:manager:error:I prefer graphic tools. So I just write some boring methods that will set default values and it’s all good.
    6. IF YOU CHANGE YOUR MODEL, THEN THE MAPPING FILE WILL BREAK AND NOT BE FOUND DURING MIGRATION.IF YOU CHANGE YOUR MODEL, THEN THE MAPPING FILE WILL BREAK AND NOT BE FOUND DURING MIGRATION.This means you have to rebuild the Mapping File.
    7. In all of the NSEntityMigrationPolicy subclass, make sure you are always dealing with NSManagedObject and not their subclasses!! If you don’t, your automatic mappings (that use custom methods on the policy subclass) will fail.
    8. You don’t HAVE to create mappings in the Mapping Model editor. Basically, the part that has attribute mappings is a way of NOT overriding the method
      func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws

      and the Relationship Mappings editor is a way of not having to subclass

      func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws

      ONE THING you DO need to understand is that during the migration process, you are dealing with NSManagedObject instances, and not instances of your (likely) subclasses. So you don’t get all the benefits of the accessor methods that would ensure object graph integrity. What does this mean? You should make sure that you connect both sides of a relationship via relationship mappings. This means, in your data model it makes sense to keep a field to a ‘primary key’, so that you can use one object to find another.

    9. YAY! The Migration manager says it migrated… except… I got an exception:
      'Mismatch between mapping and source/destination models'

      I have to say, Core Data documentation sucks. There’s just not enough of it, and no explanation of why it fails during migration. Totally crap. I’ve spent a whole day just trying to shoot into the dark, hoping I’ll figure out what’s going on with this giant black box.

      The fix for this occurs in my modified MHWMigrationManager gist, and it’s what sorted me out!

Still Missing from everything that I learned:

  • I don’t really know how one is supposed to work with the createRelationships(…) method.  I only do that aspect of migration via the Mapping Model Editor and custom methods on the Policy
  • I haven’t done anything with validation yet.  So I don’t know how that comes into play.

Wrapping Up

This was a super long post.  But hopefully it can be seen as a good reference guide or at least things to think about when trying to do heavyweight migrations.  Now that I understand how they’re done, they’re not AS scary anymore.  What was annoying was trying to figure this all out.

If you benefit from this, I would be happy if you let me know!

Advertisements

UITableView with Animating Section Header

So, I’ve been scratching my head on an issue I’ve been having:

I want to have a UITableView with a section header, because I want it to stick to the top of the screen while scrolling.  The thing is, it’s supposed to have a search bar that expands when you enter “search mode”.

What you see in the video is my prototype.  Tapping a table cell is supposed to toggle this search mode on and off.  In the video above you see that that table cell layout updates and expands, but the section header seems to be “one click behind” and expands when the cell layouts push up because they think the header is shorter.

This behaviour happened via the standard posts on stack overflow.  Here, for example.

Now, perhaps my issue is because I also have a stretchy section header, which I based off of this post:

Anyway, I found the solution involved a slightly complicated solution, but once it’s in place, there’s little to know (i, ii, iii, …) and do (A, B, C, D, …) :

i)  I assume you will have one section on your table view
A) Define an associated view on your view controller, which will be your “expandableHeaderView” (more on the specifics later)
Screenshot 2016-03-17 14.03.04

B) Define your class to have such properties and methods:

IB_DESIGNABLE
@interface QLExpandableSectionHeaderTableViewController : UITableViewController

@property (nonatomic, assign) IBInspectable NSInteger expandableSectionHeaderInactiveHeight;  // defaults to 44
@property (nonatomic, assign) IBInspectable NSInteger expandableSectionHeaderActiveHeight; // defaults to 88

@property (nonatomic, strong) IBOutlet UIView *expandableHeaderView;

@property (nonatomic, readwrite, getter=isHeaderExpanded) BOOL headerExpanded;

- (void)setSectionHeaderHeightExpanded:(BOOL)expanded animated:(BOOL)animated;

@end


@implementation QLExpandableSectionHeaderTableViewController

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        _expandableSectionHeaderInactiveHeight = 44.f;
        _expandableSectionHeaderActiveHeight = 88.0f;
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    if (!self.expandableHeaderView) {
        NSLog(@"WARNING: You haven't provided an associated header view you want to use when toggling section header height!");
    }
    else
    {
        // this has to be clear, since animation doesn't quite work as desired.
        self.expandableHeaderView.backgroundColor = [UIColor clearColor];
    }
}


- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    if (section == 0) {
        if (self.isHeaderExpanded) {
            return self.expandableSectionHeaderActiveHeight;
        }
        return self.expandableSectionHeaderInactiveHeight;
    }
    
    return 0;
}

- (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    if (section == 0) {
        
        if (self.expandableHeaderView) {
            return self.expandableHeaderView;
        }
        else
        {
            // will probably never get used
            UIView *header = [UIView new];
            header.backgroundColor = [UIColor redColor];
            header.frame = CGRectMake(0, 0, self.tableView.bounds.size.width, self.expandableSectionHeaderInactiveHeight);
            return header;
        }
    }
    

    return nil;
}

- (void)toggleSectionHeader:(UIButton*)button
{
    if (self.isHeaderExpanded) {
        [self setSectionHeaderHeightExpanded:NO animated:YES];
    }
    else {
        [self setSectionHeaderHeightExpanded:YES animated:YES];
    }
}

- (void)setSectionHeaderHeightExpanded:(BOOL)expanded animated:(BOOL)animated
{
    if (self.isHeaderExpanded && expanded) {
        return;  // nothing to do!
    }
    
    self.headerExpanded = expanded;
    [self.tableView beginUpdates];
    [self.tableView endUpdates];
    
    //NSLog(@"%@", self.searchHeaderView.constraints);
    __block NSLayoutConstraint *headerHeightConstraint = nil;
    [self.expandableHeaderView.constraints enumerateObjectsUsingBlock:^(__kindof NSLayoutConstraint * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
        // I determined this name by logging the self.searchHeaderView.constraints and noticed
        // it had a constant value that was one of my searchBar(In)activeHeight values.
        if ([obj.identifier isEqualToString:@"UIView-Encapsulated-Layout-Height"]) {
            headerHeightConstraint = obj;
            *stop = YES;
        }
    }];
    
    CGFloat newHeight = expanded ? self.expandableSectionHeaderActiveHeight : self.expandableSectionHeaderInactiveHeight;
    headerHeightConstraint.constant = newHeight;
    
    // after the animation completes, the expandableHeaderView's frame doesn't change until a layout update
    // , which only happens after you start scrolling.  This will ensure it has the right size as well.
    CGRect frame = self.expandableHeaderView.frame;
    frame.size.height = newHeight;
    
    [UIView animateWithDuration:animated ? 0.3f : 0.0f
                          delay:0.0f
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         [self.tableView layoutIfNeeded];
                     } completion:^(BOOL finished) {
                         self.expandableHeaderView.frame = frame;
                     }];
    
}

The code above will handle animation.  There is still some weirdness happening on the UIKit side of things, because the the header view’s subviews animate according to auto-layout, but the self.expandableHeaderView doesn’t visually change size until a layout update occurs.  And this only occurs once you start scrolling.  So we set that frame to the size it’s supposed to have, and that sorts it all out. As a result, we have to set the self.expandableHeaderView.backgroundColor = [UIColor clearColor];

And that solved the problem! It’s a bit hacky, but I found no other way to accomplish this that isn’t THAT ugly.

And here’s the result:

Recipe: NSString from an Enum

I had previously outlined a way to get strings from enums and enums from strings. It is posted HERE.

This approach is ok, but I find that it’s not often you need to go from string back to enum. Work with enums and use an enum to string method when you need readability.

Furthermore, it uses an array to do lookups. This doesn’t work so well if your enum values aren’t sequential.

Custom NSError classes come to mind, when you have error codes, but printing an error code to the console is of very little help.

As a result, I did the following for you to consider:

// SOError.h
// A dummy error class

typedef NS_ENUM(NSInteger, SOErrorCode)
{
  SOErrorCodeCocoaError = -1,
  SOErrorCodeUnspecified = 0,
  SOErrorCodeNotConnected = 10,
  SOErrorCodeY2KBug = 99
};

// THIS IS THE METHOD WE CARE ABOUT RIGHT NOW
extern NSString* NSStringFromSOErrorCode(SOErrorCode code);  
extern NSString * const SOErrorDomain;

@interface SOError : NSError

+ (SOError*)errorWithErrorCode:(SOErrorCode)code;  // convenience method.  Sets domain

@end

and now part of the .m file:

#import "SOError.h"
#import "SOError+ErrorCodeConversion.h"  // see next code block!

static NSDictionary *CodeConverter = nil;  

NSString* NSStringFromSOErrorCode(SOErrorCode code)
{
    if (!CodeConverter) {

        // we implement this in a category to reduce boring code in this .m file.
        CodeConverter = [SOError errorCodeConversionDictionary];  
    }
    
    NSString *result = CodeConverter[@(code)];
    
    if (!result) {
        result = @"No description found.  Did you add a new error code but not add it to errorCodeConversionDictionary?";
    }
    
    return result;
}

@implementation SOErrorCode

// Omitted...

@end

What we did above is a lazy load of an NSDictionary that will serve the SOError class. You can just store strings against the error code then look them up when you need them. Then we implement a category on SOError (don’t have to but I like separation of concerns…) and import that.

Then we just implement that category method (not forgetting to declare that in the category header (not shown):

#import "SOError+ErrorCodeDictionary.h"

@implementation SOError (ErrorCodeDictionary)

+ (NSDictionary*)errorCodeConversionDictionary
{
     NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];

     // now just use the code as a key, and a string of that enum as a value:
     dictionary[@(SOErrorCodeCocoaError)] = @"SOErrorCodeCocoaError";
     dictionary[@(SOErrorCodeUnspecified)] = @"SOErrorCodeUnspecified";
     dictionary[@(SOErrorCodeNotConnected)] = @"SOErrorCodeNotConnected";
     dictionary[@(SOErrorCodeY2KBug)] = @"SOErrorCodeY2KBug";

     return dictionary.copy;  // immutable
}

And there you go. Strings from Error codes. It’s a bit of work, but if you google around, it would seem there’s no simple way around that boring work.

How to know when a UIScrollView is scrolling (moving).

I don’t know about you, but it’s happened to me where I’ve needed to know when a scroll view is currently scrolling, and ONLY when it is scrolling. This means, when it is visually moving. You see, there are delegate methods such as scrollViewDidScroll: but these get called if the contentOffset changes. This doesn’t mean you have observed a visually moving scrollView.

I guess the most obvious use case is when you are hacking a scrollView to allow for “infinite scrolling”. There are many approaches to take when doing such work, but inevitably you’re most likely going to be trying to shift your subviews to different locations and then programmatically reset your contentOffset to contentOffset + contentWidth or some multiple of a constant that’s related to your content, or something like that.

With me so far? All I want is a scrollView that has the following

// KVO Observable
@property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;  

(If you are unfamiliar with KVO, I would say Apple has the fundamental documentation, but if you’re a bit of a blacksmith like me, such documents, although incredibly exact, also put me to sleep. So, I recommend you just google “KVO Objective-C Tutorial” and see what you find.)

So here it is. The problem is, you need to use the scrollView delegate to make it work, but you don’t want to steal the delegate callbacks away from someone who really needs them, so in order to do this, you have to sort of ‘re-route’ the delegate callbacks. (If you’re implementing an infinite scroller, chances are you need to intercept / re-route the delegate anyway because I imagine you’ll create your own dataSource and delegate that are something like (note the conformance to the UIScrollViewDelegate):

@protocol HSCircularScrollViewDelegate<UIScrollViewDelegate>

@optional
- (void)circularScrollView:(HSCircularScrollView*)csv didMoveToItemAtIndex:(NSUInteger)itemIndex;
- (void)circularScrollView:(HSCircularScrollView*)csv didTapOnItem:(UIView*)item atIndex:(NSUInteger)itemIndex;

@end

@protocol HSCircularScrollViewDataSource 

@required
- (NSUInteger)numberOfItemsInCircularView:(HSCircularScrollView*)csv;
- (UIView*)circularScrollView:(HSCircularScrollView*)csv viewForItemAtIndex:(NSUInteger)itemIndex;
@end

But I digress…

Step 1 : Intercept the delegate

In your .m file’s Private Interface (Encapsulate!):

@interface HSCircularScrollView()<UIScrollViewDelegate>  // conform to the delegate
{
    __weak id<UIScrollViewDelegate> _myDelegate;  // the delegate that other calling classes will set.

    BOOL _isSettingContentOffset;  // We will need this later!
}

/* 
   here we make scrolling have a public getter and a private setter.  
   The accessors are automatically synthesized, which is GOOD, because 
   these auto-synthesized methods generate KVO notifications whenever 
   we use the dot notation. i.e. self.scrolling = YES; 
   to change them.  Free functionality!  
*/
@property (nonatomic, assign, getter = isScrolling) BOOL scrolling;  
@end

I also hope to teach a bit of best practices, so please get in the habit of making your views be able to use Interface Builder by supporting the NSCoding protocol:

@implementation HSCircularScrollView 

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupDefaults];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder: aDecoder];
    if (self) {
        [self setupDefaults];
    }
    return self;
}
- (void)setupDefaults
{
    _scrolling = NO; 
    [super setDelegate: self];  // this is where we hijack the delegate.  see the overridden method setDelegate:
}

- (id<UIScrollViewDelegate>)delegate
{
    return _myDelegate;
}

- (void)setDelegate:(id<UIScrollViewDelegate>)aDelegate
{
    //  We are the delegate.  We will always be the delegate.  
    //  We use our delegate callbacks to pass the message along 
    //  to the argument provided to this method
    [super setDelegate:self];  
    
    if (aDelegate != _myDelegate)
    {
        _myDelegate = aDelegate;
    }
}

// ... continued in next section

I have to stop midway here to make a few comments and then talk about the next bit. We’ve now just set up the infrastructure to be able to be the scrollView’s delegate so we can do some further required work of our own, but then can also pass on the delegate’s messages to any external class that needs this scrollView’s delegate functionality. Now that we have this, we can actually proceed to adding the task of knowing when the scroll view is scrolling.

Step 2 : Dealing with contentOffset

A UIScrollView has a contentOffset property that can be set directly, or can be animated. The issue is however, if you set the contentOffset and don’t animate it, there are still messages sent to the delegate’s scrollViewDidScroll: callback. This is semantics. It basically means “scroll view did change its contentOffset”. However, for the purposes of knowing when a scrollView is visually scrolling (i.e. you see content moving across the screen), this method does not provide us with the info we need. We need to distinguish whether the contentOffset has changed because it animated or because it was ‘hard set’.

Now you will see above in the class’ private interface why there is a BOOL _isSettingContentOffset; You need to do this any time your custom code calls:

_isSettingContentOffset = YES;
self.contentOffset = somePoint;

(No, you can’t override setContentOffset and add this YES clause, because the UIScrollView itself calls this method internally as well when it actually is scrolling due to motion.)

Now we have to start implementing our UIScrollViewDelegate methods:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (_isSettingContentOffset) 
    {    
        self.scrolling = NO;
    }
    else
    {
        self.scrolling = YES;
    }
    
    if (_isSettingContentOffset) {
        _isSettingContentOffset = NO;
    }
    
    // check to see if I am my own delegate and then prevent infinite loop.
    if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewDidScroll:)])
    {
        [_myDelegate performSelector:@selector(scrollViewDidScroll:) withObject:self];
    }
}

We’ve now dealt with the unpleasantness associated with contentOffset. Remember that if in your implementation of this subclass you have to programmatically change the contentOffset to change the _isSettingContentOffset = YES;

Step 3 : Complete the Hijacking process and add the scrolling KVO notifications

Now we simply look at the UIScrollViewDelegate methods and add the rest of the scrolling KVO notifications AS WELL AS ensuring all the delegate methods will be re-routed to any ‘real’ delegate.

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (!decelerate) {
        self.scrolling = NO;
    }
    
    // check to see if I am my own delegate and then prevent infinite loop.
    if (_myDelegate != (id<UIScrollViewDelegate>)self &&
        [_myDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)])
    {
        [_myDelegate scrollViewDidEndDragging:self willDecelerate:decelerate];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    self.scrolling = NO;
    
    // check to see if I am my own delegate and then prevent infinite loop.
    if (_myDelegate != (id<UIScrollViewDelegate>)self &&
        [_myDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)])
    {
        [_myDelegate performSelector:@selector(scrollViewDidEndDecelerating:) withObject:self];
    }
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    self.scrolling = NO;
 
    // check to see if I am my own delegate and then prevent infinite loop.
    if (_myDelegate != (id<UIScrollViewDelegate>)self &&
        [_myDelegate respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)])
    {
        [_myDelegate performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:self];
    }
}

// and now for completeness...

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    // check to see if I am my own delegate and then prevent infinite loop.
    if (_myDelegate != (id<UIScrollViewDelegate>)self &&
        [_myDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)])
    {
        [_myDelegate performSelector:@selector(scrollViewWillBeginDragging:) withObject:self];
    }
}

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView   // called on finger up as we are moving
{
    // check to see if I am my own delegate and then prevent infinite loop.
    if (_myDelegate != (id<UIScrollViewDelegate>)self &&
        [_myDelegate respondsToSelector:@selector(scrollViewWillBeginDecelerating:)])
    {
        [_myDelegate performSelector:@selector(scrollViewWillBeginDecelerating:) withObject:self];
    }
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
    // check to see if I am my own delegate and then prevent infinite loop.
    if (_myDelegate != (id<UIScrollViewDelegate>)self &&
        [_myDelegate respondsToSelector:@selector(scrollViewDidScrollToTop:)])
    {
        [_myDelegate performSelector:@selector(scrollViewDidScrollToTop:) withObject:self];
    }
}

Long blog post, but hopefully this will be helpful to you. Create a subclass and add this stuff to it. Try it out by putting one of these in a view controller and have the view controller observe the scrolling property. And have the observe callback set a label’s text to scrolling or not scrolling. Then you’ll see.

Adding Documentation – appledoc

Yes, there have been thousands of posts on this topic. I guess this is more a reference for me, but perhaps it will be useful to you too. That said, this is for Xcode 4.x and Xcode 5 is coming out soon… <sigh>

  • Clone the appledoc repo at:  https://github.com/tomaz/appledoc.git
  • Build the project.  After it completes, in the project navigator, open the Products folder and right-click on appledoc, and Show In Finder.  Copy that file to your /usr/bin folder. (There is some discussion over which folder, read here.  Ultimately it ‘just works’ in /usr/bin)  Do this every time you update the appledoc project.

Documentation is generated in your project by creating a target designed to do this task.

  • Click on your project name at the top of the Project Navigator in Xcode, and you should see the settings screen in the main panel.  Add a Target, of type Aggregate.  Call it Documentation.
  • This target basically needs to do one thing: Run a Script.  So, add a build Phase of type Run Script.  We will come back to this later.

Appledoc can be run completely from a script with command line args, but let’s use a .plist to specify our parameters.  Create a file called AppledocSettings.plist, put it in a folder (relative to your project root) called ‘appledoc’ and copy-paste this into it:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>--company-id</key>
	<string>com.YOUR_COMPANY_NAME</string>
	<key>--create-docset</key>
	<true/>
	<key>--create-html</key>
	<true/>
	<key>--ignore</key>
	<array>
		<string>ThirdParty</string>
		<string>Libraries</string>
		<string>Frameworks</string>
		<string>Testing</string>
	</array>
	<key>--install-docset</key>
	<true/>
	<key>--docset-platform-family</key>
	<string>iphoneos</string>
	<key>--output</key>
	<string>appledoc</string>
	<key>--project-company</key>
	<string>YOUR_COMPANY_NAME</string>
	<key>--project-name</key>
	<string>YOUR_PROJECT_NAME</string>
	<key>--keep-undocumented-objects</key>
	<true/>
	<key>--keep-undocumented-members</key>
	<true/>
	<key>--warn-undocumented-object</key>
	<false/>
	<key>--warn-undocumented-member</key>
	<false/>
	<key>--warn-empty-description</key>
	<true/>
	<key>--warn-missing-arg</key>
	<true/>
	<key>--warn-unknown-directive</key>
	<true/>
	<key>--logformat</key>
	<string>xcode</string>
	<key>--exit-threshold</key>
	<integer>2</integer>
</dict>
</plist>
  • So now that you’ve created that, import it into your Xcode project for easy editing. Don’t add it to any target because it’s just to be visible to us in Xcode, not packaged in any bundle.

You should read up on what the different parameters do, and you can add further parameters that you need by running appledoc –help from the terminal.  Hopefully you should see how these command line args translate to this .plist file.

Go back to the Documentation Target Settings, and now we will provide the script for it to run:

appledoc \
"${PROJECT_DIR}/appledoc/AppledocSettings.plist" \
"${PROJECT_DIR}/PathToSourceFiles"

NOTE: “PathToSourceFiles” is really the path to where you have the source with the documentation you want to have generated.  So this will be specific to your Codebase.

What just happened?  We told appledoc to run with the settings provided in the .plist, and generate documentation for any source located in PathToSourceFiles.  Note in the .plist file above I have the argument “ignore .m” set to YES, since this is private.  If you’ve done your job right (see my posts on the importance of Encapsulation!!) you won’t need to document this source – your comments should hopefully be enough.

  • Build the Documentation target, you’ll see in the Xcode Organizer that your source has been added to the Xcode documentation library.  Awesome!

Actually Writing Documentation

There are two things that will help you writing Documentation formatted correctly for appledoc.  Oliver Drobnik over at Cocoanetics provides an excellent tutorial on this.  (Search for Commenting Correctly).

Using those examples, I would also recommend you create some Code Snippets for Xcode so that you can easily just drop in a template to document a method or a property or whatever.  Here is a link for setting up documentation Code Snippets in Xcode.  With the examples at Cocoanetics, you could create a few snippets for different situations (Method, Property, Class Description, etc) in no time!

I also suggest looking at the AFNetworking Docset in Xcode, and if you want your documentation to look like theirs, just go to that relevant source file to see how they formatted it. AFNetworking is really well documented!

Deleting Docsets

If you look in the appledoc folder where I asked you to place your AppledocSettings.plist, whenever your Documentation is generated, appledoc places a file there telling you where it placed the docset.  You can navigate there and delete any docsets you don’t want to appear in Xcode anymore.  Restart Xcode and this will be up-to-date!

Happy Coding!

UPDATE: Just added some code you can copy-paste to make your own Code Snippets in Xcode. See my github page for that