I'm early in the process of learning Swift/iOS. I'm trying to understand asynchronous requests, and the best way to update a tableview with results when they come in.
I'm making a simple movie list app and using 'themoviedatabase' API. Here is the code which isn't working for me. At the moment it searches for 'star wars the force awakens' and works fine, getting the expected single result from the API. Thumbs up so far.
But when I try to update the tableview with my results... problems!
The following code is part of a function I've created in my SearchVC.swift custom view controller. I'm mentioning this up front as a couple of other posts I've researched (this one and this one) appear to be saying that you should only update the UI from the main thread - I've not quite wrapped my head around whether I'm doing that or not?
SearchVC.swift:
import UIKit
class SearchVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var theResults = [SearchResult]()
@IBOutlet weak var searchField: MaterialTextField!
@IBOutlet weak var searchBtn: MaterialButton!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 106
// do I need to update self.theResults[] and do self.tableView.reloadData() in here?
}
@IBAction func searchBtnPressed(sender: MaterialButton) {
print("Search button pressed!")
performSearch(searchField.text)
}
func performSearch(searchText: String?) {
if let searchString = searchText where searchString != "" {
self.theResults = [] // clear the search results array
print("Searching for: \(searchString)")
let tmdb_apikey = "----just gonna take out my API key----"
let tmdb_query = searchString
let tmdb_query_escaped = tmdb_query.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
let url = NSURL(string: "http://api.themoviedb.org/3/search/movie?query=\(tmdb_query_escaped)&api_key=\(tmdb_apikey)&page=1")!
let request = NSMutableURLRequest(URL: url)
request.addValue("application/json", forHTTPHeaderField: "Accept")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
if let response = response, data = data {
do {
// Convert data to JSON
let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
// Convert JSON into a Dictionary
if let searchDict = json as? Dictionary<String,AnyObject> {
// Iterate through dictionary and add results to the results array
if let searchResults = searchDict["results"] as? [Dictionary<String,AnyObject>] {
for result in searchResults {
if let title = result["title"] as? String, let posterPath = result["poster_path"] as? String, let id = result["id"] as? Int {
let idStr = String(id) // cast id int to string, for storing in our custom class
let thisResult = SearchResult(title: title, posterUrl: posterPath, id: idStr)
print("Results:")
print(thisResult.movieTitle)
self.theResults.append(thisResult) // adds result to array
}
}
print("Num of results to display inside closure: \(self.theResults.count)")
// if I put 'self.tableView.reloadData()' here, which is where I thought it should go (once all the results have been added to my array) I get an error: "This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes. This will cause an exception in a future release." ... and loads of stack dumps!
} else {
print("ERROR: No 'results'?")
}
} else {
print("ERROR: Got data, converted to JSON, but could not convert into a Dictionary.")
}
} catch {
print("ERROR: Could not convert data into JSON")
}
} else {
print(error)
}
}
task.resume()
// This outputs 0, before results are delivered, because the data request is asynchronous? How do I deal with this?!
print("Num of results to display: \(theResults.count)")
self.tableView.reloadData()
} else {
// no search term specified
print("No search term specified. Doing nothing")
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theResults.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = theResults[indexPath.row]
if let cell = tableView.dequeueReusableCellWithIdentifier("SearchResultCell") as? SearchResultCell {
print("Reconfiguring cell")
cell.configureSearchCell(post)
return cell
} else {
print("Configuring cell")
return SearchResultCell()
}
}
configureSearchCell() is defined in a separate SearchResultCell.swift:
import UIKit
class SearchResultCell: UITableViewCell {
@IBOutlet weak var moviePoster: UIImageView!
@IBOutlet weak var movieTitle: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func configureSearchCell(post: SearchResult) {
self.movieTitle.text = post.movieTitle
if post.moviePosterUrl != "" {
// doing nothing here for the moment, just want to get the movie title added to a cell before worrying about adding the poster image!
}
}
}
The performSearch function, for the most part, seems to be working in that it outputs the following:
Search button pressed!
Searching for: star wars force awakens
Num of results to display: 0
Results:
Star Wars: The Force Awakens
Num of results to display inside closure: 1
So I'm getting the results, but cannot update my table, obviously because the call is asynchronous and the 3rd line output there is called while the results are still being processed (and tells me there are 0 results at that point). I get that.
BUT, when I try to call the self.tableView.reloadData()
in the middle of my code, after results have been parsed (where the print() is telling me there's 1 result now), I get more errors as noted in the comment in the code above, warning me about updating autolayout via a background thread!
Either way, nothing is being updated in my tableview. The result isn't added to a cell at all.
How do I wrap my head around what I need to be doing?
I realise I may 'simply' need a primer on asynchronous calls and how to go about updating table views in Swift/xcode, but lots of searching hasn't turned up anything that's solved this for me so far, so here I am. Hope some of you knowledgeable people can take pity on a newbie iOS programmer!
Thanks in advance for any tips.