TableView data from search not updated properly - ios

I made a TableView with Swift where I show Search results of the Query that's filled in the UiTextField.
let cellReuseIdentifier = "cell"
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:SearchCell = self.tableView.dequeueReusableCell(withIdentifier:cellReuseIdentifier) as! SearchCell
cell.objectName.text = self.search_results_list[indexPath.section]
return cell
}
On the Value Changed-event I trigger the Search action:
#IBAction func searchInputChanged(_ sender: Any) {
self.search_results_list = []
let search = self.searchField.text
let parameters: Parameters = ["api_token": token, "search": search!+" "]
let URL = "https://myweb.app/api/search"
Alamofire.request(URL, parameters: parameters).responseArray { (response: DataResponse<[SearchResponse]>) in
let searchResultsArray = response.result.value
if let searchResultsArray = searchResultsArray {
for searchResult in searchResultsArray {
self.search_results_list.append(searchResult.description!)
}
}
// Then I reloeadData of the TableView
self.tableView.reloadData()
}
}
And the NumberOfSections method:
func numberOfSections(in tableView: UITableView) -> Int {
return self.search_results_list.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
It does work, but when I fill in the form quickly, the Array has the correct order, but the TableView has a different order. When I inspect the Array I see that the order IS correct, but the TableView shows a different one. When I wait for a second and add a space at the end, the TableView does show the correct order.
The same when I add letter by letter and wait after each letter that the tableview is reloaded, the output is correct
What am I missing?

Like they said in the comments. It's probably because the asynchronous calls are returning at different times. And most probably the one with fewer results (newer calls) will come back faster than the ones that have more results (older calls). So what you have to do is invalidate every API response which doesn't match your current search text.
#IBAction func searchInputChanged(_ sender: Any) {
self.search_results_list = []
let search = self.searchField.text
let parameters: Parameters = ["api_token": token, "search": search!+" "]
let URL = "https://myweb.app/api/search"
Alamofire.request(URL, parameters: parameters).responseArray { (response: DataResponse<[SearchResponse]>) in
if search == self.searchTextField.text { // Compare search text with current text in searchTextField.
let searchResultsArray = response.result.value
if let searchResultsArray = searchResultsArray {
for searchResult in searchResultsArray {
self.search_results_list.append(searchResult.description!)
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
IMPORTANT OPTIMIZATION TIP
You can wrap your API call in a timer so that it is hit only when the user stops entering characters into the text field. This helps avoid unnecessary API calls. You can achieve this by using a Timer to make the network calls. Start the timer in textDidChange and invalidate it if the delegate is called again within a short duration (0.5s or whatever you feel is optimal)

Related

Why JSON data from decoder to become UITableView datasource does not assigns?

Recently got stuck on a problem of assigning freshly downloaded JSON data to table view datasource variable. I suppose the problem is something obvious but my skill is not enough to gather the big picture. Let me share a bunch of code.
(1) A function retrieves the data from Open Weather Map API (defined in the separate class 'GetWeather').
func getMowForecast(completion: #escaping ((WeatherForecast?, Bool)) -> Void) {
let url = URL(string: "http://api.openweathermap.org/data/2.5/forecast?id=524901&APPID=b3d57a41f87619daf456bfefa990fce4&units=metric")!
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
let json = try JSONDecoder().decode(WeatherForecast.self, from: data)
completion((json, true))
} catch {
print(error)
completion((nil, false))
}
} else {
print(error)
}
}
task.resume()
}
Everything works fine here. JSON loads correctly and fits the data model.
Here's a link to JSON data to be displayed in tableView: https://pastebin.com/KkXwxYgS
(2) A controller handles the display of retrieved JSON data in tableView format
import UIKit
class ForecastViewController: UITableViewController {
#IBOutlet weak var tableV: UITableView! // tableView outlet in the IB
let weatherGetter = GetWeather() // object to handle the JSON retrieval
var tableData: WeatherForecast? // tableView data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData?.list.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableVCCell
cell.dateLabel.text = "\(self.tableData?.list[indexPath.row].dt)"
cell.tempLabel.text = "\(self.tableData?.list[indexPath.row].main.temp)"
cell.feelsLikeLabel.text = "\(self.tableData?.list[indexPath.row].main.feels_like)"
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
tableV.delegate = self
tableV.dataSource = self
weatherGetter.getMowForecast { (data, status) in
if let data = data, status {
} else if status {
print("-------- Ошибка разбора данных прогноза погоды --------")
} else {
print("-------- Ошибка получения данных прогноза погоды --------")
}
self.tableData = data
print(self.tableData)
}
print(self.tableData?.list.count) // returns nil
self.tableData = weatherGetter.getMowForecast(completion: ((tableData, true))) // error - Cannot convert value of type '(WeatherForecast?, Bool)' to expected argument type '((WeatherForecast?, Bool)) -> Void'
}
}
The problem is - the table view gets nil datasource so it is unable to load the data and shows the blank screen.
I suppose the mistake is in scope - I try to retrieve the JSON data inside a function and it does not go anywhere else. What I am wondering about is - how comes that assigning the data to self.tableData does not makes any effect?
Could you please help.
Thank you!
Regards
First of all delete
print(self.tableData?.list.count) // returns nil
self.tableData = weatherGetter.getMowForecast(completion: ((tableData, true))) // error - Cannot convert value of type '(WeatherForecast?, Bool)' to expected argument type '((WeatherForecast?, Bool)) -> Void'
The error occurs because the method does not return anything and the completion handler syntax is wrong. Both lines are pointless anyway due to the asynchronous behavior of getMowForecast
Secondly I recommend to declare the data source array as a non-optional array of the type which represents List. Then you get rid of all those unnecessary optionals.
var tableData = [List]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableVCCell
let weatherData = self.tableData[indexPath.row]
cell.dateLabel.text = "\(weatherData.dt)"
cell.tempLabel.text = "\(weatherData.main.temp)"
cell.feelsLikeLabel.text = "\(weatherData.main.feels_like)"
return cell
}
To be able to display the data – as already mentioned by others – you have to reload the table view in the completion handler. And assign the data only if status is true.
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
tableV.delegate = self
tableV.dataSource = self
weatherGetter.getMowForecast { [weak self] (data, status) in
if let data = data, status {
self?.tableData = data.list
DispatchQueue.main.async {
self?.tableV.reloadData()
}
} else if status {
print("-------- Ошибка разбора данных прогноза погоды --------")
} else {
print("-------- Ошибка получения данных прогноза погоды --------")
}
}
}
And consider that the message Ошибка разбора данных прогноза погоды will be never displayed.
You need to reload the table inside the callback as it's asynchronous
self.tableData = data
print(self.tableData)
DispatchQueue.main.async { self.tableV.reloadData() }

