Is there a correct way to determine that an NSNumber is derived from a Bool using Swift?
Asked Answered
H

3

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?

Hannon answered 13/5, 2015 at 13:4 Comment(0)
G
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
}
Gearhart answered 13/5, 2015 at 19:50 Comment(2)
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.Hannon
@GoodbyeStackOverflow - approved your translation for you.Gearhart
V
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.

Vanesavanessa answered 13/5, 2015 at 13:23 Comment(4)
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).Odometer
This is a property of NSNumber that cannot be changed. And I do not see a problem with that.Vanesavanessa
Because OP specifically asked about determining whether an NSNumber is wrapping a BOOL object rather than something else.Odometer
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.Vanesavanessa
O
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.

Odometer answered 13/5, 2015 at 13:23 Comment(3)
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.Hannon
When transforming back to JSON, call -boolValue on the NSNumber to get a 1 or a 0.Maid
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.Hannon

© 2022 - 2024 — McMap. All rights reserved.