Conversion between CGFloat and NSNumber without unnecessary promotion to Double
Asked Answered
N

2

6

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!

Nannette answered 15/12, 2015 at 23:44 Comment(0)
N
13

Update: One can cast a CGFloat value to NSNumber and back:

let c1 = CGFloat(12.3)
let num = c1 as NSNumber
let c2 = num as CGFloat

This preserves the precision of CGFloat and works with Swift 2 and Swift 3.


(Previous answer – far too complicated): 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())
    }
Nannette answered 15/12, 2015 at 23:44 Comment(1)
It should be noted that as of April 2017, with Swift 3.1 released, the cast between NSNumber and CGFloat may fail at runtime and will become a checked cast in Swift 4, so the more convoluted solution might be the way to go.Blepharitis
O
0
let num = NSNumber(double: 1.0)
let c: CGFloat? = CGFloat(exactly: num)

Here is the documentation for the initializer.

Oleoresin answered 11/4, 2019 at 20:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.