0

I am brand new to writing swift so any tips on improvement/best practices are welcome but my main issue is that I am having trouble accessing a nested JSON array list. I am using this free API and trying to show a list of characters https://swapi.dev/api/people/

Please see the code snippet below.

When I print the type its : Optional<Any> and when i print json["results"] it prints the array like:

Optional(<__NSArrayI 0x600000fe31e0>(
{
    "birth_year" = 19BBY;
    created = "2014-12-09T13:50:51.644000Z";
    ....

I have tried several different things but have been unsuccessful. Could someone please give some advice on how I might iterate the list under json["results"?

func onLoad() -> Void {
            let url = URL(string: "https://swapi.dev/api/people")
            
            guard let requestUrl = url else { fatalError() }
            // Create URL Request
            var request = URLRequest(url: requestUrl)
            // Specify HTTP Method to use
            request.httpMethod = "GET"
            // Send HTTP Request
            let task = URLSession.shared.dataTask(with: request) { (data, response, error) in

                // Check if Error took place
                if let error = error {
                    print("Error took place \(error)")
                    return
                }

                // Convert HTTP Response Data to a simple String
                if let data = data {
//                    let json = try? JSONSerialization.jsonObject(with: data, options: [])
                    
                    do {
                        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                            // try to read out a string array
                            print(type(of: json["results"]))
                            print(json["results"])
                        }
                    } catch let error as Error {
                        print("Failed to load: \(error.localizedDescription)")
                    }
                    
                }

            }
            task.resume()

        }

Thanks for any help!

Asperi
  • 228,894
  • 20
  • 464
  • 690
craigtb
  • 647
  • 5
  • 12
  • 30

2 Answers2

1

You should really be using Decodable rather than trying to parse it with JSON as that can easily lead to errors as you are accessing values by strings and it doesn't allow the IDE to help you.

You need to create some objects that describe what you are getting in your response.

Your main json response is made up of the following

{
    "count": 82,
    "next": "http://swapi.dev/api/people/?page=2",
    "previous": null,
    "results": [...]
}

This allows you to create a People struct that conforms to Decodable.

struct People: Decodable {
    let count: Int
    let next: URL?
    let previous: URL?
    let results: [Person]
}

The results array is really what you are after as that contains all the information about a person.

{
  "name": "Luke Skywalker",
  "height": "172",
  "mass": "77",
  "hair_color": "blond",
  "skin_color": "fair",
  "eye_color": "blue",
  "birth_year": "19BBY",
  "gender": "male",
  "homeworld": "http://swapi.dev/api/planets/1/",
  "films": [
      "http://swapi.dev/api/films/1/",
      "http://swapi.dev/api/films/2/",
      "http://swapi.dev/api/films/3/",
      "http://swapi.dev/api/films/6/"
  ],
  "species": [],
  "vehicles": [
      "http://swapi.dev/api/vehicles/14/",
      "http://swapi.dev/api/vehicles/30/"
  ],
  "starships": [
      "http://swapi.dev/api/starships/12/",
      "http://swapi.dev/api/starships/22/"
  ],
  "created": "2014-12-09T13:50:51.644000Z",
  "edited": "2014-12-20T21:17:56.891000Z",
  "url": "http://swapi.dev/api/people/1/"
}

We can represent this with the following struct called Person that also conforms to Decodable

struct Person: Decodable {
    let name: String
    let height: String
    let mass: String
    let hairColor: String
    let skinColor: String
    let birthYear: String
    let gender: Gender
    let homeworld: String
    let films: [URL]
    let species: [URL]
    let vehicles: [URL]
    let starships: [URL]
    let created: Date
    let edited: Date
    let url: URL
}

enum Gender: String, Decodable {
    case male
    case female
    case unknown = "n/a"
}

Note a couple of differences between the names in the struct and the names in the object that you are getting back. eg hair_color (snakecase) and hairColor (camelCase) In Swift it is common to write it the latter way and when we use decodable we can tell our decoder to use a custom key decoding strategy. Also note that I have used an enum for Gender. This isn't required and we could have just used a String. Also note that created and edited are Dates, however they are not iso8601 compliant but we can also specify a custom date decoding strategy.

Here is how we can decode the data that you have received.

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)

let people = try decoder.decode(People.self, from: data)

Now we can put this all together in your network request to get the following:

func onLoad() {
    let url = URL(string: "https://swapi.dev/api/people")

    guard let requestUrl = url else { fatalError() }
    // Create URL Request
    var request = URLRequest(url: requestUrl)
    // Specify HTTP Method to use
    request.httpMethod = "GET"
    // Send HTTP Request
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in

        // Check if Error took place
        if let error = error {
            print("Error took place \(error)")
            return
        }

        // Convert HTTP Response Data to a simple String
        if let data = data {
            do {

                let dateFormatter = DateFormatter()
                dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"

                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                decoder.dateDecodingStrategy = .formatted(dateFormatter)

                let people = try decoder.decode(People.self, from: data)
                people.results.forEach { person in print(person) } 
            } catch {
                print("Failed to load: \(error)")
            }

        }

    }
    task.resume()
}
Andrew
  • 26,706
  • 9
  • 85
  • 101
  • This seems like the right approach. My question before i really get going is if the struct for `Person` must include all the fields, or can you only define the ones you care about? – craigtb Jun 25 '20 at 19:30
  • You can just define the ones you want. – Andrew Jun 25 '20 at 19:32
0

Cast results as an Array of Dictionary. Here's how

if let data = data {
    do {
        if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
            let results = json["results"] as? [[String: Any]] {
            for result in results {
                print(result)
            }
        }
    } catch {
        print("Failed to load: \(error.localizedDescription)")
    }
}

Better Approach: Use Codable, JSONSerialization feels bit outdated.

Related Links:

  1. https://developer.apple.com/documentation/swift/codable
  2. https://www.swiftbysundell.com/basics/codable/
Frankenstein
  • 15,732
  • 4
  • 22
  • 47
  • This works and gives what i needed but the other response seems to be the correct one. Thanks, this at least gets me going. – craigtb Jun 25 '20 at 19:29
  • `Codable` approach is the default for a while now and has been discussed several times here on SO, didn't make much sense to add another one here. Anyways, if this post helped then upvote. – Frankenstein Jun 25 '20 at 19:35