WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: JSON - How to collect specific data based on an external string?

Forums > SwiftUI

Hi Everyone, I'm working on a project which tells you about the country you are in. I get the users location from a placemark, which is then turned into a string (e.g. "Australia). This is the code for that

    var country: String { return("\(lm.placemark?.country ?? "")") }

I also have a large JSON file with information about each country, with the name of the country also saved as a string ("Australia"). Here is what the JSON looks like.

{
    "Country": "Australia",
    "Pop": "25 Million",
    "PopState": "New South Wales",
    "NatAnimal": "Red Kangaroo",

}

From this, I want to be able to select the user's current country from the JSON file and display it on the screen (e.g. "Australia's Population is 25 Million").

Is there a way to do this with my current setup, or do any of you have an idea of another way I could do this? I'm very new to swift and swiftui, so I'm unsure as to how I should be doing this. Any help would be incredible. Thanks in advance.

1      

Hi,

you could reorganize your JSON, so it would be like a dictionary, when you could use the country name as a key and get the other information.

But I think the issue with current setup is that placemark?.country is very likely localized based on the language of the device. So unless your user will have phone in English, I don't think you will be able to map the country name to your JSON data. I would look into ISO codes and whether they can be obtained from the placemark.

1      

Do you need the country where the user is in a precise moment, or depending by where it resides? I would add a Locale for every country in the Json file and retrieve the data based on the locale of the user rather then the placemark (so language doesn't affect you either), bit it depends by what you want to achieve.

1      

In the engineering world there's a saying that the only sure way to eat an entire elephant is to finish one bite at a time.

So much of structuring a program is breaking down your program's objectives into solvable "bite sized" pieces. You may be getting caught up in the enormity of your goals. ("I want to select the user's location from a placemark and grab the json data....")

Break your problem into smaller pieces. Then solve one problem at a time.

For example: Don't think of your data as being in a JSON file. The data exists as JSON for only a short time. In your application, all your data could (should?) be stored in an array! Your first solvable puzzle is how to get the data from your application's bundle into an array. This should be easy to solve because @twostraws has some great(!) videos about loading JSON files from your application bundle. Which concepts do you have trouble with? Let us know which videos you understand, and which concepts cause headaches.

Now you don't need to think about JSON anymore. Stop referring to JSON data. You have an array of countryInfo, you just have to find the entry in the array probably using a key (such as an ISO code) as suggested by @nemecek.

What's the next bite of this elephant?

Getting the user's location. Solve that problem next. Are you a fan of CoreLocation? Countries are quite big. Do you need location down to the nearest square meter, or would you be comfortable with an approximation? Are you allowing a user to tap a location in MapKit? Again, find a smaller problem you can solve. Define a structure to define where your user is. Populate that structure with Latitude and Longitude, City Name, or Country name using Apple's built-in capabilities. Again, HackingWithSwift can help you. @twostraws created some videos in 2019 demonstrating MapKit and getting locations. Follow those tutorials. Take lots of notes. Play with MapKit in Playgrounds.

If you have questions on the techniques, or parts of the videos, we are here to help you answer them. But please review those videos a few times before giving up!

If you've solved a few smaller problems, you'll have a ready source of country data. You'll have the user's location (current or planned). The last few steps will be for you to link the two together and display a nice summary for your user.

1      

@Obelix thanks for your answer. I've managed to get all the way up to the last step, but this is where I'm unsure on what I should be doing. I have the data stored in an array, and I've got the ISO code from the users location loaded as a string, but I'm unsure as to how I should be linking them together. do I just make a huge if statement, or is there a better way of doing this? Thanks for your help so far, I really appreciate it.

1      

@souponastick says:

I've managed to get all the way up to the last step, but this is where I'm unsure on what I should be doing.

Excellent! THIS is why this forum exists! I love seeing how other help in with different points of views.

You say you have the data stored in an array. Please share the structure of the array. Specifically, we'll need to know what data elements you're storing in your structure. Very important to think of your design details. What makes one array element different from the others? Probably the ISO code, but let's have a look at your structure just the same. Is your structure Identifiable?

Others might just dish out the answer. But I enjoy @twostraws' methods of asking questions, providing some hints, and hoping in the end you become a better developer by finding the answer. Let's see your structure to find a path to raise you to the next level.

1      

@Obelix Hi, here is the code I'm using to unpack the json file.


func parseJSON() {
    guard let path = Bundle.main.path(forResource: "TestJSON", ofType: "json") else {return}
    let url = URL(fileURLWithPath: path)

    var result: CountryResult?
    do {
        let jsonData = try Data(contentsOf: url)
        result = try JSONDecoder().decode(CountryResult.self, from: jsonData)

        if let result = result {
            print(result)
        }
        else {print("parse failed")}
    }
    catch {print("Error: \(error)")}
}

struct CountryResult: Codable {
    let data: [CountryData]

}
struct CountryData: Codable {
    var ISO: String
    var Pop: String
    var PopState: String
    var NatAnimal: String

}

In addition, here is the new structure of the json file, following the changes suggested earlier.

{
    "data": [
    {
        "ISO": "AU",
        "Pop": "25 Million",
        "PopState": "New South Wales",
        "NatAnimal": "Red Kangaroo",

    },
    {
        "ISO": "US",
        "Pop": "332 Million",
        "PopState": "California",
        "NatAnimal": "Bald Eagle",

    },

]
}

where do I go from here?

1      

Keep in mind, SwiftUI views are declarative. Just tell the view what you want it to show. For example, you have a long list of countries in a convenient storage package I'm calling fabulousCountries. So if I were to interview you, and asked what do you want to show in your view, you might say,

I'd like to display a fun fact about the fabulous country that I am in.

We'd then try to code something like:

// This might be a component in your application's view
// This is ONE bite of the big elephant. It is a small solvable chunk of logic.
struct FunFactView: View {
// Your views should not need to know about CountryData or CountryResults or their parts.
    var funFact: String // single purpose view --> Show a fun fact!
    var body: some View {
            Text( funFact ) // DECLARE what you want to display.
                .font(.headline)
                .frame(width: 300, height: 200)
                .background( Color.indigo )
    }
}

So how do you construct a fun fact? Try this in a Playground.

// =============
// HackingWithSwift Forums
// How to collect specific data based on external string?
// Sample Code for @souponastick
// by: Obelix  07 Dec 2021

import SwiftUI
import PlaygroundSupport

struct CountryData: Codable {
    var iso: String
    var population: String
    var state: String
    var animal: String
}

// This might be a component in your application's view
struct FunFactView: View {
    var funFact: String // single purpose view --> Show a fun fact!
    var body: some View {
            Text( funFact ) // DECLARE what you want to display.
                .font(.headline)
                .frame(width: 300, height: 200)
                .background( Color.indigo )
    }
}

// This is ANOTHER bite of the big elephant. It is a small solvable chunk of logic.
struct CountryResult: Codable {
    let data: [CountryData]  // can you think of a better name for this let ?

    // Think of this as a business rule. Keep your business rules separate from your VIEWS.
    // The View should just DECLARE what it wants to display.
    func funFact(for iso: String) -> String {
        // look in your array for a single ISO code.
        if let foundCountry = data.first(where: { $0.iso == iso })  // Array magic! Study this!
        {
            return ("\(foundCountry.state) has a population of \(foundCountry.population). While enjoying your vacation, look for \(foundCountry.animal)s.")
        } else {
            return "Sorry! ISO >\(iso)< not found in our list of fabulous countries 😢"
        }
    }
}

let australia = CountryData(iso: "AU",  population: "25 million",   state: "New South Wales", animal: "Red Kangaroo")
let usa       = CountryData(iso: "US",  population: "332 Million",  state: "California",      animal: "Bald Eagle")
let canada    = CountryData(iso: "CAN", population: "100 at least", state: "Ontario",         animal: "Moose")

// for the test, do NOT include Canada.
let fabulousCountries = CountryResult(data: [australia, usa])  // your results will come from JSON.

PlaygroundPage.current.setLiveView(FunFactView(funFact: fabulousCountries.funFact(for: "AU")))
// Uncomment and run again.
//PlaygroundPage.current.setLiveView(FunFactView(funFact: fabulousCountries.funFact(for: "CAN")))

Above you noted and asked:

I'm unsure as to how I should be linking them together. do I just make a huge if statement, or is there a better way of doing this?

Hope you can see that "NO!" you do not want a huge IF statement! Having your data in array gives you access to many array functions making it trivial to search through arrays for a single entry.

2      

Thank you so much for you help, it all makes sense now! I really appreciate it.

1      

Hacking with Swift is sponsored by Fernando Olivares

SPONSORED Fernando's book will guide you in fixing bugs in three real, open-source, downloadable apps from the App Store. Learn applied programming fundamentals by refactoring real code from published apps. Hacking with Swift readers get a $10 discount!

Read the book

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.