Autolayout and Self-sizing UITableViewCell

I’m not going to lie.  Autolayout is a massive pain.  But.  Ultimately it’s very powerful and you’re best to just go through the pain and learn it.

Even so, you should also get a bit familiar with it, then learn about the concept of self-sizing table view cells.  It’s quite important.  Basically, as long as there is a clearly defined way for a UITableViewCell to determine its own height via the auto-layout constraints, dynamic table view cells are pretty easy.

Unfortunately, there is a lot to learn:

This series

Then this

or even Apple

Quick Reference:

How to make sure a UITableViewCell autosizes when you have a label that you want to wrap around onto multiple lines.

1. Pin the Label at top left, bottom, right.

2. Edit the right constraint to be “Greater than or Equal to”, and then set the constant to be the right-most you want to allow that (probably view margin).

3. Set that right-most constraint’s priority to 750 (high), then set (on the Label!) it’s content compression resistance priority to 749.

Should be fine now.

DataSource object for UITableView / UICollectionView in Swift with Core Data

It’s amazing how quickly I now have become a part of the Swift Fan Club.  I recently worked on some old Objective-C code of mine and was amazed how quickly one learns to stop typing semi-colons.  🙂

Today’s post is all about a pattern I use more often in my projects, and it’s one that prefers composition over inheritance.  All that really means in this case is that on any UITableViewController (or similarly, UICollectionViewController), I prefer to create separate Data Source objects that keep all that code separate from the View Controller itself.  (I personally don’t find that MVC stands for Massive View Controller if you don’t let it.)

The issue here is that I pretty much don’t do projects any more without using Core Data.  It is the best solution in my opinion because of the code you oftentimes *don’t* have to write.  Also, with NSFetchedResultsController, I like how you can further separate your data layer (think Networking and importing) from you View Controllers.  View Controllers concern themselves with *what* they want to display, and not with how it is acquired.

Anyway, the strictly typed language of Swift sometimes makes old approaches not straightforward, and I would like to share what I determined today.  It will become a staple in my future Swift projects.

I create a DataSource class that takes a generic type, so that this generic type can be used for Core Data related activities.  By default, a NSFetchedResultsController also takes a generic type of NSFetchRequestResult. But sometimes that is simply not enough. More on this later. To even make a Generic Data source, we have:

class BasicFetchedResultsDataSource: NSObject, NSFetchedResultsControllerDelegate where T:NSManagedObject {
    
    let managedObjectContext: NSManagedObjectContext!
    let tableView: UITableView!
    
    init(context: NSManagedObjectContext!, tableView: UITableView!) {
        
        self.managedObjectContext = context
        self.tableView = tableView
    }
    
    private var _fetchedResultsController: NSFetchedResultsController? = nil
    var fetchedResultsController: NSFetchedResultsController {
        
        if _fetchedResultsController != nil {
            return _fetchedResultsController!
        }
        
        let request = T.fetchRequest()
        request.predicate = self.searchPredicateForFetchRequest
        request.sortDescriptors = self.sortDescriptorsForFetchRequest
        let controller = NSFetchedResultsController(fetchRequest: request as! NSFetchRequest,
                                                    managedObjectContext: self.managedObjectContext,
                                                    sectionNameKeyPath: self.sectionNameKeyPath,
                                                    cacheName: self.resultsControllerCacheName)
        
        controller.delegate = self
        _fetchedResultsController = controller
        return _fetchedResultsController!
    }
    
    
    func updateRequestAndFetch() throws {
        
        self.fetchedResultsController.fetchRequest.predicate = self.searchPredicateForFetchRequest
        self.fetchedResultsController.fetchRequest.sortDescriptors = self.sortDescriptorsForFetchRequest
        
        do {
            try self.fetchedResultsController.performFetch()
            
            self.tableView.reloadData()
        }
        catch {
            throw error
        }
    }
    
    // allows your subclass to override and change this
    var sectionNameKeyPath: String? {
        return nil
    }
    
    // allows your subclass to override and change this
    var resultsControllerCacheName: String? {
        return nil
    }
    
    // allows your subclass to override and change this according to state
    var sortDescriptorsForFetchRequest: [NSSortDescriptor]! {
        return []
    }
    