iOS OperationQueue: Concurrent asynchronous operations block screen

I'm quite new to multi-threading in iOS, and have some difficulties with the implementation of my task. I'd really appreciate any help from more experienced programmers.
I have one URL address, as a starting point, to which I make a request, check some info, and retrieve all the URL addresses contained on that web-page. After this, I need to do the same with new URL addresses again and again, until a specific number of URLs is checked.
I decided to go with Operations and OperationQueues, because I also need to be able to choose a maximum number of concurrent operations.
I created a custom async operation LoadOperation and applied it to each URL stored in an array. In the completion block, I add new URLs to the array, and call the same function, kind of recursively.
Also I created two queues:
A concurrent one for requests,
and
A serial one for accessing shared properties as follows:
class Queue<Item: Equatable> {
// concurrent queue for URL-requests
let loadOperationQueue = OperationQueue() // qos: .utilities
// serial operation queue for accessing shared properties
let serialOperationQueue = OperationQueue() // qos: .utilities
// maximum number of URLs that need to be checked ultimately
var maxNumberOfItems: Int = 1
var pendingItems: [Item] = [] // newly added URLs
var loadingItems: [Item] = [] // loading URLs
var processedItems: [Item] = [] // URLs that were checked
// all URLs for tableView dataSource
var allItems: [Item] {
return processedItems + loadingItems + pendingItems
}
func addToPending(_ item: Item) {
guard allItems.count < maxNumberOfItems else {return}
pendingItems.append(item)
}
func removeFromPending() -> Item? {
guard !self.pendingItems.isEmpty else { return nil }
let item = pendingItems.removeFirst()
loadingItems.append(item)
return item
}
func addToProcessed(_ item: Item) {
loadingItems = loadingItems.filter{$0 != item}
processedItems.append(item)
}
func isFull() -> Bool {
return allItems.count >= maxNumberOfItems
}
}
This is my searching function:
func startSearching() {
// iterate over array of URLs creating async operation
while let currentWebPage = queue.removeFromPending() {
let loadOperation = LoadOperation(currentWebPage, searchString: searchString)
loadOperation.completionBlock = {
let webPage = loadOperation.output!
self.queue.serialOperationQueue.addOperation {
// add checked URL to processed
self.queue.addToProcessed(webPage)
// add new urls to an array of pending URLs
for urlString in webPage.containedLinks {
//check if the limit of checked urls is not exceeded
guard !self.queue.isFull() else { return }
//check if this url is already in queue
guard !self.queue.allItems.contains(where: {$0.url == urlString}) else { continue }
self.queue.addToPending(WebPage(url: urlString, containedLinks: [], status: .pending))
}
// update UI
OperationQueue.main.addOperation {
self.viewDelegate?.reloadTable()
}
// repeat searching process with new urls
self.startSearching()
}
}
queue.loadOperationQueue.addOperation(loadOperation)
}
}
I can't figure out why:
When I run the app, the screen freezes. Even though all my queues are in the background (qos: utilities).
Sometimes when I try to scroll UITableView, I get sigabort due to index out of range, even though I try to access all properties in serial queue.
This is a data source code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return presenter.getNumberOfRows()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "WebPageTableViewCell", for: indexPath) as! WebPageTableViewCell
let (url, status) = presenter.getCellContent(at: indexPath.row)
cell.addressLabel.text = url
cell.statusLabel.text = status
return cell
}
And functions from presenter:
func getNumberOfRows() -> Int {
return queue.allItems.count
}
func getCellContent(at index: Int) -> (url: String, status: String) {
return (url: queue.allItems[index].url, status: queue.allItems[index].status.description)
}

