9

I am using a neat table view controller called SKSTableView in my project which allows each table row to have a number of sub rows. This code works perfectly in 32bit mode but when I run it on my iPhone 5S or in the Simulator in 4-inch 64bit mode, when you tap on a row to get the sub rows it crashes. I do not have any knowledge of the differences of 64bit and 32bit iOS systems. I would love to understand what it happening here.

You will notice that the *SubRowObjectKey is set to void- the error I get is: EXC_BAD_ACCESS_(code=EXC_I386_GPFLT)
Which is a general protection fault trying to access something that is not there(?)

When it crashes Xcode highlights this line of code:
objc_setAssociatedObject(self, SubRowObjectKey, subRowObj, OBJC_ASSOCIATION_ASSIGN);

Also there is this: Printing description of *(subRowObj): (id) [0] = Printing description of SubRowObjectKey:

It seems to be working great in 32bit mode but some how in 64bit it seems to loose where it is. Here below is the section of the code I am looking at.

#pragma mark - NSIndexPath (SKSTableView)
static void *SubRowObjectKey;
@implementation NSIndexPath (SKSTableView)
@dynamic subRow;
- (NSInteger)subRow
{
    id subRowObj = objc_getAssociatedObject(self, SubRowObjectKey);
    return [subRowObj integerValue];
}
- (void)setSubRow:(NSInteger)subRow
{
    if (IsEmpty(@(subRow))) {
        return;
    }
    id subRowObj = @(subRow);
    objc_setAssociatedObject(self, SubRowObjectKey, subRowObj, OBJC_ASSOCIATION_ASSIGN);
}
+ (NSIndexPath *)indexPathForSubRow:(NSInteger)subrow inRow:(NSInteger)row inSection:(NSInteger)section
{
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
    indexPath.subRow = subrow;   
    return indexPath;
}

You can download and play with the code here: https://github.com/sakkaras/SKSTableView

tony.stack
  • 780
  • 8
  • 21

3 Answers3

14

It seems that objc_setAssociatedObject() does not work with "tagged pointers". Tagged pointers are special pointers that do not point to something, but the "value" is stored in (some bits of) the pointer itself. See for example Tagged pointers in Objective-C, which has links to more information.

Tagged pointers are used with 64-bit code only, e.g. for index paths with small row and section numbers, or small number objects.

It can easily be verified that setting an associated object on a tagged pointer crashes with EXC_BAD_ACCESS on iOS 7/64-bit:

static void *key = &key;

id obj = [NSIndexPath indexPathForRow:1 inSection:1];
objc_setAssociatedObject(obj, key, @"345", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// --> EXC_BAD_ACCESS_(code=EXC_I386_GPFLT) 

The same happens with

id obj = @1;
id obj = [NSDate dateWithTimeIntervalSinceReferenceDate:0];

which are also tagged pointers in 64-bit mode.

I could not find any documentation of this restriction and would consider this a bug in iOS. A similar problem was reported here: https://github.com/EmbeddedSources/iAsync/issues/22. I think this is worth a bug report to Apple.

(Remark: The problem does not occur on OS X/64-bit.)


Update (possible solution/workaround): The "SKSTableView" project (link at the end of the question) uses a category on NSIndexPath and associated objects to add a third property "subrow" to an index path. This subrow is set temporarily in

tableView:cellForRowAtIndexPath:

of SKSTableView to pass the current subrow to the

tableView:cellForSubRowAtIndexPath:indexPath

delegate method in the ViewController class.

If you use "proper" three-level index paths instead, the associated object is not needed and the sample app runs in 64-bit mode as well.

I made the following changes to the sample project:

In SKSTableView.m:

+ (NSIndexPath *)indexPathForSubRow:(NSInteger)subrow inRow:(NSInteger)row inSection:(NSInteger)section
{
    // Change:
    //NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
    //indexPath.subRow = subrow;
    // To:
    NSUInteger indexes[] = { section, row, subrow };
    NSIndexPath *indexPath = [NSIndexPath indexPathWithIndexes:indexes length:3];

    return indexPath;
}

The property accessor methods

- (NSInteger)subRow;
- (void)setSubRow:(NSInteger)subRow;

are not needed anymore.

In ViewController.m:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForSubRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"UITableViewCell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (!cell)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

    // Change:
    //cell.textLabel.text = [NSString stringWithFormat:@"%@", self.contents[indexPath.section][indexPath.row][indexPath.subRow]];
    // To:
    cell.textLabel.text = [NSString stringWithFormat:@"%@", self.contents[[indexPath indexAtPosition:0]][[indexPath indexAtPosition:1]][[indexPath indexAtPosition:2]]];

    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    return cell;
}
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Did you enter a bug report using bug reporter? – David H Feb 04 '14 at 20:30
  • @DavidH: Not yet, I just managed to isolate the reason for the crash in OP's project. – Martin R Feb 04 '14 at 20:32
  • Even if objc_setAssociatedObject() did work with tagged pointers, it wouldn't do what you want. In your case, the result would be that every NSIndexPath with that path value would appear to have the same subRow value associated with it. You would be unable to have an NSIndexPath with row=1,section=1,subrow=1 and another NSIndexPath with row=1,section=1,subrow=2. – Greg Parker Feb 04 '14 at 22:21
  • @GregParker: I did not yet understand *how* the subrow property is used in that project, but it seems to work in 32-bit mode. - Is this a known restriction of tagged pointers and objc_setAssociatedObject()? – Martin R Feb 04 '14 at 22:37
  • @MartinR Thanks for the detailed and clear response. I will be implementing these changes in my project and re-testing the app. You can enter the Bug to Apple just to be clear. Thanks again so much! – tony.stack Feb 05 '14 at 15:08
  • This solution doesn't work with `UITableView` category for `NSIndexPath`: got an assert `'Invalid index path for use with UITableView. Index paths passed to table view must contain exactly two indices specifying the section and row. Please use the category on NSIndexPath in UITableView.h if possible.'` – Dmitry Jun 20 '14 at 17:07
  • @Dmitry: I did test the code before posting the solution. Note that the 3-component index path is not passed directly to a UITableView function, but only to the custom `tableView:cellForSubRowAtIndexPath:` method. - But I will double-check it again in the next days when I have the time. – Martin R Jun 20 '14 at 19:48
  • @MartinR The problem is not in `UITableView` methods but in `-[NSIndexPath(UITableView) row]` (in general, not in `SKSTableView`) – Dmitry Jun 23 '14 at 07:30
