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)
Related
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.
This is the screen shot as you can see it shows error as I forced unwrapped and some urls are empty:
How can I safely unwrap this URL so I don't have to force unwrap ?
Code:
func tableView (_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int
{
return players.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:
Reusable.reuseIdForMain) as! CustomCell
cell.nameLabel.text = players[indexPath.row].name
cell.otherInfo.text = players[indexPath.row].otherInfo
if let url = players[indexPath.row].imageUrl{
cell.profileImage.load.request(with: URL(string:url)!)
}
return cell
}
You should check for the value of the URL itself after checking the string. Both strings will be safely unwrapped this way.
if let urlString = players[indexPath.row].imageUrl,
let url = URL(string: urlString) {
cell.profileImage.load.request(with: url)
}
You can try this
if let imageUrl = players[indexPath.row].imageUrl as? String{
let url = URL(string: imageUrl)
if let url = url {
cell.profileImage.load.request(with: url)
}
}
Tomas Sengel's method works easiest and simplest; HOWEVER...
Sometimes images in my collectionView wouldn't load (failed to the ELSE part of the if-let) even tho the model downloaded (from Firebase) the string of the url just fine.
The reason was some URLs have a space in the url string, which the Apple method URL(string: ) doesn't handle properly (Apple should update it). To fix, either find/write a better method to convert strings to URL type, or replace spaces with %20. literally. " " -> "%20" and then URL(string: ) won't fail the guard condition.
Use Below code that will also resolved your problem for loading an image within nano second try this
extension UIImageView {
public func imageFromUrl(urlString: String) {
if let url = NSURL(string: urlString) {
let request = NSURLRequest(url: url as URL)
NSURLConnection.sendAsynchronousRequest(request as URLRequest, queue: OperationQueue.main) { (response: URLResponse?, data: Data?, error: Error?) -> Void in
if let imageData = data as NSData? {
self.image = UIImage(data: imageData as Data)
}
}
}
}
}
Uses
if players[indexPath.row].imageUrl != "" && players[indexPath.row].imageUrl != nil {
cell.profileImage.imageFromUrl(urlString: players[indexPath.row].imageUrl)
}
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.
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.....
I'm getting error while trying to display an image in tableview.
It is displaying text successfully but it is not showing any images.
class ViewController: UITableViewController {
var ref: FIRDatabaseReference!
var refHandle: UInt!
var valueList = [Values]()
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
fetchFirebaseData()
}
func fetchFirebaseData() {
refHandle = ref.child("SwiftJson").observe(.childAdded, with:
{(snapshot) in
if let dictionary = snapshot.value as? [String : AnyObject] {
print(dictionary)
let value = Values() as AnyObject
value.setValuesForKeys(dictionary)
self.valueList.append(value as! Values)
self.tableView.reloadData()
}
})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return valueList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! FTableViewCell
let value = valueList[indexPath.row]
cell.fTitle.text = value.title
let imageUrlString = valueList[indexPath.row]["image"] as! String
let imageURL = NSURL(string: imageUrlString)
let imageData = NSData(contentsOf: imageURL as! URL)
cell.fImage.image = UIImage(data: imageData as! Data)
return cell
}
}
Here I'm parsing data from Firebase database and trying to show it on tableview.
Probably the problem that you are having is basically that you are trying to access a subscript of a anyObject type. On swift, a anyObject doesn't has subscripts. I don't know why you are casting Values() as AnyObject, but thats is probably why you are getting this error.
Also, try to take a look better on the other questions. Your question was probably asked before.
You have:
var valueList = [Values]()
Then later:
let imageUrlString = valueList[indexPath.row]["image"] as! String
Your error message tells you exactly what is wrong:
“Type 'Values' has no subscript members”
valueList[indexPath.row] is of type Value which as far as I can guess, does not have a subscript defined in its class. Which is exactly what the error says. And you're trying to call a subscript on it:
valueList[indexPath.row]["image"]
Looking at your earlier code of when you assign Value's to the array, I'm guessing it would unwrap as [String : AnyObject]
So you need to:
if let value = valueList[indexPath.row] as? [String : AnyObject], let imageUrlString = value["image"] as? String {
let imageURL = NSURL(string: imageUrlString)
//etc, etc...
}