-1

This response from API:

{
"status": 200,
"message": "Success",
"data": {
    "result": [
        {
            "name": "Anything",
            "response": [
                {
                    "name": "XYZ",
                    "prize": "1.86"
                },
                {
                    "name": "ABCD",
                    "prize": "9.86"
                }

            ]
        }
    ],
    "overall": "XYZ"
}
}

How, can I sum the prize in the response as I have to show it in the header of Table. I did this.

var prizeArr = [Int]()

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
    let header =  tableView.dequeueReusableHeaderFooterView(withIdentifier: cellReuseIdentifier) as! OverallHeaderCell

    let countArr = winnerArr![section]["response"] as? Array<Dictionary<String,Any>>

    prizeArr = (countArr!.compactMap{ $0["prize"] })

    print(prizeArr)

    header.textL[0].text = winnerArr![section]["name"] as? String
    header.textL[3].text = "\(String(describing: countArr!.count))"

    return header
}

I am trying to store the prize in the prizeArr and then I will sum it up. But, it prints nil, but when I write it in the terminal it gives the value individually. How, can I sum the prize in this ArrayObject?

Also, when I use prizeArr = [String](), then it works and store the prize in the Array, but for Int why is it not working?

Rob
  • 164
  • 1
  • 2
  • 12
  • Just pointing out but you are getting strings (`"1.86"`) for the price field. The `priceArr` is of type `[Int]`. There is no conversion occurring anywhere though. Also the numbers after the decimal are not going to transfer the way you have it now, – keji Aug 27 '18 at 15:04
  • Is there a way to convert them as when I use string, I get ["2.01"]. But, It is not converting. – Rob Aug 27 '18 at 15:11
  • You need to convert the strings from the API to whatever format you want. Like `Int("1.86")`. Note this will round because Int's don't have decimals. Also you will get an optional value. Please look up about `Float` and `Double`. However they are susceptible to off rounding that makes them look like: `1.85999999999`. Another option is using "cents" as the value. You could just remove the `.` and remember that the price is in cents (or whatever unit you use). – keji Aug 27 '18 at 15:11

4 Answers4

2

First, as @Benhamine pointed out you should start with a clean architecture and map your JSON into a type safe class.

Step 1: Codable Types

Lets define a structure that represents our JSON so we can better consume it in our App. JSON is something we never want to pass around our App. Lets rather pass around something well-defined and documented so we have no need to do any force unwraps and crash our App.

struct JSONResponse: Codable {
    enum CodingKeys: String, CodingKey {
        case data
    }
    let data: Data
}

extension JSONResponse {
    struct Data: Codable {

        enum CodingKeys: String, CodingKey {
            case results = "result"
        }

        let results: [Result]
    }
}

extension JSONResponse.Data {
    struct Result: Codable {
        let name: String
        let winners: [Winner]

        enum CodingKeys: String, CodingKey {
            case winners = "response"
            case name
        }
    }
}

extension JSONResponse.Data.Result {
    struct Winner: Codable {
        let name: String
        let prize: String
    }
}

Step 2: Parsing

Parsing using Codable is super simple. The code below will show how we convert it into JSON and also how one could go about getting the sum of the float values.

do {
    let o: JSONResponse = try JSONDecoder().decode(JSONResponse.self, from: jsonData)
    let floatValues = o.data.results.flatMap({ $0.winners }).compactMap({ Float($0.prize) })
    floatValues.reduce(0, +)
    print(floatValues)
} catch let e {
    print(e)
}

Step 3: Integrate

We now have the building blocks we need to get this information so let's hook it up to your code by starting with what we want our code to look like.

/// We store our main data type for easy reference
var resultsBySection: [Result]

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
    let header =  tableView.dequeueReusableHeaderFooterView(withIdentifier: cellReuseIdentifier) as! OverallHeaderCell

    let currentResults = resultsBySection[section]
    let prizeTotals = currentResults.flatMap({ $0.winners }).compactMap({ Float($0.prize) })
    let totalPriceMoney = prizeTotals.reduce(0, +)

    header.textL[0].text = currentResults.name
    header.textL[3].text = "\(String(describing: totalPriceMoney))"

    return header
}

Notice how in the above code I do not do any JSON decoding in cell dequeueing. Ideally that should be done when we retrieve our JSON and convert it to our types.

Step 4: Refactor

An essential piece of any code experience should contain some reflection into the code we have written and consider how it could be refactored.

In our example above we could probably hardcode the totals onto the controller or create a custom data structure that will do this for us when we parse JSON. Do we always want to manually calculate the totals if we always need this total? We could have a custom function to do the calculation or just do it in our JSON decoding logic.

Anyways, the idea is that we should always look at what we are writing and question how it could be improved

