How to set a variable that runs on background thread and needs to access ui

I am fetching a data source in background. There are 2 urls and I choose it with a tabBar. To know which url I need to access, I use navigationController?.tabBarItem.tag. But It throws an error of "navigationController must be used from main thread only". I’ve tried to wrap it with DispatchQueue.main.async but it didn’t work. Any fix or new approach appreciated.

override func viewDidLoad() {
    super.viewDidLoad()
    performSelector(inBackground: #selector(fetchJSON), with: nil)
}

@objc func fetchJSON() {
    let urlString: String
    
    if navigationController?.tabBarItem.tag == 0 {a
        urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
    } else {
        urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
    }
    
    if let url = URL(string: urlString) {
        if let data = try? Data(contentsOf: url) {
            parse(json: data)
            return
        }
    }
    performSelector(onMainThread: #selector(showError), with: nil, waitUntilDone: false)
}

>Solution :

Move the logic that needs to access the UI to the main thread, then pass the result as an argument to your function on the background thread.

Here, there’s several issues:

  • The performSelector(…) methods are quite low-level and not a good solution with Swift. Avoid these, they have issues and make it cumbersome to pass arguments around. Use GCD or async/await instead.
  • Using the synchronous Data(contentsOf: …) is also not a good idea. If you would asynchronous solutions you wouldn’t run into the threading issue in the first place.

I really suggest you look into the second problem (e.g. using a DataTask), as it completely eliminates your threading issues, but here’s a simple way to refactor your existing code using GCD that should already work:

override func viewDidLoad() {
    super.viewDidLoad()

    let urlString: String
    
    if navigationController?.tabBarItem.tag == 0 {a
        urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
    } else {
        urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
    }

    DispatchQueue.global(qos: .utility).async {
        self.fetchJSON(urlString)
    }
}

func fetchJSON(_ urlString: String) {    
    if let url = URL(string: urlString) {
        if let data = try? Data(contentsOf: url) {
            parse(json: data)
            return
        }
    }

    DispatchQueue.main.async {
        self.showError()
    }
}

Leave a Reply