5

Change setting in below methods:

- (NSInteger)subRow;
- (void)setSubRow:(NSInteger)subRow;

- (NSInteger)subRow
{
    id myclass = [SKSTableView class];

    id subRowObj = objc_getAssociatedObject(myclass, SubRowObjectKey);
    return [subRowObj integerValue];
}

- (void)setSubRow:(NSInteger)subRow
{
    id subRowObj = [NSNumber numberWithInteger:subRow];
    id myclass = [SKSTableView class];
    objc_setAssociatedObject(myclass, nil, subRowObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

Special thanks to @tufnica at https://github.com/sakkaras/SKSTableView/issues/2

Tirupati Balan
  • 674
  • 1
  • 10
  • 29
  • There is only one class object `[SKSTableView class]`. If you use that to store the subRow number then you could also use a global variable (without any associated objects). – Martin R May 18 '15 at 17:02
0

I came across this as well but in a different scenario where I was needing to add a property to a tagged pointer type.

Here is what I came up with; it makes use of NSMapTable weakToStrongObjectsMapTable. It can even be used as a drop-in replacement for objc_setAssociatedObject/objc_getAssociatedObject for tagged pointers and regular object types:

static NSString* SomeVariableKey = @"SomeVariableKey";

@interface NSObject (SomeAddition)

- (void)setSomeVariable:(NSObject*)var {
    [[APObjectAssociationManager defaultManager] setValue:var forKey:SomeVariableKey forAssociatedObject:self];
}

- (NSObject*)someVariable {
    return [[APObjectAssociationManager defaultManager] valueForKey:SomeVariableKey forAssociatedObject:self];
}

@end


@interface APObjectAssociationManager ()

@property (nonatomic, strong) NSMapTable *associations;

@end

@implementation APObjectAssociationManager


- (id)init {
    self = [super init];

    if (self) {
        self.associations = [NSMapTable weakToStrongObjectsMapTable];
    }

    return self;
}

+ (APObjectAssociationManager*)defaultManager {
    static APObjectAssociationManager *instance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[APObjectAssociationManager alloc] init];
    });

    return instance;
}

- (void)setValue:(id)value forKey:(NSString *)key forAssociatedObject:(NSObject*)associatedObject {


    NSMutableDictionary *dictionary = [self.associations objectForKey:associatedObject];

    if (!dictionary) {
        dictionary = [NSMutableDictionary dictionary];
        [self.associations setObject:dictionary forKey:associatedObject];
    }

    [dictionary setValue:value forKey:key];
}

- (id)valueForKey:(NSString *)key forAssociatedObject:(NSObject*)associatedObject {

    NSDictionary *dictionary = [self.associations objectForKey:associatedObject];
    return dictionary[key];
}

@end
Spencer Hall
  • 3,739
  • 1
  • 18
  • 8
  • I like this idea, but wouldn't weak keys be useless when using objects that are tagged pointers? My impression is that tagged pointers will never be considered deallocated and so NSMapTable will never release the values once they are inserted. – John Estropia Jul 14 '15 at 05:13