'Build a json array from multiple properties of a custom type in Swift

After being pointed towards using Codable to format json in Swift, I am trying to add multiple results to a json array. I am unfortunately struggling to work out how to build this json array for a set of results that contains multiple properties.

Here is an example of my required json array for two 'sets' of results (N.B. the number of sets can vary in the data recorded):

[{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"orientation_forward","value":"2"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"start_forward","value":"3.45"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"finish_forward","value":"5.29"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"minimum_forward","value":"7.81"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"maximum_forward","value":"9.20"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"range_forward","value":"2.39"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"orientation_forward","value":"1"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"start_forward","value":"1.89"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"finish_forward","value":"4.20"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"minimum_forward","value":"8.26"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"maximum_forward","value":"46.82"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"range_forward","value":"2.98"}]

And below is my Swift code. The results arrive as an argument within an array of custom type ORKRangeOfMotionResult (from ResearchKit with each set of results giving me a value for orientation, start, finish, minumum, maximum and range properties). Each set of ORKRangeOfMotionResult is also accompanied by an identifier (variableIdentifier), pulled in as another result that I can use to ensure I know which set is which.

I am currently using a loop to extract individual values for each ORKRangeOfMotionResult property. After this, it is relatively easy to add one result to the json array [items] using the variableIdentifier in if statements (as can be seen below for start) but I don't know how to add all properties from a set of results to the array. I'm guessing a loop, but haven't figured out how as yet.

func buildJsonArrayOfResults(_ rangeOfMotionResults: [ORKRangeOfMotionResult], withIdentifier variableIdentifiers: [String]) {
    let record = getIDNumber().stringValue
    let repeat_instance = getRepeatInstance().stringValue
    let repeat_instrument = "range_of_motion_result"
    let event = getEventName()
    var fieldName: String?
    var identifier: String?
    var stringData: String?
    var value: String?
    var items: [Item] = []
        
    struct Item: Codable {
        let record: String
        let fieldName: String
        let repeatInstance: String
        let repeatInstrument: String
        let value: String
        let event: String

        enum CodingKeys: String, CodingKey {
            case record
            case fieldName = "field_name"
            case repeatInstance = "redcap_repeat_instance"
            case repeatInstrument = "redcap_repeat_instrument"
            case value
            case event = "redcap_event_name"
        }
    }
        
    var setNumber: Int = 0
    while setNumber < rangeOfMotionResults.count {
        setNumber += 1
            
        // obtain results for each set of movements as a string
        let orientation = String(rangeOfMotionResults[setNumber - 1].orientation)
        let start = String(rangeOfMotionResults[setNumber - 1].start)
        let finish = String(rangeOfMotionResults[setNumber - 1].finish)
        let minimum = String(rangeOfMotionResults[setNumber - 1].minimum)
        let maximum = String(rangeOfMotionResults[setNumber - 1].maximum)
        let range = String(rangeOfMotionResults[setNumber - 1].range)
            
        // assign one value and its redcap field name
        identifier = variableIdentifiers[setNumber - 1]
        if identifier!.contains("forward") {
            fieldName = "start_forward"
            value = start
        } else if identifier!.contains("backward") {
            fieldName = "start_backward"
            value = start
        } else if identifier!.contains("left.bending") {
            fieldName = "start_left_bending"
            value = start
        } else if identifier!.contains("right.bending") {
            fieldName = "start_right_bending"
            value = start
        } else if identifier!.contains("left.rotation") {
            fieldName = "start_left_rotation"
            value = start
        } else if identifier!.contains("right.rotation") {
            fieldName = "start_left_rotation"
            value = start
        }
            
        let item = Item(record: record, fieldName: fieldName!, repeatInstance: String(repeat_instance), repeatInstrument: repeat_instrument, value: value!, event: event)
            
        items.append(item)
    }

    do {
        let data = try JSONEncoder().encode(items)
        stringData = String(data: data, encoding: .utf8)
        print(stringData!)
    } catch {
        print(error)
    }

    // now do something with stringData

}

All hep graciously received. Thank you in advance.



Solution 1:[1]

According to the given scenario you need only these properties

func buildJsonArrayOfResults(_ rangeOfMotionResults: [ORKRangeOfMotionResult], withIdentifier variableIdentifiers: [String]) {
    let record = getIDNumber().stringValue
    let repeatInstance = getRepeatInstance().stringValue
    let repeatInstrument = "range_of_motion_result"
    let event = getEventName()
    var items: [Item] = []

    struct Item: Codable { ... }

And as the order of the identifiers match the order of the result array the recommended way to enumerate an array by index and element is enumerated

    for (index, result) in rangeOfMotionResults.enumerated() {
        let identifier = variableIdentifiers[index]

and it seems that finish, minimum, maximum and range are unused

       // obtain results for each set of movements as a string
        let orientation = String(result.orientation)
        let value = String(result.start)       

a switch expression is swiftier than an if - else chain

        let fieldName : String

        switch identifier {
            case let id where id.contains("forward"): fieldName = "start_forward"
            case let id where id.contains("backward"): fieldName = "start_backward"
            case let id where id.contains("left.bending"): fieldName = "start_left_bending"
            case let id where id.contains("right.bending"): fieldName = "start_right_bending"
            case let id where id.contains("left.rotation"): fieldName = "start_left_rotation"
            case let id where id.contains("right.rotation"): fieldName = "start_left_rotation"
            default: fieldName = "unknown"
        }
        
        let item = Item(record: record, fieldName: fieldName, repeatInstance: repeatInstance, repeatInstrument: repeatInstrument, value: value, event: event)
        
        items.append(item)
    }

    do {
        let data = try JSONEncoder().encode(items)
        let stringData = String(data: data, encoding: .utf8)!
        print(stringData)

        // now do something with stringData
    } catch {
        print(error)
    }
}

I recommend also to insert this line before the for loop

assert(rangeOfMotionResults.count == variableIdentifiers.count, "The size of rangeOfMotionResults and variableIdentifiers must be equal")

It checks if the sizes of both arrays are equal, the expression is ignored in a Release Build.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1