Martin R Martin R - 3 years ago 329
Swift Question

Conversion between CGFloat and NSNumber without unnecessary promotion to Double

As we all know,

CGFloat
(which is ubiquitous in CoreGraphics, UIKit etc)
can be a 32-bit or 64-bit floating point number, depending on the
processor architecture.

In C,
CGFloat
it is a typealias
to
float
or
double
, in Swift is it defined as a
struct CGFloat
with
a
native
property (which is
Float
or
Double
).

It has been observed repeatedly that a
NSNumber
can be created from
and converted to
Float
and
Double
, but that there exist not
similar conversions from and to
CGFloat
. The general advice
(e.g. in Convert CGFloat to NSNumber in Swift) is to
convert via
Double


CGFloat <--> Double <--> NSNumber


Example:

let c1 = CGFloat(12.3)
let num = NSNumber(double: Double(c1))
let c2 = CGFloat(num.doubleValue)


and that is simple and correct, no precision is lost.
Also most platforms are 64-bit nowadays, and then the
CGFloat/Double

conversion is trivial and probably optimized by the compiler.

However, it aroused my curiosity if a conversion can be done
without promoting
CGFloat
to
Double
on 32-bit platforms.

One could use a build configuration statement (as e.g. in Should conditional compilation be used to cope with difference in CGFloat on different architectures?):

extension NSNumber {
convenience init(cgFloatValue value : CGFloat) {
#if arch(x86_64) || arch(arm64)
self.init(double: value.native)
#else
self.init(float: value.native)
#endif
}
}


But what if Swift is ported to other architectures which are not
Intel or ARM? This does not look very future proof.

One could also use the
CGFLOAT_IS_DOUBLE
constant (as e.g. in
NSNumber from CGFloat):

if CGFLOAT_IS_DOUBLE != 0 {
// ...
} else {
// ...
}


The disadvantage here is that the compiler will always emit a
"Will never be executed" warning on one of the cases.

So to make the long story short:


  • How can we convert between
    CGFloat
    and
    NSNumber
    in a safe way, without compiler warnings,
    and without unnecessary promotion to
    Double
    ?



Please note that this is meant as an "academic" problem. As mentioned
above (and in other Q&A's) one can simply convert via
Double

practically.

I am posting a "self-answer" here in the spirit of
share your knowledge, Q&A-style. Of course other answers are welcome!

Answer Source

There are two solutions that I found. The first uses the toll-free bridging between NSNumber and CFNumber (as in What is most common and correct practice to get a CGFloat from an NSNumber? for Objective-C). It uses the fact that CFNumber has a dedicated conversion mode for CGFloat values:

extension NSNumber {

    // CGFloat -> NSNumber
    class func numberWithCGFloat(var value: CGFloat) -> NSNumber {
        return CFNumberCreate(nil , .CGFloatType, &value)
    }

    // NSNumber -> CGFloat
    var cgFloatValue : CGFloat {
        var value : CGFloat = 0
        CFNumberGetValue(self, .CGFloatType, &value)
        return value
    }
}

That is simple and nice. The only drawback: I could not figure out how to make the constructor an init method instead of a class method.

The second possible solution is a bit longer:

extension NSNumber {

    // CGFloat -> NSNumber
    private convenience init(doubleOrFloat d : Double) {
        self.init(double : d)
    }
    private convenience init(doubleOrFloat f : Float) {
        self.init(float : f)
    }
    convenience init(cgFloat : CGFloat) {
        self.init(doubleOrFloat: cgFloat.native)
    }

    // NSNumber -> CGFloat
    private func doubleOrFloatValue() -> Double {
        return self.doubleValue
    }
    private func doubleOrFloatValue() -> Float {
        return self.floatValue
    }
    var cgFloatValue : CGFloat {
        return CGFloat(floatLiteral: doubleOrFloatValue())
    }
}

There are two private "helper" init methods with the same external parameter name doubleOrFloat but different parameter types. From the actual type of cgFloat.native the compiler determines which one to call in

    convenience init(cgFloat : CGFloat) {
        self.init(doubleOrFloat: cgFloat.native)
    }

Same idea in the accessor method. From the type of self.native the compiler determines which of the two doubleOrFloatValue() methods to call in

    var cgFloatValue : CGFloat {
        return CGFloat(floatLiteral: doubleOrFloatValue())
    }
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download