    // allows your subclass to override and change this according to state
    var searchPredicateForFetchRequest: NSPredicate? {
        return nil
    }
    
    // ... Typical NSFetchedResultsController and UITableViewDataSource code here.
}

That’s it for the basics, but what if my data model is a bit more interesting? In my current project I want my data to be sortable, filterable, searchable, and possibly groupable.

So I define the following:

import CoreData
@objc protocol Sortable: NSFetchRequestResult {
    static func defaultSortDescriptors() -> [NSSortDescriptor]!
}

@objc protocol Groupable: NSFetchRequestResult {
    var groupIndex: String! { get }
}

@objc protocol RelationshipFilterable: NSFetchRequestResult {
    static func relationshipFilterPredicate(for constraintObject:NSManagedObject?) -> NSPredicate?
}

@objc protocol TextSearchable: NSFetchRequestResult {
    static func searchPredicate(for searchTerm:String?) -> NSPredicate?
}

@objc protocol MyGenericDataObject: Sortable, Groupable, RelationshipFilterable, TextSearchable {
    // combines them
}

Then the cool stuff. Subclass the Basic view controller above:

class GenericFetchedResultsDataSource: BasicFetchedResultsDataSource where T:NSManagedObject {
    
    let allowsGrouping: Bool
    let allowsTextSearching: Bool
    
    override init(context: NSManagedObjectContext!, tableView: UITableView!) {
        self.allowsGrouping = true
        self.allowsTextSearching = true
        super.init(context: context, tableView: tableView)
    }
    
    var currentSearchTerm: String? {
        didSet {
            if self.allowsTextSearching {
                do {
                    try self.updateRequestAndFetch()
                }
                catch {
                    print("Fetch Error: \(error)")
                }
            }
        }
    }
    
    override var sectionNameKeyPath: String? {
        return self.allowsGrouping ? #keyPath(MyGenericDataObject.groupIndex) : nil
    }
    
    override var resultsControllerCacheName: String? {
        return nil
    }
    
    override var sortDescriptorsForFetchRequest: [NSSortDescriptor]! {
        return T.defaultSortDescriptors()
    }
    
    override var searchPredicateForFetchRequest: NSPredicate? {
        return self.allowsTextSearching ? T.searchPredicate(for: self.currentSearchTerm) : nil
    }
}

And that’s how you can work with generics and their subclasses. It’s why protocol oriented programming and swift go together nicely!

UINavigationBar that Scrolls Away (Revised)

On this blog one of my most searched for posts is the one about a UINavigationBar that scrolls away.

That was written a while back.  I’ve recently had to revisit this topic again, so I thought I’d revise my implementation, which aims to remove many caveats and just make it easy to drop into your code and you’re done.

So I redid that code and made it slightly less hacky, and implemented it as a category on UIVIewController. If it doesn’t work for you the only thing I can think of are the new properties on UIViewController that pertain to layout. Mine had automaticallyAdjustScrollViewInsets to YES, and extend edges under top bars.

//  UIViewController+ScrollyNavBar.h
//
//  Created by Stephen O'Connor on 24/03/16.
//  MIT License
//

/*
 
 How to use this:
 
 import this into your (probably) table view controller
 
 make sure you set HS_navigationBarLayerDefaultPosition in viewDidLoad:
 
 self.HS_navigationBarLayerDefaultPosition = self.navigationController.navigationBar.layer.position;
 
 optionally set HS_scrollingNavigationBarThresholdHeight if you want to be able to scroll a bit before 
 the nav bar starts scrolling with it.  A typical use case would be if you want the bar to start
 scrolling with your first table view section, and not with the tableViewHeader.
 
 in viewWillAppear:, call:
 
 - (void)HS_updateNavigationBarPositionUsingScrollView:(UIScrollView*)scroller
 
 and, assuming your table view controller is still the UITableView's delegate, it's also
 a scroll view delegate.  In -scrollViewDidScroll:, call:
 
 - (void)HS_updateNavigationBarPositionUsingScrollView:(UIScrollView*)scroller
 
 as well.
 
 Done!
 
 
 */


