0
votes

I'm working on an app where I need to store a Country and City in a Firebase database. Besides storing, I also need to retrieve that info and present to the user in a pickerView. Given that, I need to read the Country and City from the Database, check what is their index and set it in pickerView.

Countries and Cities are store in JSON

{
    "Country": [
        {
            "name": "UK",
            "cities": [
                {
                    "name": "London"
                },
                {
                    "name": "Manchester"
                },
                {
                    "name": "Bristol"
                }
            ]
        },
    {
        "name": "USA",
        "cities": [
            {
                "name": "New York"
            },
            {
                "name": "Chicago"
            }
        ]
    },
    {
        "name": "China",
        "cities": [
            {
                "name": "Beijing"
            },
            {
                "name": "Shanghai"
            },
            {
                "name": "Shenzhen"
            },
            {
                "name": "Hong Kong"
            }
       ]
    }
    ]
}

My code to read JSON is

// Declared in Class
var countryList = [NSDictionary]()
var selectedRow = [NSDictionary]()
var selectedCity = ""
var selectedCountry = ""

func readJson() {
   if let path = Bundle.main.path(forResource: "Countries", ofType: "json") {
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
            let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
            if let jsonResult = jsonResult as? Dictionary<String, AnyObject>, let country = jsonResult["Country"] as? [NSDictionary] {

                //handles the array of countries on your json file.
                self.countryList = country
                self.selectedRow = self.countryList.first?.object(forKey: "cities") as! [NSDictionary]


            }
        } catch {
            print("error loading countries")
            // handle error

        }
    }
}

The code above allows me to feed a UIPickerView with 2 sessions and the Country and the list of cities within that Country. From there, I can also identify which Country and City were selected.

AS part of my code I have a func that would allow me to identify what is indexes of the saved Country(countryIndex) and City(cityIndex) in UIPickerView so that I can set it and that's where my issues start

func indexOf(city: String, inCountry country: String, in countries: [Country]) -> (countryIndex: Int, cityIndex: Int)? {
    // countries is an array of [Country]
    // var countries = [Country]()
    guard let countryIndex = countries.firstIndex(where: {$0.name == country}), let cityIndex = countries[countryIndex].cities.firstIndex(where: {$0.name == city}) else {return nil}
    //let cityIndex = 0
    return (countryIndex, cityIndex)
} // courtesy of @flanker

This func was working perfectly fine when my Countries and Cities were stored to a [Country] but is not working with NSDictionary coming from JSON.

I have tried to 1) Change [Country] by [NSDictionary], "countries" by "countryList" and "name" by "Country" Here I receive and error "NSDictionary has no member Country" I also tried to leave just $0 == Country which hasn't worked as well. 2) Tried also "countryList.firstIndex(of: "USA")" but got the error below Cannot convert value of type 'String' to expected argument type 'NSDictionary'

Anyone would be able to assist? How can I make the func indexOf work again?

Thanks

Updated according to @vadian's suggestion

My updated code is

import UIKit

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {


    @IBOutlet weak var pickerView: UIPickerView!
    @IBOutlet weak var countryLbl: UILabel!

    var familyNames: [String] = []
    var fontName = "Arial"
    let fontCount = 0
    var countryList = [Country]()
    var selectedRow = [City]()
    var selectedCity : City?
    var selectedCountry : Country?

    struct Root : Decodable {
        let country : [Country] // better plural let countries
        private enum CodingKeys : String, CodingKey { case country  = "Country" }
    }

    struct Country : Decodable {
        var name : String
        var cities : [City]
    }

    struct City : Decodable {
        var name : String
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        pickerView.delegate = self
        pickerView.dataSource = self

        fontName = "HelveticaNeue" 
    }

func indexOf(city: String, inCountry country: String, in countries: [Country]) -> (countryIndex: Int, cityIndex: Int)? {
    guard let countryIndex = countries.firstIndex(where: {$0.name == country}), let cityIndex = countries[countryIndex].cities.firstIndex(where: {$0.name == city}) else {return nil}
    return (countryIndex, cityIndex)
}

    func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
        if component == 0 {
            return 80
        } else {
            return 300
        }

    }

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if component == 0 {
            return countryList.count
        } else {
            return selectedRow.count
        }
    }

    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {

        var rowTitle = ""
        let pickerLabel = UILabel()

        pickerLabel.textColor = UIColor.blue

        switch component {
        case 0:
            rowTitle = countryList[row].name
        case 1:
            rowTitle = selectedRow[row].name
        default:
            break
        }

        pickerLabel.text = rowTitle
        pickerLabel.font = UIFont(name: fontName, size: 20.0)
        pickerLabel.textAlignment = .center

        return pickerLabel
    }

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    pickerView.reloadAllComponents()

    if component == 0 {
        self.selectedCountry = self.countryList[row]
        self.selectedRow = self.countryList[row].cities

        pickerView.reloadComponent(1)
        self.pickerView.selectRow(0, inComponent: 1, animated: true)
        self.selectedCity = self.selectedRow[0]
    } else {
        self.selectedCity = self.selectedRow[row]
    }

    if let indexes = indexOf(city: self.selectedCity!.name, inCountry: self.selectedCountry!.name, in: countryList) {
           //do something with indexes.countryIndex and indexes.cityIndex
           print("This is the result \(indexes.cityIndex) and \(indexes.countryIndex)")

           }
    countryLbl.text = "The right answer is: \(self.selectedCountry?.name) and the city is \(self.selectedCity?.name)"
    }

    func readJson() {
        let url = Bundle.main.url(forResource: "Countries", withExtension: "json")!
        do {
            let data = try Data(contentsOf: url)
            let jsonResult = try JSONDecoder().decode(Root.self, from: data)

            //handles the array of countries on your json file.
            self.countryList = jsonResult.country
            self.selectedRow = self.countryList.first!.cities
        } catch {
            print("error loading countries", error)
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
        readJson()
    }
}
1

1 Answers

1
votes

You have to decode the JSON in the bundle also into custom structs

struct Root : Decodable {
    let country : [Country] // better plural let countries
    private enum CodingKeys : String, CodingKey { case country  = "Country" }
}

struct Country : Decodable {
    let name : String
    let cities : [City]
}

struct City : Decodable {
    let name : String
}

var countryList = [Country]()
var selectedRow : [City]()
var selectedCity : City?
var selectedCountry : Country?

func readJson() {
    let url = Bundle.main.url(forResource: "Countries", withExtension: "json")!
    do {
        let data = try Data(contentsOf: url)
        let jsonResult = try JSONDecoder().decode(Root.self, from: data)

        //handles the array of countries on your json file.
        self.countryList = jsonResult.country
        self.selectedRow = self.countryList.first!.cities
    } catch {
        print("error loading countries", error)
    }
}

Then your method indexOf(city:inCountry:in:) works

As the file is in the application bundle consider to omit the root dictionary "Country" and decode [Country].self.


The usual side notes:

  • Do not use NS... collection types in Swift. You throw away the type information. Use native types.

  • .mutableContainers and .mutableLeaves are pointless in Swift. Apart from that ironically you assign the value to an immutable constant anyway.

  • A JSON dictionary in Swift 3+ is always value type [String:Any] not reference type[String:AnyObject].