10

I'm working on building a custom file opener in iOS Swift for shapefiles (a GIS format, not particularly relevant to this question). These files have a header which is 100 bytes long. I'm able to read this into 4-byte arrays, which store information I want. I can convert these arrays into the Swift types Data and NSData, and have a few other options for transforming them (like Base64EncodedString). But I'm having trouble converting these raw arrays or the Data or any of the formats into useful attributes like Double, Int, and String.

import Foundation
    struct ShapeReader {
        var shapeFile = FileHandle(forReadingAtPath: "/Users/christopherjlowrie/Documents/Shapes/SF_Neighborhoods/Planning_Zones.shp")
        var fileHeader: String{
            let header = shapeFile?.readData(ofLength: 100)
            let headerStream = InputStream(data: header!)
            headerStream.open()
            var buffer = [UInt8](repeating: 0, count: 4)
            while (headerStream.hasBytesAvailable){
                headerStream.read(&buffer, maxLength: buffer.count)
                print(buffer)
                let x = Data(buffer)
                print(x)
        }
        return "A"
    }
}

This currently only returns A because for testing reasons I am having it return a string

How can I open files, and read their raw bytes into types (Doubles, Ints, Strings) in Swift?

Leo Dabus
  • 198,248
  • 51
  • 423
  • 494
Chris Lowrie
  • 111
  • 1
  • 5

1 Answers1

21

Xcode 11 • Swift 5.1 or later

To convert from String or any Numeric type to Data:

extension StringProtocol {
    var data: Data { .init(utf8) }
}

extension Numeric {
    var data: Data {
        var source = self
        // This will return 1 byte for 8-bit, 2 bytes for 16-bit, 4 bytes for 32-bit and 8 bytes for 64-bit binary integers. For floating point types it will return 4 bytes for single-precision, 8 bytes for double-precision and 16 bytes for extended precision.
        return .init(bytes: &source, count: MemoryLayout<Self>.size)
    }
}

To convert from Data (bytes) back to String

extension DataProtocol {
    var string: String? { String(bytes: self, encoding: .utf8) }
}

To convert from Data back to generic Numeric value

extension Numeric {
    init<D: DataProtocol>(_ data: D) {
        var value: Self = .zero
        let size = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
        assert(size == MemoryLayout.size(ofValue: value))
        self = value
    }
}

extension DataProtocol {
    func value<N: Numeric>() -> N { .init(self) }
}

let value = 12.34                      // implicit Double 12.34
let data = value.data                  // double data - 8 bytes
let double = Double(data)              // implicit Double 12.34
let double1: Double = .init(data)      // explicit Double 12.34
let double2: Double = data.value()     // explicit Double 12.34
let double3 = data.value() as Double   // casting to Double 12.34

Now we can easily add a property for each Numeric type:

extension DataProtocol {
    var integer: Int { value() }
    var int32: Int32 { value() }
    var float: Float { value() }
    var cgFloat: CGFloat { value() }
    var float80: Float80 { value() }
    var double: Double { value() }
    var decimal: Decimal { value() }
}

Playground testing

let intData = 1_234_567_890_123_456_789.data    // 8 bytes (64 bit Integer)
let dataToInt: Int = intData.integer                 // 1234567890123456789

let intMinData = Int.min.data                   // 8 bytes (64 bit Integer)
let backToIntMin = intMinData.integer           // -9223372036854775808

let intMaxData = Int.max.data                   // 8 bytes (64 bit Integer)
let backToIntMax = intMaxData.integer           // 9223372036854775807

let myInt32Data = Int32(1_234_567_890).data     // 4 bytes (32 bit Integer)
let backToInt32 = myInt32Data.int32             // 1234567890

let int32MinData = Int32.min.data               // 4 bytes (32 bit Integer)
let backToInt32Min = int32MinData.int32         // -2147483648

let int32MaxData = Int32.max.data               // 4 bytes (32 bit Integer)
let backToInt32Max = int32MaxData.int32         // 2147483647

let myFloatData = Float.pi.data                 // 4 bytes (32 bit single=precison FloatingPoint)
let backToFloat = myFloatData.float             // 3.141593
backToFloat == .pi      // true

let myCGFloatData = CGFloat.pi.data                 // 4 bytes (32 bit single=precison FloatingPoint)
let backToCGFloat = myCGFloatData.cgFloat             // 3.141593
backToCGFloat == .pi      // true

let myDoubleData = Double.pi.data               // 8 bytes (64 bit double-precision FloatingPoint)
let backToDouble = myDoubleData.double          // 3.141592653589793
backToDouble == .pi     // true

let myFloat80Data = Float80.pi.data             // 16 bytes (128 bit extended-precision FloatingPoint)
let backToFloat80 = myFloat80Data.float80       // 3.141592653589793116
backToFloat80 == .pi    // true

let decimalData = Decimal.pi.data             // 20 bytes Decimal type
let backToDecimal = decimalData.decimal       // 3.14159265358979323846264338327950288419
backToDecimal == .pi    // true

let stringBytes = "Hello World !!!".data.prefix(4)  // 4 bytes
let backToString = stringBytes.string               //  "Hell"
Leo Dabus
  • 198,248
  • 51
  • 423
  • 494
  • Followup Questions: The conversion to double doesn't work as intended. Instead of return doubles around 100, the return values are tiny (on the scale of e-325). This might have something to do with Big Endian/ Little Endian. Do you have any solutions or places I might start looking for this? – Chris Lowrie Apr 07 '17 at 20:43
  • @ChrisLowrie Can you post the data input, the type and expected output? are you sure your input is double 64bit (8bites) and the output is double also ? in you have only 4 bytes just create a float and initialize a double from it. `Double(float)` – Leo Dabus Apr 07 '17 at 21:53
  • I have tested the double extension and it does work as expected. you need to show your actual code otherwise I would be just guessing – Leo Dabus Apr 07 '17 at 22:33
  • @ChrisLowrie yes, endianness is source of your trouble, if you don't have an access to ESRI documentation, check at least https://en.wikipedia.org/wiki/Shapefile – user3441734 Apr 14 '17 at 18:42
  • This might crash with `Fatal error: load from misaligned raw pointer`. See https://stackoverflow.com/questions/38023838/round-trip-swift-number-types-to-from-data/38024025#38024025 for more info. – Klaas Jun 01 '19 at 08:44
  • 1
    @Klaas it depends on how you are accessing the bytes of your data. You were probably accessing the bytes through subscript. You can use Data's subdata method to avoid this issue. https://stackoverflow.com/a/47530223/2303865 – Leo Dabus Feb 29 '20 at 21:39