swiftinitializationscenekitswift4codable

How to extend float3 or any other built-in type to conform to the Codable protocol?


In trying to serialize an array of float3 objects with the basic JSONEncoder, it's revealed that float3 does not conform to the Codable protocol, so this cannot be done.

I tried to write a basic extension as suggested in Encoding and Decoding Custom Types as seen below, but the error 'self' used before all stored properties are initialized is rendered for each of the assignment lines in the init. I assume this is because the compiler is not sure that Float.self is defined prior to the initialization of float3, but I'm not sure how to resolve this.

Further, the end of the init says Return from initializer without initializing all stored properties which I assume means there are float3 properties in addition to x, y, and z, but I'm wondering if there is a way to default/ignore these, and/or how to find the full list of properties aside from digging through all the float3 extensions in simd.

If you have any thoughts about how to go about doing this, sharing them would be much appreciated. Thanks!

import SceneKit

extension float3: Codable {
    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        x = try values.decode(Float.self, forKey: .x)
        y = try values.decode(Float.self, forKey: .y)
        z = try values.decode(Float.self, forKey: .z)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
        try container.encode(z, forKey: .z)
    }

    enum CodingKeys: String, CodingKey {
        case x
        case y
        case z
    }
}

Here is the (I think complete) float3 definitions from the simd file:

/// A vector of three `Float`.  This corresponds to the C and
/// Obj-C type `vector_float3` and the C++ type `simd::float3`.
public struct float3 {

    public var x: Float

    public var y: Float

    public var z: Float

    /// Initialize to the zero vector.
    public init()

    /// Initialize a vector with the specified elements.
    public init(_ x: Float, _ y: Float, _ z: Float)

    /// Initialize a vector with the specified elements.
    public init(x: Float, y: Float, z: Float)

    /// Initialize to a vector with all elements equal to `scalar`.
    public init(_ scalar: Float)

    /// Initialize to a vector with elements taken from `array`.
    ///
    /// - Precondition: `array` must have exactly three elements.
    public init(_ array: [Float])

    /// Access individual elements of the vector via subscript.
    public subscript(index: Int) -> Float
}

extension float3 : Equatable {

    /// True iff every element of lhs is equal to the corresponding element of
    /// rhs.
    public static func ==(lhs: float3, rhs: float3) -> Bool
}

extension float3 : CustomDebugStringConvertible {

    /// Debug string representation
    public var debugDescription: String { get }
}

extension float3 : ExpressibleByArrayLiteral {

    /// Initialize using `arrayLiteral`.
    ///
    /// - Precondition: the array literal must exactly three
    ///   elements.
    public init(arrayLiteral elements: Float...)
}

extension float3 : Collection {

    /// The position of the first element in a nonempty collection.
    ///
    /// If the collection is empty, `startIndex` is equal to `endIndex`.
    public var startIndex: Int { get }

    /// The collection's "past the end" position---that is, the position one
    /// greater than the last valid subscript argument.
    ///
    /// When you need a range that includes the last element of a collection, use
    /// the half-open range operator (`..<`) with `endIndex`. The `..<` operator
    /// creates a range that doesn't include the upper bound, so it's always
    /// safe to use with `endIndex`. For example:
    ///
    ///     let numbers = [10, 20, 30, 40, 50]
    ///     if let index = numbers.index(of: 30) {
    ///         print(numbers[index ..< numbers.endIndex])
    ///     }
    ///     // Prints "[30, 40, 50]"
    ///
    /// If the collection is empty, `endIndex` is equal to `startIndex`.
    public var endIndex: Int { get }

    /// Returns the position immediately after the given index.
    ///
    /// The successor of an index must be well defined. For an index `i` into a
    /// collection `c`, calling `c.index(after: i)` returns the same index every
    /// time.
    ///
    /// - Parameter i: A valid index of the collection. `i` must be less than
    ///   `endIndex`.
    /// - Returns: The index value immediately after `i`.
    public func index(after i: Int) -> Int
}

extension float3 {

    /// Vector (elementwise) sum of `lhs` and `rhs`.
    public static func +(lhs: float3, rhs: float3) -> float3

    /// Vector (elementwise) difference of `lhs` and `rhs`.
    public static func -(lhs: float3, rhs: float3) -> float3

    /// Negation of `rhs`.
    prefix public static func -(rhs: float3) -> float3

    /// Elementwise product of `lhs` and `rhs` (A.k.a. the Hadamard or Schur
    /// vector product).
    public static func *(lhs: float3, rhs: float3) -> float3

    /// Scalar-Vector product.
    public static func *(lhs: Float, rhs: float3) -> float3

    /// Scalar-Vector product.
    public static func *(lhs: float3, rhs: Float) -> float3

    /// Elementwise quotient of `lhs` and `rhs`.
    public static func /(lhs: float3, rhs: float3) -> float3

    /// Divide vector by scalar.
    public static func /(lhs: float3, rhs: Float) -> float3

    /// Add `rhs` to `lhs`.
    public static func +=(lhs: inout float3, rhs: float3)

    /// Subtract `rhs` from `lhs`.
    public static func -=(lhs: inout float3, rhs: float3)

    /// Multiply `lhs` by `rhs` (elementwise).
    public static func *=(lhs: inout float3, rhs: float3)

    /// Divide `lhs` by `rhs` (elementwise).
    public static func /=(lhs: inout float3, rhs: float3)

    /// Scales `lhs` by `rhs`.
    public static func *=(lhs: inout float3, rhs: Float)

    /// Scales `lhs` by `1/rhs`.
    public static func /=(lhs: inout float3, rhs: Float)
}

Solution

  • You can solve the compiler error by instead of trying to directly assign the decoded values to the fields of your type, storing the decoded values in local variables, then calling a designated initializer of float3.

    As Rob mentions in his answer, the cause of the issue has to do with x, y and z being computed properties rather than stored ones, so they cannot be directly written during initialization.

    extension float3: Codable {
        public init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            let x = try values.decode(Float.self, forKey: .x)
            let y = try values.decode(Float.self, forKey: .y)
            let z = try values.decode(Float.self, forKey: .z)
            self.init(x, y, z)
        }
    
        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(x, forKey: .x)
            try container.encode(y, forKey: .y)
            try container.encode(z, forKey: .z)
        }
    
        private enum CodingKeys: String, CodingKey {
            case x,y,z
        }
    }
    

    You can test both encoding and decoding in a Playground using below code:

    let vector = float3(3, 2.4, 1)
    do {
        let encodedVector = try JSONEncoder().encode(vector)
        let jsonVector = String(data: encodedVector, encoding: .utf8) //"{"x":3,"y":2.4000000953674316,"z":1}"
        let decodedVector = try JSONDecoder().decode(float3.self, from: encodedVector) //float3(3.0, 2.4, 1.0)
    } catch {
        print(error)
    }
    

    If you prefer more concise code, the init(from decoder:) method can be shortened to:

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        try self.init(values.decode(Float.self, forKey: .x), values.decode(Float.self, forKey: .y), values.decode(Float.self, forKey: .z))
    }