When I scroll to the bottom of the UITableView the app is suppose to call a function ("CallAlamo(url: nextSearchURL)"), which just appends new content to array, then call tableView.reloadData(), and the tableview is then updated with the more content. However, the tableView freezes completely for about 2-3 seconds during this process. How can I get it to not freeze and work like most table views do in other apps where the new content is being loaded and the user is free to move the tableview.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = posts.count - 1
if indexPath.row == lastElement {
callAlamo(url: nextSearchURL) //appends new content to array
tableView.reloadData()
}
}
UPDATE
This is what callAlamo does:
func callAlamo(url : String){
Alamofire.request(url).responseJSON(completionHandler: {
response in
self.parseData(JSONData: response.data!)
})
}
func parseData(JSONData : Data){
do{
var readableJSON = try JSONSerialization.jsonObject(with: JSONData, options: .mutableContainers) as! JSONStandard
//print(readableJSON)
if let tracks = readableJSON["tracks"] as? JSONStandard{
nextSearchURL = tracks["next"] as! String
if let items = tracks["items"] as? [JSONStandard]{
//print(items) //Prints the JSON information from Spotify
for i in 0..<items.count{
let item = items[i]
let name = item["name"] as! String
let previewURL = item["preview_url"] as! String
if let album = item["album"] as? JSONStandard{
if let images = album["images"] as? [JSONStandard],let artist = album["artists"] as? [JSONStandard]{
let imageData = images[0] //this changes the quality of the album image (0,1,2)
let mainImageURL = URL(string: imageData["url"] as! String)
let mainImageData = NSData(contentsOf: mainImageURL!)
let mainImage = UIImage(data: mainImageData as! Data)
let artistNames = artist[0]
let artistName = artistNames["name"] as! String
posts.append(post.init(mainImage: mainImage, name: name, artistName: artistName, previewURL: previewURL))
self.tableView.reloadData()
}
}
}
}
}
} catch{
print(error)
}
}
UPDATE 2
Using #Anbu.Karthik choice 2:
Question 1: is "imageData" going to be my "mainImagedata"?
Question 2: I get an error in the Alamofire.request... saying "Extra argument 'method' in call" and when i delete it, i get an error that says "NSData? has no subscript members"
Very bad code design, you should pass the url to the cell and let it do the fetching and parsing, and you are doing this on the main queue. You can do this using(using another queue) DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async. IDK if Alamofire calls your closure on the main queue, but it look like it does the request on it. And don't forget to get back on the main queue when you want do to UI using DispatchQueue.main.async
UPDATE: I hope that it was clear that reloadData(), kinda gives you an infinite loop, and you should call these outside the TableViewDataSource funcitons
UPDATE 2: I don't see it here, but you should use tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) and use in it let cell = tableView.dequeueReusableCell.....
Related
I want to display a list of store names, descriptions and images in my table cells like below:
I created my storyboard like this:
And this is what I've got so far:
I store the data in Firebase in below format
Created a data model that fits Firebase data
for stores in snapshot.children.allObjects as! [DataSnapshot]{
let storeObject = stores.value as? [String: AnyObject]
let storeName = storeObject?["storeName"]
let storeDesc = storeObject?["storeDesc"]
let storeUrl = storeObject?["storeUrl"]
let store = StoreModel(
name: storeName as! String?,
desc: storeDesc as! String?,
url: storeUrl as! String?)
self.storeList.append(store)
}
Display data
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewControllerTableViewCell
let store: StoreModel
store = storeList[indexPath.row]
cell.labelName.text = store.name
cell.labelDesc.text = store.desc
return cell
}
I've successfully displayed the list of store names and descriptions, but don't know how to display the images by URL I store in Firebase.
I've tried below code in the tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int function, but it didn't work
let imageUrl:URL = URL(string: store.url)!
let imageData:NSData = NSData(contentsOf: imageUrl)!
let image = UIImage(data: imageData as Data)
cell.imageStore.image = image
Error messages:
Value of optional type 'String?' must be unwrapped to a value of type 'String'
Coalesce using '??' to provide a default when the optional value contains 'nil'
Force-unwrap using '!' to abort execution if the optional value contains 'nil'
Thank you!
As a suggestion you should avoid force unwrap and use Swift types, for example Data instead of NSData :) then, the code you tried works synchronously and it’s better to download your images asynchronously to avoid blocking the UI, try using URLSession, you can create a UIImageView extension, for example:
extension UIImageView {
func setImage(from urlAddress: String?) {
guard let urlAddress = urlAddress, let url = URL(string: urlAddress) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data)
}
}
task.resume()
}
}
Then you can call it in this way:
cell.imageView.setImage(from: storeUrl)
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.
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´m building a widget for iOS with Swift. The main app´s purpose is to connect to a URL news feed and get the latest news, while the widget only get the title to display in a tableView in the Today view.
I´ve written this method for the widget in order to get the data to populate the table, but for some reason nothing is showing. I´ve tried to debug it, but being a widget it seems to be practically imposible.
This is the cellForRowAt, where I connect to the feed and try to extract data. The funny part is, the main app uses basically the same code and it works perfectly.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let urlRequest = URLRequest(url: URL(string: "https://newsapi.org/v1/articles?source=techcrunch&sortBy=top&apiKey=c64849bc30eb484fb820b80a136c9b0a")!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data,response,error) in
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String: AnyObject]
if let articlesFromJson = json["articles"] as? [[String: AnyObject]] {
if !(error != nil) {
var resultArray: NSMutableArray = NSMutableArray()
for articlesFromJson in articlesFromJson {
if let title = articlesFromJson["title"] as? String{
resultArray.add(title)
}
let array:NSArray = resultArray.reverseObjectEnumerator().allObjects as NSArray
resultArray = array as! NSMutableArray
let title:String = resultArray.object(at: indexPath.row) as! String
cell.textLabel?.text = title
}
}
}
//reload on main thread to speed it up
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let error {
print(error)
}
}
task.resume()
return cell
}
If someone can help me figure out where is the mistake it would be a huge help, i´ve been stuck on this issue for days now. Thanks
You want to make your network request outside of cellForRow and then reloadData once it's complete to have the tableView reload the cells which calls cellForRow.
store the array of data outside of request so you can reference it from outside the function.
var resultArray: NSMutableArray = []
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
func getData() {
let urlRequest = URLRequest(url: URL(string: "https://newsapi.org/v1/articles?source=techcrunch&sortBy=top&apiKey=c64849bc30eb484fb820b80a136c9b0a")!)
let task = URLSession.shared.dataTask(with: urlRequest) {[weak self] (data,response,error) in
guard let strongSelf = self else { return }
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String: AnyObject]
if let articlesFromJson = json["articles"] as? [[String: AnyObject]] {
if error == nil {
for articlesFromJson in articlesFromJson {
if let title = articlesFromJson["title"] as? String{
strongSelf.resultArray.add(title)
}
let array:NSArray = strongSelf.resultArray.reverseObjectEnumerator().allObjects as NSArray
strongSelf.resultArray = array as! NSMutableArray
DispatchQueue.main.async {
strongSelf.tableView.reloadData()
}
} catch let error {
print(error)
}
}
task.resume()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let title:String = resultArray.object(at: indexPath.row) as! String
cell.textLabel?.text = title
return cell
}
checks proper if TableView delegate or datasource proper connected. and check array count before load data in cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if (resultArray.count > 0){
let title:String = resultArray.object(at: indexPath.row) as! String
cell.textLabel?.text = title
}
else
{
print("Error: resultArray contain nil value ")
}
return cell
}