Search code examples
swiftmapboxgeojson

Store values from an unordered data file


When you parse / loop through a data file that is unordered, how can I store the values into an object or variable for later use?

The actual file is a GeoJSON file with map coordinates for golf hole features, that has many geographic objects defined in it of 2 types (polygons or points). Below is a small sample:

{
  "features": [
    {
      "type": "Feature",
      "properties": {
        "holeno": 19,
        "feature": "teebox"
      },
      "geometry": {
        "coordinates": [
          [
            [
              -1.478163,
              53.869862
            ],
            [
              -1.478122,
              53.869801
            ],
            [
              -1.477888,
              53.869859
            ],
            [
              -1.477927,
              53.869922
            ],
            [
              -1.478163,
              53.869862
            ]
          ]
        ],
        "type": "Polygon"
      },
      "id": "0351f53178c564588a506709c7039509"
    },
    {
      "type": "Feature",
      "properties": {
        "holeno": 15,
        "feature": "pastGreen"
      },
      "geometry": {
        "coordinates": [
          -1.472843,
          53.871483
        ],
        "type": "Point"
      },
      "id": "03850283164d2a7a63d1793baebff719"
    },
    {
      "type": "Feature",
      "properties": {
        "holeno": 8,
        "feature": "teebox"
      },
      "geometry": {
        "coordinates": [
          [
            [
              -1.479439,
              53.875594
            ],
            [
              -1.47972,
              53.875493
            ],
            [
              -1.479667,
              53.875434
            ],
            [
              -1.479363,
              53.87554
            ],
            [
              -1.479439,
              53.875594
            ]
          ]
        ],
        "type": "Polygon"
      },
      "id": "05a1644f6c7d11db4f802fb14b98b8b3"
    }
],
  "type": "FeatureCollection"
}

I would like to be able to store these into classes, variables or objects for later use / processing. But the unordered structure is causing me problems, specifically around the choice to data type and initialisation.

An array has its own index, starting at 0, and you cannot insert at a random number using the insert(at:) function.

A dictionary only stores a single key-value pair. I have 2 map coordinates per hole, plus a minimum of 3 polygons, but possibly more.

A struct has simpler initialisation, but I will only know 1 property's value at the time I would create each struct. I could use Optional values for each property, but what would be a recommended way to complete the instance's initialisation of all properties? How would I check that an instance for a particular hole has already been previously created?

A class has the same issues as a struct, but more complex regarding initialisation.

I am new to and learning Swift, so have I missed or misinterpreted something with the above data types? Is there a means to accomplish the above I may not have heard of?


Solution

  • [Swift 4] Based on your Json structure, you have to design your model classes/struct using Codable. Based on your sample, I found 4 model structs those are enough to map your json to an object.

    First Create RootModel struct which will hold another struct say Feature:

        struct RootModel : Codable {
         let features : [Features]?
         let type : String?
    
         enum CodingKeys: String, CodingKey {
    
         case features = "features"
         case type = "type"
        }
    
        init(from decoder: Decoder) throws {
          let values = try decoder.container(keyedBy: CodingKeys.self)
          features = try values.decodeIfPresent([Features].self, forKey: .features)
          type = try values.decodeIfPresent(String.self, forKey: .type)
        }
    
        }
    

    Now you need the Feature model struct which will holding Properties & Geometry:

    struct Features : Codable {
    let type : String?
    let properties : Properties?
    let geometry : Geometry?
    let id : String?
    
    enum CodingKeys: String, CodingKey {
    
        case type = "type"
        case properties
        case geometry
        case id = "id"
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        type = try values.decodeIfPresent(String.self, forKey: .type)
        properties = try Properties(from: decoder)
        geometry = try Geometry(from: decoder)
        id = try values.decodeIfPresent(String.self, forKey: .id)
    }
    
    }
    

    Geometry & Properties struct:

    struct Properties : Codable {
    let holeno : Int?
    let feature : String?
    ----- similar CodingKeys & init----
    }
    
    struct Geometry : Codable {
    let coordinates : [[[Double]]]?
    let type : String?
    ----- similar CodingKeys & init----
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: Features.CodingKeys.self)
        let geoValues = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .geometry)
        type = try geoValues.decodeIfPresent(String.self, forKey: .type)
        if type == "Point" {
            let pointVal = try geoValues.decodeIfPresent([Double].self, forKey: .coordinates)
            let nestedVal = [[pointVal]]
            coordinates = nestedVal as? [[[Double]]]
        } else {
            coordinates = try geoValues.decodeIfPresent([[[Double]]].self, forKey: .coordinates)
    
        }
    }
    }
    

    Finally, just use the RootModel struct as below:

    let data: Data? = your_json_string.data(using: .utf8)
    let jsonDecoder = JSONDecoder()
    let responseModel = try! jsonDecoder.decode(RootModel.self, from: data!)