Search code examples
swiftuistructswift3

How to use extern Structure in a View correctly?


I couldn't find a way to use my Structure in a ContentView. Structure fills correctly, but that's it. How can I replace example array in a View:

let countries = ["Germany", "Egypt", "Italy"] // example

with a real array of Countries from:

let countries: CountriesWikiData = await decodeWikiData(from: countriesQuery)
struct CountriesWikiData: Decodable {
    let head: CountriesHead
    let results: CountriesResults
}

struct CountriesHead: Decodable {
    let vars: [String]
}

struct CountriesResults: Decodable {
    let bindings: [CountriesBindings]
}

struct CountriesBindings: Decodable {
    let country: [String: String]
    let countryLabel: [String: String]
}


func decodeWikiData<T: Decodable>(from url: String) async -> T {
    
    let url = URL(string: url)!
    let request = URLRequest(url: url)
    
    do {
        let (data, _) = try await URLSession.shared.data(for: request)
        guard
            let jsonObj = try? JSONSerialization.jsonObject(with: data, options: []),
            let jsonData = try? JSONSerialization.data(withJSONObject: jsonObj, options: .prettyPrinted)
        else {
            fatalError("Cannot convert data to JSON.")
        }
        guard let strc = try? JSONDecoder().decode(T.self, from: jsonData)
        else {
            fatalError("Cannot make a structure.")
        }
        return strc // Wheeeee!!
    } catch {
        fatalError("Cannot get data from URL.")
    }
}


struct ContentView: View {
        
    /*
    SELECT DISTINCT ?country ?countryLabel
    WHERE
    {
      ?country wdt:P31 wd:Q3624078.
      SERVICE wikibase:label { bd:serviceParam wikibase:language "en" }
    }
    */
    
    let countriesQuery = "https://query.wikidata.org/sparql?query=SELECT%20DISTINCT%20%3Fcountry%20%3FcountryLabel%0AWHERE%0A%7B%0A%20%20%3Fcountry%20wdt%3AP31%20wd%3AQ3624078.%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22en%22%20%7D%0A%7D&format=json"
    
    let countries = ["Germany", "Egypt", "Italy"] // example
    @State var selectedCountry = ""
    
    //let countries: CountriesWikiData = await decodeWikiData(from: countriesQuery)

    var body: some View {
        VStack {
            Picker("", selection: $selectedCountry) {
                ForEach(countries, id: \.self) {
                    Text($0)
                }
            }
        }
    }
}


Solution

  • You could try this approach to display the results of the API call using the model structs as shown in the example code.

    The approach uses a modified CountriesBindings with Country and CountryLabel structs. An updated func decodeWikiData(...) in a .task {...} view modifier and a Picker with the important .tag(country.value)

    struct CountriesWikiData: Decodable {
        let head: CountriesHead
        let results: CountriesResults
    }
    
    struct CountriesHead: Decodable {
        let vars: [String]
    }
    
    struct CountriesResults: Decodable {
        let bindings: [CountriesBindings]
    }
    
    struct CountriesBindings: Codable {
        let country: Country
        let countryLabel: CountryLabel
    }
    
    struct Country: Identifiable, Codable, Hashable {
        let id = UUID()
        let type: String
        let value: String
    }
    
    struct CountryLabel: Identifiable, Codable, Hashable {
        let id = UUID()
        let xmlLang: String
        let type: String
        let value: String
    
        enum CodingKeys: String, CodingKey {
            case xmlLang = "xml:lang"
            case type, value
        }
    }
    
    struct ContentView: View {
        let countriesQuery = "https://query.wikidata.org/sparql?query=SELECT%20DISTINCT%20%3Fcountry%20%3FcountryLabel%0AWHERE%0A%7B%0A%20%20%3Fcountry%20wdt%3AP31%20wd%3AQ3624078.%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22en%22%20%7D%0A%7D&format=json"
        
        @State var countries: [CountryLabel] = []
        @State var selectedCountry: String = ""
    
        var body: some View {
            VStack {
                Text("selected country: \(selectedCountry)")
                Picker("", selection: $selectedCountry) {
                    ForEach(countries) { country in
                        Text(country.value).tag(country.value)
                    }
                }
            }
            .task {
                let response: CountriesWikiData? = await decodeWikiData(from: countriesQuery)
                if let resp = response {
                    for bin in resp.results.bindings {
                        countries.append(bin.countryLabel)
                    }
                }
                if let first = countries.first {
                    selectedCountry = first.value
                }
            }
        }
        
        func decodeWikiData<T: Decodable>(from url: String) async -> T? {
            if let url = URL(string: url) {
                do {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    return try JSONDecoder().decode(T.self, from: data)
                } catch {
                    print(error)
                }
            }
            return nil
        }
        
    }