Daniel Galasko
  • 23,617
  • 8
  • 77
  • 97
  • Also note that `viewForHeaderInSection` is called over and over as the user scrolls. So you end up calculating the same totals over and over. All of this should be calculated once and stored in an appropriate data model being used by the table view. – rmaddy Aug 27 '18 at 15:39
  • Okay, I am done with step 1, where should I write the step 2? – Rob Aug 27 '18 at 15:43
  • @Rob I assume you have some code somewhere that fetches that JSON you are using. All you need to do is parse it to a property in your controller and you should be fine – Daniel Galasko Aug 28 '18 at 07:46
  • Thanks! With your answer I could sum json values in nested array :) – Abrcd18 Oct 03 '22 at 22:56
1

Try this Codable solution to reduce the prizes together:

struct Winners: Codable {
    let status: Int
    let message: String
    let data: DataClass
}

struct DataClass: Codable {
    let result: [Result]
    let overall: String
}

struct Result: Codable {
    let name: String
    let response: [Response]
}

class Response: Codable {
    let name: String
    let prize: Double

    init(name: String, prize: Double) {
        self.name = name
        self.prize = prize
    }

    enum CodingKeys: String, CodingKey {
        case name
        case prize
    }

    enum SerializationError: Error {
        case missing(String)
        case invalid(String, Any)
    }

    public required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)

        let prizeString = try container.decode(String.self, forKey: .prize)
        guard let prize = Double(prizeString) else {
            throw SerializationError.invalid("prize", prizeString)
        }

        self.prize = prize
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode("\(prize)", forKey: .prize)
    }
}

func testing() {
    let winners = try! JSONDecoder().decode(Winners.self, from: jsonData)

    let sum = winners.data.result[section].response
        .map({ $0.prize })
        .reduce(0, +)
    print(sum)
}
Fabian
  • 5,040
  • 2
  • 23
  • 35
  • @Rob Just insert for it the variable where the `jsonData` is stored. – Fabian Aug 27 '18 at 15:48
  • @Rob Now I didn't get you. Sometimes happens to me too! Do you mean you didn't get what to put there or "just happens sometimes"? Ask questions maybe I can help. – Fabian Aug 27 '18 at 15:50
  • I didn't know what to use instead of `jsonStr`. – Rob Aug 27 '18 at 15:59
  • Well, the decoder is to decode the response from API, I should have written `from: responseFromAPI` to make it clearer. Wherever you get the response, so you can immediately decode and store `winners` so you don’t do the work multiple times. – Fabian Aug 27 '18 at 16:04
0
let sum = winnerArr?[section]["response"].reduce(0, { x, y in
    x + y["prize"]
}) ?? 0

As an aside, I would recommend parsing the response and turning it into usable objects rather than dealing with the raw response : https://developer.apple.com/swift/blog/?id=37

Benhamine
  • 143
  • 6
  • Consider the optionals and that the prices are strings. – vadian Aug 27 '18 at 15:08
  • It says `Value of type Any? has no member reduce` – Rob Aug 27 '18 at 15:10
  • 1
    Love pointing out the developer docs. In this post specifically what is missing is some clean architecture. Once you have your JSON into a custom object you wont need to do all those dodgy force unwraps which are just waiting to crash your App – Daniel Galasko Aug 27 '18 at 15:17
  • Blegh, yea so I'd definitely work on converting this response into some sort of response object first then this step should be trivial and would be more idiomatic. – Benhamine Aug 27 '18 at 15:22
  • How can I convert the response to some of response object? – Rob Aug 27 '18 at 15:29
  • This Apple article should get you through it. You just want an object that matches the JSON format and then use JSONSerialization to deserialize the raw JSON into the response object: https://developer.apple.com/swift/blog/?id=37 – Benhamine Aug 27 '18 at 15:35
0

Thanks to all of your guys, who helped. I am a newbie and not familiar with Codable. Thanks for introducing me to the same. I will try to understand it and use it for future purposes.

So, coming back to the answer:

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
    {
    self.prizeArr.removeAll()

    let header =  tableView.dequeueReusableHeaderFooterView(withIdentifier: cellReuseIdentifier) as! OverallHeaderCell

    let resArr   = self.winnerArr![section]["response"]! as? Array<Dictionary<String,Any>>

    for prize in  resArr!
    {
        let doubleStr = prize["prize"] as? NSString

        self.prizeArr.append((doubleStr?.doubleValue)!)
    }

    let sumedArr = prizeArr.reduce(0, +)

    let countArr = winnerArr![section]["response"] as? Array<Dictionary<String,Any>>

    header.textL[0].text = winnerArr![section]["name"] as? String

    header.textL[1].text = "$\(sumedArr)"

    header.textL[3].text = "\(String(describing: countArr!.count))"

    return header
    }

Here I am converting each string to a double and appending it to the prizeArr and then, finally, summing the entire array to get the desired result. But, this is not the ideal way to do it. This answer is for all the newbies like me and please learn Codables.

Rob
  • 164
  • 1
  • 2
  • 12