NSAttributedString Color Test

2019-03-28 11:25发布

What is the correct way to compare, or test, for a particular color attribute of an NSAttributed string?

As an example, I'd like to know if the text selection has red text. I have tried several approaches, as you can see below, but none of them ever results in a match. I see the text turn red on screen, and logging the attribute returns: UIDeviceRGBColorSpace 1 0 0 1

- (BOOL)isRedWithAttributes:(NSDictionary *)attributes
{
    BOOL isRedWithAttributes = NO;
    if (attributes != nil)
    {
// if ( [attributes objectForKey:NSForegroundColorAttributeName] == [UIColor redColor] )    
// if ( [attributes objectForKey:NSForegroundColorAttributeName] == @"UIDeviceRGBColorSpace 1 0 0 1" )

       if ( [attributes objectForKey:NSForegroundColorAttributeName] == [UIColor colorWithRed:1 green:0 blue:0 alpha:1] )
       {
            isRedWithAttributes = YES;
        }
        else
        {
            isRedWithAttributes = NO;
        }
    }
    return isRedWithAttributes;
}

Here is how I pass the attribute for the test:

NSAttributedString *selectedString = [attributedString attributedSubstringFromRange:selectedRange];
        [selectedString enumerateAttributesInRange:NSMakeRange(0, [selectedString length]) 
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
                              usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop)
        {
            UIFont *fontFace = [attributes objectForKey:NSFontAttributeName];       
            BOOL isRedWithAttributes = [fontFace isRedWithAttributes:attributes];
            if (isRedWithAttributes)
            {
                NSLog(@"detected red. set selected");
                [self.redButton setSelected:YES];
            }
            else
            {
                NSLog(@"detected not red. do not set selected");
                [self.redButton setSelected:NO];
            }
}

I don't think it matters, but for completeness, here is how I set the text to be red.

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:self.synopsisTextView.attributedText];
[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:selectedRange];
self.synopsisTextView.attributedText = attributedString;

3条回答
Anthone
2楼-- · 2019-03-28 12:15

Objective-C is a strict superset of C. Objective-C objects live on the heap and are kept track of via pointer. The net effect of this is that the test:

[attributes objectForKey:NSForegroundColorAttributeName] == 
[UIColor colorWithRed:1 green:0 blue:0 alpha:1]

tests identity rather than equality. That is, rather than testing whether two objects have the same value you're testing whether they're the same physical object, sitting at the same address in memory. C doesn't allow operator overloading so the == operator has exactly the same meaning in Objective-C as in C. You're comparing two pointers. That they happen to point to Objective-C objects is neither here nor there.

You probably want:

[[attributes objectForKey:NSForegroundColorAttributeName] isEqual:
    [UIColor colorWithRed:1 green:0 blue:0 alpha:1]]

Which will allow the UIColor object to apply whatever test it considers will establish equality.

The only potential caveat is that UIColor considers colours to be equal only if they're defined relative to the same colour space and have the same coefficients. Assuming you're always defining all your colours in RGBA that shouldn't make a difference.

查看更多
Root(大扎)
3楼-- · 2019-03-28 12:25

You would have to create an -isEqualToColor: method for the comparison. UIColor only inherits an -isEqual: from NSObject and this sees two instances of UIColor as equal if their hashes are equal.

Such a method is fairly easy to construct, you just get the RGB+A values independent of the color space and the colors are equal if all of these values are the same.

If you were lazy you'd use CGColorEqualToColor but that does not report equality if one is a gray color (2 components) versus the other is an RGBA color (4 components).

This is probably the reason why Apple hasn't implemented an -isEqualToColor: method because opinions would wildly differ what if the number of components should play a role or whether it is only the RGB values. So I recommend you find your own definition and implement the comparison accordingly.

PS: You might find isEqual working in some cases because UIColor keeps a cache of some standard colors and if e.g. if you request a red then you will get the same instance. The same instance means the same hash and hence "is equal". But as I said before that relies on the implementation is not good style to do.

查看更多
别忘想泡老子
4楼-- · 2019-03-28 12:31

Try testing for the NSForegroundColorAttributeName object in the attributes dict instead of NSFontAttributeName like so:

NSAttributedString *selectedString = [attributedString attributedSubstringFromRange:selectedRange];
[selectedString enumerateAttributesInRange:NSMakeRange(0, [selectedString length]) 
                                   options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired 
                                usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
    UIColor *fgColor = [attributes objectForKey:NSForegroundColorAttributeName];       
    if ([fgColor isEqual:[UIColor redColor]]) {
        NSLog(@"detected red. set selected");
        [self.redButton setSelected:YES];
    } else {
        NSLog(@"detected not red. do not set selected");
        [self.redButton setSelected:NO];
    }
}
查看更多
登录 后发表回答