#import <UIKit/UIKit.h>

@interface UIViewController (ScrollyNavBar)

@property (nonatomic, assign) CGFloat HS_scrollingNavigationBarThresholdHeight;  // defaults to 0.  I.e. think about tableViewHeader's height
@property (nonatomic, assign) CGPoint HS_navigationBarLayerDefaultPosition;

- (void)HS_updateNavigationBarPositionUsingScrollView:(UIScrollView*)scroller;

@end

Then then .m file:

// some theoretical knowledge here:  http://nshipster.com/associated-objects/

#import "UIViewController+ScrollyNavBar.h"
#import <objc/runtime.h>


@implementation UIViewController (ScrollyNavBar)

@dynamic HS_scrollingNavigationBarThresholdHeight;
@dynamic HS_navigationBarLayerDefaultPosition;

- (void)setHS_navigationBarLayerDefaultPosition:(CGPoint)HS_navigationBarLayerDefaultPosition {
    objc_setAssociatedObject(self,
                             @selector(HS_navigationBarLayerDefaultPosition),
                             [NSValue valueWithCGPoint:HS_navigationBarLayerDefaultPosition],
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (CGPoint)HS_navigationBarLayerDefaultPosition {
    return [(NSValue*)objc_getAssociatedObject(self, @selector(HS_navigationBarLayerDefaultPosition)) CGPointValue];
}

- (void)setHS_scrollingNavigationBarThresholdHeight:(CGFloat)HS_scrollingNavigationBarThresholdHeight {
    objc_setAssociatedObject(self,
                             @selector(HS_scrollingNavigationBarThresholdHeight),
                             @(HS_scrollingNavigationBarThresholdHeight),
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (CGFloat)HS_scrollingNavigationBarThresholdHeight {
    return [(NSNumber*)objc_getAssociatedObject(self, @selector(HS_scrollingNavigationBarThresholdHeight)) floatValue];
}

- (void)HS_updateNavigationBarPositionUsingScrollView:(UIScrollView*)scroller
{
    // get the navigation bar's underlying CALayer object
    CALayer *layer = self.navigationController.navigationBar.layer;
    CGFloat contentOffsetY = scroller.contentOffset.y;
    CGPoint defaultBarPosition = self.HS_navigationBarLayerDefaultPosition;
    CGFloat scrollingThresholdHeight = self.HS_scrollingNavigationBarThresholdHeight;
    
    // if the scrolling is not at the top and has passed the threshold, then set the navigationBar layer's position accordingly.
    if (contentOffsetY > -scroller.contentInset.top + scrollingThresholdHeight) {
        
        CGPoint newPos;
        newPos.x = layer.position.x;
        newPos.y = defaultBarPosition.y;
        newPos.y = newPos.y - MIN(contentOffsetY + scroller.contentInset.top - scrollingThresholdHeight, scroller.contentInset.top);
        
        layer.position = newPos;
        
    }
    else
    {
        layer.position = defaultBarPosition;  // otherwise we are at the top and the navigation bar should be seen as if it can't scroll away.
    }
}

@end

Boom! Pretty straightforward.

Note however, that I don’t really recommend this functionality because it’s prone to error, and it’s sort of hacking UINavigationBar.  It has a few related consequences:

  • UITableView section headers will still ‘stick’ to the bottom of the now invisible navigation bar.
  • You must use a translucent UINavigationBar for this to really work properly.

Now, for the first of those two points, I don’t have a solution and anyone who does, please leave me something in the comments.

For the second one, if you read the API docs of UINavigationBar closely, you’ll see:

@property(nonatomic, assign, getter=isTranslucent) BOOL translucent

Discussion

The default value is YES. If the navigation bar has a custom background image, the default is YES if any pixel of the image has an alpha value of less than 1.0, and NO otherwise.

So, with a little trickery, we can just change the Class on a UINavigationController to use a custom UINavigationBar class that sets an *almost* opaque image as the background image any time you change the barTintColor:


#import "UIImage+NotQuiteOpaque.h"

@interface HSNavigationBar : UINavigationBar
@end

@implementation HSNavigationBar

- (void)awakeFromNib
{
    [self setBarTintColor:self.barTintColor];
}

- (void)setTranslucent:(BOOL)translucent
{
    NSLog(@"This HSNavigationBar must remain translucent!");
    [super setTranslucent:YES];
}

- (void)setBarTintColor:(UIColor *)barTintColor
{
    UIImage *bgImage = [UIImage HS_stretchableImageWithColor:barTintColor];
    [self setBackgroundImage:bgImage forBarMetrics:UIBarMetricsDefault];
}

@end

And you’ll see here that the real trick is this category on UIImage that generates a stretchable image of the color specified, but it sets the top right corner’s pixel alpha value to 0.99. I assume that nobody will notice that slight translucency in one pixel at the top right of the screen.

@implementation UIImage (NotQuiteOpaque)

+ (UIImage*)HS_stretchableImageWithColor:(UIColor *)color
{
    
    UIImage *result;
    
    CGSize size = {5,5};
    CGFloat scale = 1;
    
    CGFloat width = size.width * scale;
    CGFloat height = size.height * scale;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    
    size_t bitsPerComponent = 8;
    size_t bytesPerPixel    = 4;
    size_t bytesPerRow      = (width * bitsPerComponent * bytesPerPixel + 7) / 8;
    size_t dataSize         = bytesPerRow * height;
    
    unsigned char *data = malloc(dataSize);
    memset(data, 0, dataSize);
    
    CGContextRef context = CGBitmapContextCreate(data, width, height,
                                                 bitsPerComponent,
                                                 bytesPerRow, colorSpace,
                                                 (CGBitmapInfo)kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    
    CGFloat r, g, b, a;
    UIColor *colorAtPixel = color;
    
    r = 0, g = 0, b = 0, a = 0;
    
    for (int x = 0; x < (int)width; x++)
    {
        for (int y = 0; y < (int)height; y++)
        {
            if (x == (int)width - 1 && y == 0) {
                colorAtPixel = [color colorWithAlphaComponent:0.99f];  // top right
            }
            
            [colorAtPixel getRed:&r green:&g blue:&b alpha:&a];
            
            
            int byteIndex = (int)((bytesPerRow * y) + x * bytesPerPixel);
            data[byteIndex + 0] = (int)roundf(r * 255);    // R
            data[byteIndex + 1] = (int)roundf(g * 255);  // G
            data[byteIndex + 2] = (int)roundf(b * 255);  // B
            data[byteIndex + 3] = (int)roundf(a * 255);  // A
            
            colorAtPixel = color;
        }
    }
    
    CGColorSpaceRelease(colorSpace);
    CGImageRef imageRef = CGBitmapContextCreateImage(context);
    result = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
    CGImageRelease(imageRef);
    CGContextRelease(context);
    free(data);

    result = [result resizableImageWithCapInsets:UIEdgeInsetsMake(2, 2, 2, 2) resizingMode:UIImageResizingModeStretch];
    
    return result;
    
}

@end

So, there it is. It works, yes, but not perfectly, and the whole thing feels a bit hacky. I wonder if there’s a better way to do this, or if in the future Apple will start to update this code. Seeing as we see collapsing address bars on WebView controllers like in Safari, perhaps they might abstract this further to support UINavigationControllers.

Stretchy Header UITableView (and how I hate hacks)

I really dislike using a UIViewController subclass to make fundamental functionality of a UIView work.  That said, I will do what needs doing to get the job done.  Sometimes there are no other obvious ways to do something non-typical.

Recently I looked for code that could help me recreate “stretchy header” functionality.  And I found some here.  It wasn’t a bad approach.  It worked.  But somehow seemed a bit brittle, or for a very specific use-case.  It was adjusting contentInset depending on contentOffset value.  That seemed sub-optimal.

I’m currently working on a UITableViewController subclass that should be configurable to have stretchy headers, expandable section headers (see previous post), and scrollable navigation bars (also a post of mine from 2012… will finally (soon) get a fresh coat of paint).

Anyway, I found the easiest way to make stretchy header views.  Simply, you have to subclass UITableView and make sure your UITableViewController subclass uses an instance of it and not of a standard UITableView.  The advantage here is that you don’t need to change any view controller code, so you don’t have issues associated with class inheritance.

Enough talk.  Here’s the code.  You can try it for yourself.

//  HSStretchyHeaderTableView.h
//  TableViewSandbox
//
//  Created by Stephen O'Connor on 23/03/16.
//  MIT License.
//

#import <UIKit/UIKit.h>

IB_DESIGNABLE
@interface HSStretchyHeaderTableView : UITableView

@property (nonatomic, assign, getter=hasStretchyHeader) IBInspectable BOOL stretchyHeader;

@end

And now the .m file:

#import "HSStretchyHeaderTableView.h"

@interface HSStretchyHeaderTableView()
{
    CGFloat _headerViewHeight;
}
@end

@implementation HSStretchyHeaderTableView

- (void)awakeFromNib
{
    // NOTE, You will have to modify this solution if you don't use InterfaceBuilder!
    _headerViewHeight = self.tableHeaderView.bounds.size.height;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    
    if (self.hasStretchyHeader)
    {
        CGRect headerFrame = self.tableHeaderView.frame;
        
        if (self.contentOffset.y  _headerViewHeight)
        {
            headerFrame.origin.y = 0;
            headerFrame.size.height = _headerViewHeight;
            self.tableHeaderView.frame = headerFrame;
        }
    }
}

@end

Pretty easy! Happy coding!

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:

Custom UITableViewHeaderFooterView with Interface Builder

So, one area that Apple needs to tighten up as it’s not clear and the Internet is also a bit divided on is setting up UITableViewHeaderFooterView objects in Interface Builder. Now, sadly you can’t do this in a table view in a Storyboard much like you’d design a prototype cell. No, but there are still ways to visually design these and write minimal code.

So you will have to subclass UITableViewHeaderFooterView and override -(UIView*)contentView. That’s the jist of it. You probably have to subclass it anyway because you have custom content!

@implementation MySettingsHeaderView
- (UIView*)contentView
{
  return self.subviews[0];
}
@end

And then create a View NIB file, where the root view’s class is set to this above. The trick is that the first subview is going to be the contentView of the UITableViewHeaderFooterView, but strangely it is a readonly property. So that’s why you need to override.

Custom UITableViewHeaderFooterView in IB

So treat this view as the contentView and add everything to that. This way you can preserve AutoLayout stuff and avoid warnings like:

Setting the background color on UITableViewHeaderFooterView has been deprecated. Please use contentView.backgroundColor instead

Then, all you do is in your viewDidLoad method, you put something like:

[self.tableView registerNib:[UINib nibWithNibName:@"MySettingsHeaderView" bundle:nil] forHeaderFooterViewReuseIdentifier:@"MyHeaderIdentifier"]; // though, be a good coder, and don't pass a hard-coded string, but define a constant for your header identifier!!

Boom! I love you too.

Core Data and UITableView – Re-ordering Rows Magic

This is as much for my reference as it is for yours. I have a situation where I needed to re-order rows in a UITableView. The models in table view are part of a Core Data to-many relationship, so I have an attribute on the model called position.

Full disclosure: I am basically re-hashing this post on stack overflow.

This is basically what was causing me problems: “NSFetchedResultsController and its delegate are not meant to be used for user-driven model changes.” and I was trying to re-assign position values, which would in turn update the managedObjectContext, which would in turn update the NSFetchedResultsController and there would be a lot of mayhem and incorrectly set data.

To make this brief, basically on any UITableViewController, or any UIViewController that implements UITableViewDataSource and UITableViewDelegate along with NSFetchedResultsControllerDelegate, add a private property called:

@property (nonatomic, assign, getter=isManuallyReordering) BOOL manuallyReordering;

and use it in your delegate methods as such:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
 self.manuallyReordering = YES;

 //...[UPDATE THE MODEL then SAVE CONTEXT]...

 self.manuallyReordering = NO;
}

and in this time ignore change messages sent to the NSFetchedResultsControllerDelegate

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
 if (self.isManuallyReordering) return;
 //...
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
 if (self.isManuallyReordering) return;
 //...
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
 if (self.isManuallyReordering) return;
 //...
}

There it is.