Dynamically populating an iOS table view with Swift from an API

I'm currently in the process of creating an app to display the latest football scores. I've connected to an API through a URL and pulled back the team names for the english premier league into an array of strings.
The problem seems to come from populating the iOS table view that I intend to display the list of teams with. The data appears to be pulled from the API fine, but for some reason the TableView method which creates a cell and returns it doesn't seem to be called. The only time I can get the method to be called is when I actually hard code a value into the array of team names.
Here is my code:
class Main: UIViewController {
var names = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let URL_String = "https://football-api.com/api/?Action=standings&APIKey=[API_KEY_REMOVED]&comp_id=1204"
let url = NSURL(string: URL_String)
let urlRequest = NSURLRequest(URL: url!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
}
} catch {
print("error serializing JSON: \(error)")
}
})
task.resume()
}
// Number of Sections In Table
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
// Number of Rows in each Section
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
// Sets the content of each cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
Just wondering if anyone can point me in the right direction here. This code doesn't crash or throw any errors, it just refuses to load a table view. The only reason I can possibly think of is that the array of team names is empty after completing a request to the API. However I've set breakpoints throughout and checked the values of local variables and the desired information is being pulled from the API as intended...
you are in the correct way , just refresh the table using reloadData once you got the new data from API
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.yourtableViewname.reloadData()
})
}

