2

I am new to swift and the sync/async way to load a file. I have a big JSON file in local, for an iPad app about football with the list and stats of football players.

At the moment I load the whole list of players inside an array of dictionaries and I let the user search for the specific player

func loadJSON() {
    /// Load Json File
    if let path = Bundle.main.path(forResource: "players", ofType: "json") {
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
            let jsonObj = try JSON(data: data)

            /// For Player in JSON Serialize values
            for (_,subJson):(String, JSON) in jsonObj["PackData"]["PlayerData"]["P"] {

                let firstName = subJson["_f"].stringValue
                let lastName = subJson["_s"].stringValue
                let id = subJson["_id"].stringValue
                let dateOfBirth = subJson["_d"].stringValue
                let height = subJson["_h"].stringValue
                let weight = subJson["_w"].stringValue
                let image = subJson["_i"].stringValue


                let player = Player(id: id, firstName: firstName, lastName: lastName, dateOfBirth: dateOfBirth, height: height, weight: weight, image: image)

                /// Append Player in players Array
                players.append(player)

            }

Since I use loadJSON() in ViewDidLoad, the app freeze for few seconds and uses a lot of memory when I segue to this view.

What is the proper way to handle/implement something like a search in DB, in async?

EDIT: I already tried to use dispatch DispatchQueue.global(qos: .background).async but I get the error: indexPath.row out of range on player = filteredPlayers[indexPath.row]

 // create a cell for each table view row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    // create a new cell if needed or reuse an old one

    let cell:UITableViewCell = self.searchTableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
    let player: Player

    /// Return a different table if is searching or not
    if isFiltering() {
        player = filteredPlayers[indexPath.row]
    } else {
        player = players[indexPath.row]
    }
    cell.textLabel?.text = player.firstName! + " " + player.lastName!

    cell.textLabel?.textColor = UIColor.white

    return cell

}
dan
  • 77
  • 1
  • 10

5 Answers5

3

You need to use in a DispatchQueue in background,

func loadJSON() {
    /// Load Json File
    DispatchQueue.global(qos: .background).async{
        if let path = Bundle.main.path(forResource: "players", ofType: "json") {
            do {
                let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
                let jsonObj = try JSON(data: data)

                /// For Player in JSON Serialize values
                for (_,subJson):(String, JSON) in jsonObj["PackData"]["PlayerData"]["P"] {

                    let firstName = subJson["_f"].stringValue
                    let lastName = subJson["_s"].stringValue
                    let id = subJson["_id"].stringValue
                    let dateOfBirth = subJson["_d"].stringValue
                    let height = subJson["_h"].stringValue
                    let weight = subJson["_w"].stringValue
                    let image = subJson["_i"].stringValue


                    let player = Player(id: id, firstName: firstName, lastName: lastName, dateOfBirth: dateOfBirth, height: height, weight: weight, image: image)

                    /// Append Player in players Array
                    players.append(player)

                }
            }
        }
    }
}
A.Munzer
  • 1,890
  • 1
  • 16
  • 27
  • I tried to use it, but I get the out of range error on [indexPath.row] when I display that data in the tableView, should I change also the tableView to display data in async? – dan May 08 '18 at 14:21
  • how do you declare your array **players** ? @dan – A.Munzer May 08 '18 at 14:39
  • I added additional code to the question. I have two arrays, one is for filtered players, when user starts to type in searchBar, the other is for a generic list of players. – dan May 08 '18 at 15:04
  • @dan after you added player to players array filter it directly. – A.Munzer May 09 '18 at 06:27
  • could you please go more in detail? I don't understand what you mean by filter it directly. Thanks :) – dan May 09 '18 at 09:59
  • You should filter your array after you append all your players, also check that this function func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if isFilterd{ return filteredPlayers.count}else{ return players.count } } use filteredPlayers , please click on current answer if it helps you – A.Munzer May 09 '18 at 10:21
2

You need to dispatch your lengthy task to background queue and dispatch the result back to the main one.

My simplified json example:

{
    "person": "Bob"
}

Create load json method

