19

An NSNumber containing a Bool is easily confused with other types that can be wrapped in the NSNumber class:

NSNumber(bool:true).boolValue // true
NSNumber(integer: 1).boolValue // true
NSNumber(integer: 1) as? Bool // true
NSNumber(bool:true) as? Int // 1

NSNumber(bool:true).isEqualToNumber(1) // true
NSNumber(integer: 1).isEqualToNumber(true) // true

However, information about its original type is retained, as we can see here:

NSNumber(bool:true).objCType.memory == 99 // true
NSNumber(bool:true).dynamicType.className() == "__NSCFBoolean" // true
NSNumber(bool:true).isEqualToValue(true) || NSNumber(bool:true).isEqualToValue(false) //true

The question is: which of these approaches is the best (and/or safest) approach to determining when a Bool has been wrapped within an NSNumber rather than something else? Are all equally valid? Or, is there another, better solution?

sketchyTech
  • 5,746
  • 1
  • 33
  • 56

3 Answers3

32

You can ask the same question for Objective-C, and here is an answer in Objective-C - which you can call from, or translate into, Swift.

NSNumber is toll-free bridged to CFNumberRef, which is another way of saying an NSNumber object is in fact a CFNumber one (and vice-versa). Now CFNumberRef has a specific type for booleans, CFBooleanRef, and this is used when creating a boolean CFNumberRef aka NSNumber *... So all you need to do is check whether your NSNumber * is an instance of CFBooleanRef:

- (BOOL) isBoolNumber:(NSNumber *)num
{
   CFTypeID boolID = CFBooleanGetTypeID(); // the type ID of CFBoolean
   CFTypeID numID = CFGetTypeID((__bridge CFTypeRef)(num)); // the type ID of num
   return numID == boolID;
}

Note: You may notice that NSNumber/CFNumber objects created from booleans are actually pre-defined constant objects; one for YES, one for NO. You may be tempted to rely on this for identification. However, though is currently appears to be true, and is shown in Apple's source code, to our knowledge it is not documented so should not be relied upon.

HTH

Addendum

Swift code translation (by GoodbyeStackOverflow):

func isBoolNumber(num:NSNumber) -> Bool
{
    let boolID = CFBooleanGetTypeID() // the type ID of CFBoolean
    let numID = CFGetTypeID(num) // the type ID of num
    return numID == boolID
}
CRD
  • 52,522
  • 5
  • 70
  • 86
  • I've marked this as the correct answer as you've provided a working solution. Since it's not in Swift, I've edited your response to include the Swift code (just waiting for the peer review of that edit). Thanks. – sketchyTech May 14 '15 at 09:00
  • @GoodbyeStackOverflow - approved your translation for you. – CRD May 14 '15 at 09:11
0

The first one is the correct one.

NSNumber is an Objective-C class. It is built for Objective-C. It stores the type using the type encodings of Objective-C. So in Objctive-C the best solution would be:

number.objCType[0] == @encoding(BOOL)[0] // or string compare, what is not necessary here

This ensures that a change of the type encoding will work after re-compile.

AFAIK you do not have @encoding() in Swift. So you have to use a literal. However, this will not break, because @encoding() is replaced at compile time and changing the encodings would break with compiled code. Unlikely.

The second approach uses a internal identifier. This is likely subject of change.

I think the third approach will have false positives.

Amin Negm-Awad
  • 16,582
  • 3
  • 35
  • 50
  • The problem is that `@encode(BOOL)` and `@encode(signed char)` and `@encode(char)` all return `c` / `99`, (provided `-funsigned-char` was not given to the compiler). – dreamlax May 13 '15 at 13:31
  • This is a property of `NSNumber` that cannot be changed. And I do not see a problem with that. – Amin Negm-Awad May 13 '15 at 13:36
  • Because OP specifically asked about determining whether an `NSNumber` is wrapping a `BOOL` object *rather than something else*. – dreamlax May 13 '15 at 13:38
  • Yes, but look at his third solution. `BOOL`'s have been chars for a long time without problems. Both are still integers. I do not think that this is a problem. – Amin Negm-Awad May 13 '15 at 13:43
0

Don't rely on the class name as it likely belongs to a class cluster, and it is an implementation detail (and therefore subject to change).

Unfortunately, the Objective-C BOOL type was originally a just typedef for a signed char in C, which is always encoded as c (this is the 99 value you are seeing, since c in ASCII is 99).

In modern Objective-C, I believe the BOOL type is an actual Boolean type (i.e. no longer just a typedef for signed char) but for compatibility, it still encodes as c when given to @encode().

So, there's no way to tell whether the 99 originally referred to a signed char or a BOOL, as far as NSNumber is concerned they are the same.

Maybe if you explain why you need to know whether the NSNumber was originally a BOOL, there may be a better solution.

dreamlax
  • 93,976
  • 29
  • 161
  • 209
  • The reason is because when using NSJSONSerialization Bools are imported as NSNumber instances and I need to distinguish between these to deal with JSON in a type safe manner, e.g. only allowing a Bool to change to true or false not 10 or 100, or 999.99, and to also make sure they are transformed back to true and false not 1 and 0 in exported JSON. – sketchyTech May 13 '15 at 13:53
  • When transforming back to JSON, call `-boolValue` on the `NSNumber` to get a 1 or a 0. – Zev Eisenberg May 13 '15 at 14:00
  • 1
    As I understand it from the documentation, boolValue simply provides a bool interpretation of any NSNumber instance: "A 0 value always means false, and any nonzero value is interpreted as true." But I want to maintain the true/false representation and not confuse bools and numbers. – sketchyTech May 13 '15 at 14:26