Populating table with API response Swift

I'm trying to get search results to display on a tableView. I believe I have correctly parsed the JSON, the only problem is that the results won't display on my tableView.
Here is the code:
var searchText : String! {
didSet {
getSearchResults(searchText)
}
}
var itemsArray = [[String:AnyObject]]()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.reloadData()
}
// MARK: - Get data
func getSearchResults(text: String) {
if let excapedText = text.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) {
Alamofire.request(.GET, "https://api.duckduckgo.com/?q=\(excapedText)&format=json")
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error \(response.result.error!)")
return
}
let items = JSON(response.result.value!)
if let relatedTopics = items["RelatedTopics"].arrayObject {
self.itemsArray = relatedTopics as! [[String:AnyObject]]
}
if self.itemsArray.count > 0 {
self.tableView.reloadData()
}
}
}
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 6 // itemsArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchResultCell", forIndexPath: indexPath) as! SearchResultCell
if itemsArray.count > 0 {
var dict = itemsArray[indexPath.row]
cell.resultLabel?.text = dict["Text"] as? String
} else {
print("Results not loaded yet")
}
return cell
}
If I had a static API request I think this code would work because I could fetch in the viewDidLoad and avoid a lot of the .isEmpty checks.
When I run the program I get 6 Results not loaded yet (from my print in cellForRowAtIndexPath).
When the completion handler is called response in, it goes down to self.items.count > 3 (which passes) then hits self.tableView.reloadData() which does nothing (I checked by putting a breakpoint on it).
What is the problem with my code?
Edit
if self.itemsArray.count > 0 {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
Tried this but the tableView still did not reload even though its reloading 6 times before the alamofire hander is called...
Here is the strange thing, obviously before the hander is called my itemsArray.count is going to be 0 so that's why I get Results not loaded yet. I figured out why it repeats 6 times though; I set it in numberOfRowsInSection... So #Rob, I can't check dict["Text"] or cell.resultLabel?.text because they're never getting called. "Text" is correct though, here is the link to the JSON: http://api.duckduckgo.com/?q=DuckDuckGo&format=json&pretty=1
Also, I do have the label linked up to a custom cell class SearchResultCell
Lastly, I am getting visible results.
Two problems.
One issue is prepareForSegue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let searchResultTVC = SearchResultsTVC()
searchResultTVC.searchText = searchField.text
}
That's not using the "destination" view controller that was already instantiated, but rather creating a second SearchResultsTVC, setting its searchText and then letting it fall out of scope and be deallocated, losing the search text in the process.
Instead, you want:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let searchResultTVC = segue.destination as? SearchResultsTVC {
searchResultTVC.searchText = searchField.text
}
}
You shouldn't rely on didSet in the destination view controller to trigger the search, because that property is getting set by source view controller before the table view has even been instantiated. You do not want to initiate the search until view has loaded (viewDidLoad).
I would advise replacing the didSet logic and just perform search in viewDidLoad of that SearchResultsTVC.
My original answer, discussing the code provided in the original question is below.
--
I used the code originally provided in the question and it worked fine. Personally, I might streamline it further:
eliminate the rid of the hard coded "6" in numberOfRowsInSection, because that's going to give you false positive errors in the console;
the percent escaping not quite right (certain characters are going to slip past, unescaped); rather than dwelling on the correct way to do this yourself, it's better to just let Alamofire do that for you, using parameters;
I'd personally eliminate SwiftyJSON as it's not offering any value ... Alamofire already did the JSON parsing for us.
Anyway, my simplified rendition looks like:
class ViewController: UITableViewController {
var searchText : String!
override func viewDidLoad() {
super.viewDidLoad()
getSearchResults("DuckDuckGo")
}
var itemsArray: [[String:AnyObject]]?
func getSearchResults(text: String) {
let parameters = ["q": text, "format" : "json"]
Alamofire.request("https://api.duckduckgo.com/", parameters: parameters)
.responseJSON { response in
guard response.result.error == nil else {
print("error \(response.result.error!)")
return
}
self.itemsArray = response.result.value?["RelatedTopics"] as? [[String:AnyObject]]
self.tableView.reloadData()
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemsArray?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SearchResultCell", for: indexPath) as! SearchResultCell
let dict = itemsArray?[indexPath.row]
cell.resultLabel?.text = dict?["Text"] as? String
return cell
}
}
When I did that, I got the following:
The problem must rest elsewhere. Perhaps it's in the storyboard. Perhaps it's in the code in which searchText is updated that you didn't share with us (which triggers the query via didSet). It's hard to say. But it doesn't appear to be a problem in the code snippet you provided.
But when doing your debugging, make sure you don't conflate the first time the table view delegate methods are called and the second time they are, as triggered by the responseJSON block. By eliminating the hardcoded "6" in numberOfRowsInSection, that will reduce some of those false positives.
I think you should edit :
func getSearchResults(text: String) {
if let excapedText = text.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) {
Alamofire.request(.GET, "https://api.duckduckgo.com/?q=\(excapedText)&format=json")
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error \(response.result.error!)")
return
}
let items = JSON(response.result.value!)
if let relatedTopics = items["RelatedTopics"].arrayObject {
self.itemsArray = relatedTopics as! [[String:AnyObject]]
// if have result data -> reload , & no if no
if self.itemsArray.count > 0 {
self.tableView.reloadData()
}
}else{
print("Results not loaded yet")
}
}
}
}
And
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchResultCell", forIndexPath: indexPath) as! SearchResultCell
// i 'm sure: itemsArray.count > 0 in here if in numberOfRowsInSection return itemsArray.count
var dict = itemsArray[indexPath.row]
cell.resultLabel?.text = dict["Text"] as? String
return cell
}
And you should share json result(format) ,print dict in cellForRowAtIndexPath, so it s easy for help