func loadJSON(completion: @escaping (_ data: String?, _ error: Error?) -> ())  {
    var person: String?
    var receivedError: Error?

    /// Load json file and parse in background queue
    DispatchQueue.global(qos: .background).async {
        let path = Bundle.main.path(forResource: "myJSON", ofType: "json")!
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
            let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
            let jsonDictionary =  json as! Dictionary<String, Any>

            person = jsonDictionary["person"] as? String
        } catch {
            receivedError = error
        }

        // Dispatch the found value to main queue
        DispatchQueue.main.async {
            completion(person, receivedError)
        }
    }
}

And call this in your code:

loadJSON { (data, error) in
    if let retrievedData = data {
        print(retrievedData)
        // It is safe to assign the value to UI objects 
        // because the callback is on the main thread
    }
}
Au Ris
  • 4,541
  • 2
  • 26
  • 53
1

Perform loadJSON() in a background thread, then push the result back to main via callback or property assignment. The code:

DispatchQueue(label: "jsonLoading", qos: .background).async {
    let players = self.loadJSON()
    DispatchQueue.main.async {
        // Handle data in the main thread in whatever way you need, eg:
        self.players = players
    }
}
Vadim Popov
  • 1,177
  • 8
  • 17
0
func loadJSON(completion: @escaping ()->()) {
    /// Above Parameter is a completion handler which informs user that some task have been done.
    //Do such heavy operations like json loading from local file in a back ground thread so your main thread doesn't get affected.
    DispatchQueue.global(qos: .background).async {
        if let path = Bundle.main.path(forResource: "players", ofType: "json") {
            do {
                let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
                let jsonObj = try JSON(data: data)

                /// For Player in JSON Serialize values
                for (_,subJson):(String, JSON) in jsonObj["PackData"]["PlayerData"]["P"] {

                    let firstName = subJson["_f"].stringValue
                    let lastName = subJson["_s"].stringValue
                    let id = subJson["_id"].stringValue
                    let dateOfBirth = subJson["_d"].stringValue
                    let height = subJson["_h"].stringValue
                    let weight = subJson["_w"].stringValue
                    let image = subJson["_i"].stringValue


                    let player = Player(id: id, firstName: firstName, lastName: lastName, dateOfBirth: dateOfBirth, height: height, weight: weight, image: image)

                    /// Append Player in players Array
                    players.append(player)
                }
                completion()
            }
            catch _ {

            }
        }
    }
}

Now call above method in your viewController class or any other you want.

func yourViewControllerMethod() {
    loadJSON {
        // This Block will execute when your json is loaded and parsed completely.
        DispatchQueue.main.async{
             // This is the main thread now you are again here on your main thread after your journey from background thread.

        }
    }
}

Never do any thing with your UI elements in a background thread

Syed Qamar Abbas
  • 3,637
  • 1
  • 28
  • 52
0

Move to a background thread to do some long running work

func loadJSON(_ completion:@escaping (_ jsonObj:JSON?) -> Void){
        DispatchQueue.global(qos: .background).async {
            if let path = Bundle.main.path(forResource: "players", ofType: "json") {
               if  let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped), let jsonObj = try? JSON.init(data: data){
                    completion(jsonObj)
               }else{
                 completion(nil)
                }
            }else{
                 completion(nil)
            }
        }
    }

then call like this

   self.loadJSON { (jsonObj) in

            DispatchQueue.main.async {
                guard let jsonObj = jsonObj else {return}
                /// For Player in JSON Serialize values
                for (_,subJson):(String, JSON) in jsonObj["PackData"]["PlayerData"]["P"] {

                    let firstName = subJson["_f"].stringValue
                    let lastName = subJson["_s"].stringValue
                    let id = subJson["_id"].stringValue
                    let dateOfBirth = subJson["_d"].stringValue
                    let height = subJson["_h"].stringValue
                    let weight = subJson["_w"].stringValue
                    let image = subJson["_i"].stringValue


                    let player = Player(id: id, firstName: firstName, lastName: lastName, dateOfBirth: dateOfBirth, height: height, weight: weight, image: image)

                    /// Append Player in players Array
                    players.append(player)
            }
        }
Abdelahad Darwish
  • 5,969
  • 1
  • 17
  • 35