I'm struggling with multithreading in news app. The thing is - my application freezes often when I scroll table view after data was parsed and loaded and its way too often. I think I'm some kind of wrong of reloading data every time.
First part:
final let urlString = "http://api.to.parse"
Here I create array of structs to fill in my data
struct jsonObjects {
var id : Int
var date : String
var title : String
var imageURL : URL
}
var jsonData = [jsonObjects]()
Here's my viewDidLoad of tableView
override func viewDidLoad() {
super.viewDidLoad()
// MARK : - Download JSON info on start
JsonManager.downloadJsonWithURL(urlString: urlString, сompletion: {(jsonArray) -> Void in
guard let data = jsonArray else { print("Empty dude"); return;}
for jsonObject in data {
if let objectsDict = jsonObject as? NSDictionary {
guard
let id = objectsDict.value(forKey: "id") as? Int,
let date = objectsDict.value(forKey: "date") as? String,
let titleUnparsed = objectsDict.value(forKey: "title") as? NSDictionary,
let title = (titleUnparsed as NSDictionary).value(forKey: "rendered") as? String,
let imageString = objectsDict.value(forKey: "featured_image_url") as? String,
let imageURL = NSURL(string: imageString) as URL?
else {
print("Error connecting to server")
return
}
There I go with appending filled structure to array:
self.jsonData.append(jsonObjects(id: id, date: date, title: title,
imageURL: imageURL))
}
}
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
})
and downloadJsonWithURL is simply:
class JsonManager {
class func downloadJsonWithURL(urlString: String, сompletion: #escaping (NSArray?) -> Void) {
guard let url = NSURL(string: urlString) else { print("There is no connection to the internet"); return;}
URLSession.shared.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
guard let parseData = data else { print("There is no data"); return;}
if let jsonObj = try? JSONSerialization.jsonObject(with: parseData, options: .allowFragments)
as? NSArray {
сompletion(jsonObj)
}
}).resume()
}
And finally - I input that in my TableViewCell:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return jsonData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "newscell") as? NewsTableViewCell else {
fatalError("Could not find cell by identifier")
}
guard let imageData = NSData(contentsOf: jsonData[indexPath.row].imageURL) else {
fatalError("Could not find image")
}
cell.newsTitleLabel.text = self.jsonData[indexPath.row].title
cell.newsTitleLabel.font = UIFont.boldSystemFont(ofSize: 20.0)
cell.newsImageView.image = UIImage(data: imageData as Data)
return cell
}
So there are two questions: how should I distribute my threads and how should I call them so that I have smooth and nice tableview with all downloaded data? and how should I reload data in cell?
Your issue is caused by the imageData its blocking the main thread. The best way to solve this is to download all the images into an image cache. And I would most certainly remove the downloading of images from within the cellForRowAtIndexPath.
Downloading data, parsing in background thread, the updating the UI on main-thread.
Basically if you do correctly like this, everything will be okay.
So you may need to double check one more time if you are rendering UI on main-thread.
On the debugging panel, there's pause/play button.
So whenever your app frozen, try to pause the app immediately:
1) Then check if any of your UI method is running on background-thread.
2) Check if your downloading task or parsing json doing on main-thread.
If it falls under above cases, it needs to be correct.
Related
I'm trying to find a way to parse through some Json data on reddit and display the information in a table view. (https://api.reddit.com).
So far this is what my code looks like:
var names: [String] = []
var comment: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://api.reddit.com")
do{
let reddit = try Data(contentsOf: url!)
let redditAll = try JSONSerialization.jsonObject(with: reddit, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String : AnyObject]
if let theJSON = redditAll["children"] as? [AnyObject]{
for child in 0...theJSON.count-1 {
let redditObject = theJSON[child] as! [String : AnyObject]
names.append(redditObject["name"] as! String)
}
}
print(names)
}
catch{
print(error)
}
}
//Table View
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
//Configure cells...
cell.textLabel?.text = names[indexPath.row]
cell.detailTextLabel?.text = comments[indexPath.row]
return cell
}
I know for a fact, the information is actually coming through the "redditALL" constant but i'm not sure what i'm doing incorrect after the JSONSerialization.
Also, i would really appreciate it if there was some kind of link to help me understand JSON Parsing in swift better, Thanks.
First of don't use Data(contentsOf:) to get JSON from URL because it will block your Main thread instead of that use URLSession.
Now to retrieve your children array you need to first access data dictionary because children is inside it. So try like this way.
let url = URL(string: "https://api.reddit.com")
let task = Session.dataTask(with: url!) { data, response, error in
if error != nil{
print(error.)
}
else
{
if let redditAll = (try? JSONSerialization.jsonObject(with: reddit, options: []) as? [String : Any],
let dataDic = redditAll["data"] as? [String:Any],
let children = dataDic["children"] as? [[String:Any]] {
for child in children {
if let name = child["name"] as? String {
names.append(name)
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
task.resume()
JSON parsing in Swift (Foundation) is dirt-simple. You call JSONSerialization.jsonObject(with:) and you get back an "object graph". Usually it's a dictionary or array containing other objects. You have to know about the format of the data you're getting in order to cast the results to the proper types and walk the object graph. If you cast wrong your code will fail to run as expected. You should show us your JSON data. It's likely there is a mismatch between your JASON and your code.
My app has a user account page that uses a tableview to return a timeline type detail. I'd like to have the actual logged-in user detail at the top in a table header.
This is handled on the useraccountViewController. I have added a function to the viewDidLoad called getUserAndTimeLine() which makes 2 api calls to my web-service to get the details for my logged in user (this view controller is opened by a segue from the login page).
I also have 2 structs one for the loggedUser, and one for the userPost that define what I am expecting to receive from the API calls. I also have arrays for the user and the posts based on the structs.
The getUserAndTimeLine() code is as follows:
//creates api url to get user
let url = URL(string: "myapi.url/getuser?uid=" + user)
//httpget sent to the api and listens for response
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print(error as Any)
}
else {
do{
//parses the json data
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! NSDictionary
//get the user data
let thisUser = parsedData["userName"] as? String
let thisUserImage = parsedData["userAvatar"] as? String
let thisLoggedUser = loggedUser( userAvatar: thisUserImage, userName: thisUser)
self.arrayOfLoggedUser.append(thisLoggedUser)
}
catch{
print("parse error")
}
}
}.resume()
//creates api url to get user TL
let tlurl = URL(string: "myapi.url?uid=" + user)
//httpget sent to the api and listens for response
URLSession.shared.dataTask(with:tlurl!) { (data, response, error) in
if error != nil {
print(error as Any)
}
else {
do{
//parses the json data
let parsedTLData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:AnyObject]
guard let posts = parsedTLData["timeline"] as? [[String:AnyObject]] else {return}
for post in posts {
let thisPost = postData(postText: post["postText"] as! String, postURL: post["postURL"] as! String, postImageURL: post["postImage"] as! String,
postDomainName: post["postSource"] as! String,
postDomainLink: post["postDomainLink"] as! String,
postPoster: post["postPoster"] as! String,
postPosterAvatar: post["postPosterAvatar"] as! String,
postDate: post["postCreateDate"] as! String)
self.arrayOfPostData.append(thisPost)
self.tableView.reloadData() }
}
catch{
print("parse error")
}
}
}.resume()
In the individual cells, I am successfully able to populate my tableview (timeline) with the data from arrayOfPostData via:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("postTimeLineCell", owner: self, options: nil)?.first as! postTimeLineCell
cell.postImage.sd_setImage(with: URL(string: arrayOfPostData[indexPath.row].postImageURL))
cell.postText.setTitle(arrayOfPost[indexPath.row].postText, for:.normal )
cell.postDate.text = arrayOfPostData[indexPath.row].postDate
// etc.
return cell
}
However I cannot return the header:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let loggedUserHeader = Bundle.main.loadNibNamed("userHeader", owner: self, options: nil)?.first as! userHeader
loggedUserHeader.userName.text = arrayOfLoggedUser[section].userName
return loggedUserHeader
}
as I get an error: EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0) which in the output is "fatal error: Index out of range"
Can anyone help out as I'm rather stuck!
Asynchronous data is almost always the problem.
The user array will be populated after the API call has completed - but you will be refreshing the tableView before that.
Simplest thing is to put a check in viewForHeaderInSection and if the array.count < section index then just return with nothing, or a default value
I have built my own swift framework for running queries on a MySQL database. I am currently developing a chat application in which I need to display an avatar image for each user who is currently chatting.
I am retrieving the user's profile picture URL from my database and then I need to return a UIImage once I get the profile picture URL. I get the error Unexpected non-void return value in void function. Is there a way to make this easier or bypass this error?
This is my full code:
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
let currentMessage = self.messages[indexPath.row]
if (currentMessage.senderId == self.senderId) {
// Current User sent message
var imageURL: NSURL!
ConnectionManager.sharedInstance.complexQuery("SELECT profile_picture FROM users WHERE username='\(self.senderId)' ", completion: { (result) in
if let results = result as? [[String: AnyObject]] {
for result in results {
if let profilePictureURL = result["profile_picture"] {
print(profilePictureURL)
imageURL = profilePictureURL as! NSURL
let imageData = NSData(contentsOfURL: imageURL)
let image = UIImage(data: imageData!)
// ERROR HERE (line below)
return JSQMessagesAvatarImageFactory.avatarImageWithImage(image, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
}
}
}
})
} else {
var imageURL: NSURL!
ConnectionManager.sharedInstance.complexQuery("SELECT profile_picture FROM users WHERE username='\(currentMessage.senderId)' ", completion: { (result) in
if let results = result as? [[String: AnyObject]] {
for result in results {
if let profilePictureURL = result["profile_picture"] {
print(profilePictureURL)
imageURL = profilePictureURL as! NSURL
let imageData = NSData(contentsOfURL: imageURL)
let image = UIImage(data: imageData!)
// ERROR HERE (line below)
return JSQMessagesAvatarImageFactory.avatarImageWithImage(image, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
}
}
}
})
}
}
It seems that ConnectionManager.sharedInstance.complexQuery is an asynchronous function, so the main thread will continue after calling it, there will be nothing left in this context to receive what will be returned
so, do what ever you want with the image in the callback function "completion"
for example, you may get a reference of your UIImageView and set it's image when you get it, but you need to use another data source delegate method
collectionView(_:cellForItemAtIndexPath:)
Something like this:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(YOUR_ID,
forIndexPath: indexPath) as! YourOwnCell
ConnectionManager.sharedInstance.complexQuery("SELECT profile_picture FROM users WHERE username='\(currentMessage.senderId)' ", completion: { (result) in
if let results = result as? [[String: AnyObject]] {
for result in results {
if let profilePictureURL = result["profile_picture"] {
print(profilePictureURL)
imageURL = profilePictureURL as! NSURL
let imageData = NSData(contentsOfURL: imageURL)
let image = UIImage(data: imageData!)
// Assuming avatarImageWithImage is returning UIImageView
cell.imageView = JSQMessagesAvatarImageFactory.avatarImageWithImage(image, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
}
}
}
})
return cell;
}
What you try isn't how asynchronous functions work. It's not how they could work. The whole point of an asynchronous call is that your app can continue running while the asynchronous call continues running in the background. Your complexQuery function returns immediately so that you can continue.
Instead of this function returning the value that you want, your completion code must deposit the value in the right place.
I'm populating my tableView with JSON data, most of the time the data shows but for some strange reason other times it doesn't. I tested the JSON data in Chrome and the info is there. I also made print statements to print the info after it has downloaded and it appears to download correctly. I can't figure out why 80% of the time the data populates the tableView correctly and 20% of the time it doesn't. Here is a sample of my code, there are many more cells but I shortened it to 2 for this example:
var task : NSURLSessionTask?
var newURL : String?
var bannerArray: [String] = []
var overViewArray: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
getJSON(newURL!)
}
func getJSON (urlString: String) {
let url = NSURL(string: urlString)!
let session = NSURLSession.sharedSession()
task = session.dataTaskWithURL(url) {(data, response, error) in
dispatch_async(dispatch_get_main_queue()) {
if (error == nil) {
self.updateDetailShowInfo(data)
}
else {
"Not getting JSON"
}
}
}
task!.resume()
}
func updateDetailShowInfo (data: NSData!) {
do {
let jsonResult = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
guard let banner = jsonResult["banner"] as? String,
let overview = jsonResult["overview"] as? String
else { return }
_ = ""
print(overview)
bannerArray.append(banner)
overViewArray.append(overview)
}
catch {
print("It ain't working")
}
self.DetailTvTableView.reloadData()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return bannerArray.count
case 1: return overViewArray.count
default: fatalError("Unknown Selection")
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCellWithIdentifier("bannerCell", forIndexPath: indexPath) as! BannerCell
cell.bannerImage.sd_setImageWithURL(NSURL(string: bannerArray[indexPath.row]))
self.DetailTvTableView.rowHeight = 100
DetailTvTableView.allowsSelection = false
return cell
case 1:
let cell = tableView.dequeueReusableCellWithIdentifier("overviewCell", forIndexPath: indexPath) as! OverviewCell
let overViewText = overViewArray[indexPath.row]
if overViewText != "" {
cell.overView.text = overViewText
} else {
cell.overView.text = "N/A"
}
self.DetailTvTableView.rowHeight = 200
print(overViewArray[indexPath.row])
return cell
default: ""
}
return cell
}
I'm just doing this off the web. And I think there are some errors. You need to debug them yourself.
Your understanding of fetching the JSON and GCD is totally wrong. I believe these codes you got somewhere off the web. Go read up what is dispatch_async.
Basically, you need to create session to fetch JSON data, which you have done it correctly, however, within the NSJSONSerialization, you need to store them in a variable and append it to your array. This is fetched asynchronously. Your dispatch_async will reload data serially.
func getJSON (urlString: String) {
let url = NSURL(string: urlString)!
let session = NSURLSession.sharedSession()
task = session.dataTaskWithURL(url) {(data, response, error) in
let jsonResult = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
guard let banner = jsonResult["banner"] as? String,
let overview = jsonResult["overview"] as? String
bannerArray.append(banner)
overViewArray.append(overview)
} dispatch_async(dispatch_get_main_queue()) {
if (error == nil) {
self.DetailTvTableView.reloadData()
}
else {
"Not getting JSON"
}
}
catch {
print("It ain't working")
}
}
}
task!.resume()
}
I need help on how to solve this problem.
I tried to fetch data using json but when I tried to view in Table View its not showing.
I used the code below to test if table view is working and it works!
// self.clientList = ["Mango", "Banana", "Orange", "Guava", "Grapes"]
I used the code below to test if there's data returned from json. Still it works.
for item in jsonClientList {
let firstName = item["firstName"]
//Printing is working
print(firstName as! String)
}
Line not working! I dont know why. Its inside of loop but to data upon loading the table view.
Thanks in advance.
self.clientList.append(firstName as! String)
//---------------------------------
var clientList:[String] = []
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.clientList.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "tblClientList")
cell.textLabel?.text = self.clientList[indexPath.row]
return cell
}
internal func jsonParser() -> Void{
//-------------------------
let postEndpoint: String = "http://domain.com/client"
let url = NSURL(string: postEndpoint)
let session = NSURLSession.sharedSession()
session.dataTaskWithURL(url!, completionHandler:
{
(data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
do{
let ipString = NSString(data:data!, encoding: NSUTF8StringEncoding)
if (ipString != nil) {
let jsonClientList = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSArray
for item in jsonClientList {
let firstName = item["firstName"]
//I tried to print the data from json and its working!
print(firstName as! String)
//Line not working
//I tried to insert the firstName to clientList array
self.clientList.append(firstName as! String)
}
}
//If I use this its working
// self.clientList = ["Mango", "Banana", "Orange", "Guava", "Grapes"]
}
} catch{
print("Something bad happed!")
}
}
).resume()
//--------------
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
jsonParser()
}
//---------------------------------
you forget to refresh the new data to show in table, do like
self.clientList.append(firstName as! String)
}
dispatch_async(dispatch_get_main_queue())
self.yourtableViewname.reloadData()
}
As mentioned in the other answer the issue is that the table view needs to be reloaded.
In Swift there is a more convenient way to populate the data source array without repeat loop using the map function.
It assumes – like in the question – that all dictionaries in jsonClientList contain a key firstName.
tableView is the name of the UITableView instance.
...
let jsonClientList = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as! [[String:AnyObject]]
self.clientList = jsonClientList.map{ $0["firstName"] as! String }
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
} catch {
...
Reading the JSON with mutable containers is not needed in this case.