Can't retain array data outside of method in SWIFT

I am storing the category name from a JSON in an Array using alamofire .
The array has values only when it is called from this Method CategoryNameFunc.
If i call the the array from the tableview or any other method it always returns 0
CODE
var CategoryNameArray : [String] = []
override func viewDidLoad() {
super.viewDidLoad()
Network()
tester()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return CategoryNameArray.count // This returns 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : UITableViewCell = self.TableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
println(self.CategoryNameArray[indexPath.row])
cell.textLabel?.text = "Hello"
return cell
}
func Network(){
Alamofire.request(.GET, "http://www.wive.com/index.php/capp/category_list")
.responseJSON { (_, _, data, _) in
let json = JSON(data!)
let count = json.count
self.CategoryNameFunc(json, Count: count) }
}
func CategoryNameFunc(Json: JSON, Count: Int)
{
for index in 0...Count-1 {
let name = Json[index]["CATEGORY_NAME"].string
CategoryNameArray.append(name!)
}
// This returns 23 (The correct value)
println(self.CategoryNameArray.count)
}
When you called Network() function it creates a new thread (Alamofire start an asynchronous request) and your tester() function is not waiting for your Network() function to finish before you count your CategoryNameArray().But your CategoryNameFunc() function waits for network operation to finish.
I am not sure (didn't use Almofire) but it think, this happens because the method Network, more precisly the Almofire request is fired asynchronously.
So, the methods Network() and tester() are running simultaneously, but because Network() needs to fetch data first, tester() is faster and is executed first.
The proper way to execute tester() and Network() one after another would be:
func CategoryNameFunc(Json: JSON, Count: Int)
{
for index in 0...Count-1 {
let name = Json[index]["CATEGORY_NAME"].string
CategoryNameArray.append(name!)
}
// run tester, AFTER you have the data.
tester()
}

Resources