iosswiftxcodemobileswifty-json

value becomes null after function swift


I've been practicing swift in a couple of days now, just stuck with this problem when after calling a function getItunesData() the model var songItems: [SongItem] = [] becomes nil.

I've tried breakpoint inside the calling function I can still see the data, however after that it becomes nil.

Hope you guys can help me with this one. Cheers

import UIKit
import Alamofire
import SwiftyJSON

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    @IBOutlet weak var tableView: UITableView!
    var songItems: [SongItem] = []

    let city = ["one","two","three"]
    let params = ["term":"marroon5",
                  "limit":"5"]
    let url: String = "https://itunes.apple.com/search?"

    
    override func viewDidLoad() {
        super.viewDidLoad()
        getItunesData()

        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UINib(nibName: "customCell", bundle: nil), forCellReuseIdentifier: "customCell")
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return songItems.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! customCell
        
        cell.labelTxt.text = songItems[indexPath.row].releaseDate
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    }
    
    func getItunesData() {
        Alamofire.request(url, method: .get, parameters: params).responseJSON { response in
            if response.result.isSuccess {
                let json = JSON(response.result.value)
                self.songItems = AllSongsModel(json: json).allSongsArray
            } else {
                
            }}
    }
}

Solution

  • I think you are confused about how async code works. This code:

    override func viewDidLoad() {
        super.viewDidLoad()
        //#1
        getItunesData()
        //#4
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UINib(nibName: "customCell", bundle: nil), forCellReuseIdentifier: "customCell")
    }
    
    func getItunesData() {
        //#2
        Alamofire.request(url, method: .get, parameters: params).responseJSON 
        //-----------------------------------------
        //The code in the braces below does not execute until later.
        { response in
            //#5
            if response.result.isSuccess {
                let json = JSON(response.result.value)
                self.songItems = AllSongsModel(json: json).allSongsArray
                //#6: Add a call to reloadData() here, once the data is ready.
                tableView.reloadData()
    
            } else {
                
            }
        }
        //-----------------------------------------
        //#3
    }
    

    Executes as follows:

    #1, You are in viewDidLoad, about to call getItunesData().

    #2, Inside getItunesData() you call Alamofire.request(), passing in a completion handler that the request function saves away to call later, once the request is completed. The code in braces, which I have enclosed with //---------- dashes, is saved away for later, and does not execute rigtht away.

    Next, execution moves on to #3. (The code after the Alamofire.request() call.) There's no code there, so the getItunesData() function returns. The network call triggered by the Alamofire.request() call still hasn't happened yet, and your closure code still hasn't executed.

    #4, the lines in viewDidLoad() execute next. Your entire viewDidLoad() function completes and returns. The network call from your Alamofire.request() (and it's completion handler) still has not happened.

    The system calls your tableView.numberOfRowsInSection(_:) function now, before you have installed your new song items into songItems, so songItems.count == 0. UIKit thinks your table view is empty, and displays it as empty.

    At some future time, probably several seconds later, the network call your Alamofire.request() triggered completes, and Alamofire calls the closure you passed it. NOW your closure code at #5 executes. Assuming the network call succeeds, you install the results into self.songItems

    As Sh_Khan points out, you should put a call to tableView.reloadData() in your completion handler (#6 in the code above)

    Without adding the call to tableView.reloadData() in your completion handler, you never tell the table view that there is now data to display, and it appears empty.

    Edit:

    Think of an async call like this:

    You're making dinner and you realize you need breadcrumbs. You ask your kid to go to the store for them. They head off to the store. You don't immediately reach for the not-yet-acquired breadcrumbs as soon as you ask your kid to get them.

    You go on with other tasks and wait for them to return. The kid running an errand is the async task. The "Here's your breadcrumbs" from your kid is the completion handler firing. In the completion handler, you go back and finish that part of the recipe that needed the breadcrumbs.

    #2 in the code above (the call to Alamofire.request) is you asking your kid to pick up some breadcrumbs.

    #3 is you putting the cooking step that needs bread-crumbs aside and moving on to doing other things.

    #6 (the code inside the completion handler) is when you kid comes back and says "here are the breadcrumbs you asked for." In #6, you take the breadcrumbs and use them to finish that step that you were waiting for.