Reloading cells in view after image downloaded - swift

I am currently learning swift. I have experience in android but now time for something new. I am starting with basics to load movie DB from API to table. I am storing dowloaded poster in Movie class (which also downloads them) when scrolling I can see the posters but after download the current cells in the view not updated, only after scroll. How can I implement callback from Movie to table view to update visible cells after download.
Movie:
import UIKit
let URL_PREFIX = "https://image.tmdb.org/t/p/original"
class Movie {
let movieId: CLong?
let title: String?
let posterPath: String?
let overview: String?
let releaseDate: String?
var posterImage: UIImage?
var callback: ((_ id: Int) -> Void)?
init(movieId: CLong,title: String,posterPath: String,overview: String,releaseDate: String,posterImage: UIImage?=nil) {
self.movieId = movieId
self.title = title
self.posterPath = posterPath
self.overview = overview
self.releaseDate = releaseDate
self.posterImage = posterImage
setResizedImage(path: posterPath)
}
func setResizedImage(path: String)
{
let conPath = URL_PREFIX + path
print("Path: \(conPath)")
guard let urlPath = URL(string: conPath) else {
print("You fucked up")
return
}
print("Download Started")
getData(from: urlPath) { data, response, error in
guard let _ = data, error == nil else { return }
print(response?.suggestedFilename ?? urlPath.lastPathComponent)
print("Download Finished")
//update
DispatchQueue.main.async()
{
self.posterImage = UIImage(data: data!)
}
}
}
}
MyViewController:
import UIKit
let URL_MOVIES = "https://api.themoviedb.org/3/movie/upcoming?
api_key=000";
class DataViewController: UIViewController,UITableViewDelegate,
UITableViewDataSource {
#IBOutlet weak var myTable: UITableView!
var movieArray :[Movie] = []
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movieArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MovieTableViewCell", for: indexPath) as? CustomTableViewCell else {
fatalError("The dequeued cell is not an instance of MovieTableViewCell.")
}
let movie = movieArray[indexPath.row]
cell.title.text = movie.title
cell.releaseDate.text = movie.releaseDate
cell.overview.text = movie.overview
//cell.url.text = movie.overview
if (movie.posterImage==nil)
{
print("Loaded placeholder")
cell.poster.image = UIImage(named: "poster")
}
else
{
print("Hello2")
cell.poster.image = movie.posterImage
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You tapped cell number \(indexPath.row).")
}
override func viewDidLoad() {
super.viewDidLoad()
myTable.rowHeight = UITableView.automaticDimension
myTable.estimatedRowHeight = 50
getJsonFromUrl()
}
func getJsonFromUrl(){
let url = NSURL(string: URL_MOVIES)
URLSession.shared.dataTask(with: (url as URL?)!, completionHandler: {(data, response, error) -> Void in
if let jsonObj = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
if let resultArray = jsonObj.value(forKey: "results") as? NSArray
{
for film in resultArray
{
if let movieDict = film as? NSDictionary
{
//getting the name from the dictionary
let id = movieDict.value(forKey: "id")
let title = movieDict.value(forKey: "title")
let posterPath = movieDict.value(forKey: "poster_path")
let overview = movieDict.value(forKey: "overview")
let releaseDate = movieDict.value(forKey: "release_date")
let movie = Movie(movieId:id as! CLong, title: title as! String, posterPath: posterPath as! String, overview: overview as! String, releaseDate: releaseDate as! String)
self.movieArray.append(movie)
}
}
}
OperationQueue.main.addOperation({
self.myTable.reloadData()
})
}
}).resume()
}
}

You can add the download function inside the cell custom class and assign the imageView inside the callback, but this has many problems such as redownloading same image multiple times when scrolling, it's better to use SDWebImage or you can use Kingfisher Library
import SDWebImage
cell.poster.sd_setImage(with: URL(string:movie.imageUrlStr), placeholderImage: UIImage(named: "placeholder.png"))

Related

With Firebase, I'd like to get data asynchronously and show on TableView but some of data are duplicated on TableView

With Firebase, I'd like to get data asynchronously, and then show it on TableView, but some of data are duplicated on TableView. I think it's because I use closure in For statement but I'm not sure where I should call it instead.
I tried to call setDataToArray in Dispatchqueue.main.async but it doesn"t work.
Class PostTableViewController: UITableViewController {
let db = Firestore.firestore()
var postArray: [Post] = [Post]()
override func viewDidLoad() {
super.viewDidLoad()
SVProgressHUD.show()
configureTableview()
setDataToArray()
}
func setDataToArray() {
retrievePost() { (posts) in
print(posts!)
for post in posts! {
self.postArray.append(post)
}
print(self.postArray)
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
func retrievePost(completion: #escaping ([Post]?) -> Void) {
var posts = [Post]()
let postsColRef = db.collection("posts").order(by: "createdAt")
postsColRef.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Document data: \(error)")
} else {
for document in querySnapshot!.documents {
let data = document.data()
let userId = data["userId"] as? String
let postImage = data["postImageURL"] as? String
let postText = data["postText"] as? String
let createdAt = data["createdAt"] as? String
let numberOfLike = data["numberOfLike"] as? Int
let docRef = self.db.collection("users").document(userId!)
docRef.getDocument() { (document, error) in
if let document = document, document.exists {
let data = document.data()!
let userName = data["userName"] as? String
let userImage = data["userImageURL"] as? String
let post = Post(
userId: userId!,
userName: userName!,
userImageURL: userImage!,
postImageURL: postImage!,
postText: postText!,
createdAt: createdAt!,
numberOfLike: numberOfLike!)
print(post)
posts.append(post)
completion(posts)
}
}
}
}
}
}
#IBAction func cameraButtonPressed(_ sender: UIBarButtonItem) {
performSegue(withIdentifier: "homeToChooseImage", sender: nil)
}
}
extension PostTableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell
let post = postArray[indexPath.row]
let postImageURL = URL(string: post.postImageURL)
do {
let data = try Data(contentsOf: postImageURL!)
cell.postImage.image = UIImage(data: data)
}catch let err {
print("Error : \(err.localizedDescription)")
}
let userImageURL = URL(string: post.userImageURL)
do {
let data = try Data(contentsOf: userImageURL!)
cell.userImage.image = UIImage(data: data)
}catch let err {
print("Error : \(err.localizedDescription)")
}
cell.postText.text = post.postText
cell.userName.text = post.userName
cell.createdAt.text = post.createdAt
cell.postLikeButton.titleLabel?.text = "\(post.numberOfLike) Likes"
return cell
}
}
I guess where I call "completion(posts)" is wrong.
Where should I call it instead?

How to update table view

I used search bar. It isn't updating the table view.
struct ApiResults:Decodable {
let resultCount: Int
let results: [Music]
}
struct Music:Decodable {
let trackName: String?
let artistName: String?
let artworkUrl60: String?
}
class ItunesDataViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
var musicArray:[Music] = []
var mArray:[Music] = []
var filteredData:[Music] = []
var isSearching = false
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = self
self.tableView.delegate = self
self.searchBar.delegate = self
searchBar.placeholder = "search"
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
{
print("\n\nsearchText : \(searchText)\n\n")
Search(searchTerm: "\(searchText)")
if searchBar.text == nil || searchBar.text == ""
{
isSearching = false
view.endEditing(true)
self.tableView.reloadData()
}
else
{
isSearching = true
filteredData = mArray.filter{$0.artistName == searchText}
self.tableView.reloadData()
}
}
func Search(searchTerm: String)
{
guard let url = URL(string: "https://itunes.apple.com/search?term=\(searchTerm)&attribute=actorTerm&attribute=languageTerm&attribute=allArtistTerm&attribute=tvEpisodeTerm&attribute=shortFilmTerm&attribute=directorTerm&attribute=releaseYearTerm&attribute=titleTerm&attribute=featureFilmTerm&attribute=ratingIndex&attribute=keywordsTerm&attribute=descriptionTerm&attribute=authorTerm&attribute=genreIndex&attribute=mixTerm&attribute=allTrackTerm&attribute=artistTerm&attribute=composerTerm&attribute=tvSeasonTerm&attribute=producerTerm&attribute=ratingTerm&attribute=songTerm&attribute=movieArtistTerm&attribute=showTerm&attribute=movieTerm&attribute=albumTerm") else {return}
URLSession.shared.dataTask(with: url){(data, response, error) in
guard let data = data else {return}
do
{
let apiressults = try JSONDecoder().decode(ApiResults.self, from: data)
for item in apiressults.results
{
if let track_Name = item.trackName, let artist_Name = item.artistName, let artwork_Url60 = item.artworkUrl60
{
let musics = Music(trackName: track_Name, artistName: artist_Name, artworkUrl60: artwork_Url60)
self.musicArray.append(musics)
print(musics.artistName!,"-", musics.trackName!)
}
}
DispatchQueue.main.async
{
self.tableView.reloadData()
}
}
catch let jsonError
{
print("Error:", jsonError)
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isSearching
{
return filteredData.count
}
else
{
return mArray.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "musicCell", for: indexPath) as! ItunesDataTableViewCell
if isSearching
{
cell.lblDesc?.text = filteredData[indexPath.row].artistName
cell.lblSongDesc?.text = filteredData[indexPath.row].trackName
let imgString = filteredData[indexPath.row].artworkUrl60!
let imgUrl:URL = URL(string: imgString)!
DispatchQueue.global(qos: .userInitiated).async {
let imageData:NSData = NSData(contentsOf: imgUrl)!
DispatchQueue.main.async {
let image = UIImage(data: imageData as Data)
cell.imgArt?.image = image
}
}
}
else
{
cell.lblDesc?.text = mArray[indexPath.row].artistName
cell.lblSongDesc?.text = mArray[indexPath.row].trackName
let imgString = mArray[indexPath.row].artworkUrl60!
let imgUrl:URL = URL(string: imgString)!
DispatchQueue.global(qos: .userInitiated).async {
let imageData:NSData = NSData(contentsOf: imgUrl)!
DispatchQueue.main.async {
let image = UIImage(data: imageData as Data)
cell.imgArt?.image = image
}
}
}
return cell
}
}
Please clean up your code 😉: You have two different arrays mArray and musicArray.
You are populating musicArray in Search but mArray is used as data source.
Why do you create new Music items from Music items? You can reduce the code to
let apiressults = try JSONDecoder().decode(ApiResults.self, from: data)
self.mArray = apiressults.results
DispatchQueue.main.async {
self.tableView.reloadData()
}
Please change your code in the cellForRowAt delegate method to:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "musicCell", for: indexPath) as! ItunesDataTableViewCell
let tempArray: [Music] = isSearching ? filteredData : musicArray
cell.lblDesc?.text = tempArray[indexPath.row].artistName
cell.lblSongDesc?.text = tempArray[indexPath.row].trackName
guard let imgString = tempArray[indexPath.row].artworkUrl60,
let imgUrl = URL(string: imgString) else {
// Handle properly the fact that there's no image to display
return cell
}
// Review this code as I'm not sure about this double dispatch
// However, please, no force unwrap optionals (!)
DispatchQueue.global(qos: .userInitiated).async {
do {
let imageData = try Data(contentsOf: imgUrl)
DispatchQueue.main.async {
let image = UIImage(data: imageData)
cell.imgArt?.image = image
}
} catch let error {
print("Error with the image URL: ", error)
}
}
return cell
}
See how you don't repeat your code that way?
Furthermore you were not using the right music array, or we don't have all the information to assess what is wrong with this mix of mArray and musicArray.

When tapped on search bar, the app crashes

The app crashes with the following error when the search bar is tapped:
Not able to understand why?
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'unable to dequeue a cell with identifier ContactCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
This is my code:
import UIKit
class ContactViewController: UITableViewController, UISearchResultsUpdating {
var dataSource: [Contact] = []
var filteredResult = [Contact]()
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
downloadJSONUrl()
}
func downloadJSONUrl() {
let urlString = "https://us-central1-practo-contacts-sample.cloudfunctions.net/get"
let url = NSURL(string: urlString)
URLSession.shared.dataTask(with: (url as URL?)!, completionHandler: {(data, response , error) -> Void in
if let jsonObj = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
if let NameArray = jsonObj?.value(forKey: "contacts") as? [[String: Any]] {
for names in NameArray {
var cont = Contact()
if let name = names["name"] as? String {
cont.name = name
}
if let ph = names["number"] as? String {
cont.phoneNumber = ph
}
self.dataSource.append(cont)
}
self.dataSource.sort {$0.name.lowercased() < $1.name.lowercased()}
}
OperationQueue.main.addOperation {
self.tableView.reloadData()
}
}
}).resume()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as UITableViewCell
let contact = self.dataSource[indexPath.row]
cell.textLabel?.text = contact.name
cell.detailTextLabel?.text = contact.phoneNumber
return cell
}
func updateSearchResults(for searchController: UISearchController) {
if searchController.searchBar.text! == "" {
filteredResult = dataSource
} else {
filteredResult = dataSource.filter { $0.name.lowercased().contains(searchController.searchBar.text!.lowercased()) }
}
self.tableView.reloadData()
}
#IBAction func unwindToContactList(segue: UIStoryboardSegue) {
guard let viewController = segue.source as? AddOrEditViewController else { return }
if let name = viewController.nameTextField.text, let phoneNumber = viewController.phoneNumberTextField.text {
let contact = Contact(name: name, phoneNumber: phoneNumber)
self.dataSource.append(contact)
tableView.reloadData()
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "contactDetailsSegue" {
guard let viewController = segue.destination as? ContactDetialsViewController else {
return
}
guard let indexPath = tableView.indexPathForSelectedRow else { return }
let contact = self.dataSource[indexPath.row]
viewController.contact = contact
}
}
}
Are you sure that you have set the identifier for the cell on the storyboard and the identifier name is the same used on the code "ContactCell"?

Search Bar Swift 3 - Can't use in/contains operator with collection

I am implementing a search bar into my project but I am being presented with the below error.
reason: 'Can't use in/contains operator with collection wellpleased.attendees.UserData(firstname: "Ben", lastname: "Delonge", fullname: "Ben Delonge", company: "AllStar.com", jobtitle: "Business Development Manager", image: "6.jpg") (not a collection)'
I have done plenty of searching around the NSPredicate but cannot seem to prevent this crashing.
I am using the code below, any assistance resolving this would be much appreciated.
class attendees: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
var tableData = ""
var value:String!
var searchString: String = ""
var dataSource: [UserData] = []
struct UserData {
var firstname: String
var lastname: String
var fullname: String
var company: String
var jobtitle: String
var image: String
}
var filteredAppleProducts = [String]()
var resultSearchController = UISearchController()
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
print(value)
searchBar.delegate = self
self.tableView.reloadData()
let nib = UINib(nibName: "vwTblCell2", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "cell2")
}
override func viewDidAppear(_ animated: Bool) {
getTableData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if filteredAppleProducts != []{
return self.filteredAppleProducts.count
}
else
{
if searchString != "[]" {
return self.dataSource.count
}else {
return 0
}
}
}
// 3
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell2: TblCell2 = self.tableView.dequeueReusableCell(withIdentifier: "cell2") as! TblCell2
print(filteredAppleProducts)
if filteredAppleProducts != []{
cell2.nameLabel.text = self.filteredAppleProducts[indexPath.row]
return cell2
}
else
{
if searchString != "[]"{
cell2.nameLabel.text = self.dataSource[indexPath.row].fullname
cell2.companyLabel.text = self.dataSource[indexPath.row].company
cell2.jobTitleLabel.text = self.dataSource[indexPath.row].jobtitle
let url = URL(string: "https://www.asmserver.co.uk/wellpleased/backend/profileimages/\(self.dataSource[indexPath.row].image)")
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
cell2.userImage.image = UIImage(data: data!)
}
return cell2
}
}
// 4
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
// 5
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 90
}
func updateSearchResults(){
self.filteredAppleProducts.removeAll(keepingCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchString)
let array = (self.dataSource as NSArray).filtered(using: searchPredicate)
self.filteredAppleProducts = array as! [String]
self.tableView.reloadData()
print(filteredAppleProducts)
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print("searchText \(searchText)")
print(filteredAppleProducts)
searchString = searchText
updateSearchResults()
}
func getTableData(){
self.dataSource.removeAll()
let defaults = UserDefaults()
let userid = defaults.string(forKey: "id")
let url = NSURL(string: "https://www.******.co.uk/wellpleased/backend/searchattendees.php?userid=\(userid!)&eventid=\(value!)")
print(url)
let task = URLSession.shared.dataTask(with: url as! URL) { (data, response, error) -> Void in
if let urlContent = data {
do {
if let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: []) as? [[String:AnyObject]] {
var i = 0
while i < jsonResult.count {
self.dataSource.append(UserData(firstname:"\(jsonResult[i]["firstname"]! as! String)", lastname: "\(jsonResult[i]["lastname"]! as! String)", fullname:"\(jsonResult[i]["fullname"]! as! String)", company: "\(jsonResult[i]["company"]! as! String)", jobtitle:"\(jsonResult[i]["jobtitle"]! as! String)", image:"\(jsonResult[i]["image"]! as! String)"))
i = i + 1
}
}
} catch {
print("JSON serialization failed")
}
} else {
print("ERROR FOUND HERE")
}
DispatchQueue.main.async(execute: { () -> Void in
self.tableView.reloadData()
})
self.tableView.isUserInteractionEnabled = true
}
task.resume()
}
}
I have also tired:
let searchPredicate = NSPredicate(format: "fullname CONTAINS[c] %#", searchString as String)
which returns the error:
this class is not key value coding-compliant for the key fullname
NSPredicate is a Cocoa feature that lives in the Objective-C world. It's never going to work on an array of UserData because UserData is a Swift struct — and Objective-C cannot see a Swift struct at all (and even if it could, it certainly can't see any type namespaced inside a class, as your UserData is).
You would have an easy time of this if you simply used the built-in Swift filter method to filter the dataSource array. For example (if this is what you're trying to do):
let array = self.dataSource.filter{$0.fullname.contains(searchString)}
In Swift 3, you can combine NSArray with NSPredicate like this:
let searchPredicate = NSPredicate(format: "%K CONTAINS[c] %#", "fullname",searchString)
let array = NSArray(array: self.dataSource).filtered(using: searchPredicate)

NSOperation for loading image to TableView

Here is my Movie class:
import UIKit
class Movie {
var title: String = ""
var rating: Double = 0
var image: UIImage = UIImage()
}
I want to load the the array of movie to tableView, I have tried like this:
import UIKit
import Cosmos
import Alamofire
//import AlamofireImage
import SwiftyJSON
class Downloader {
class func downloadImageWithURL(url:String) -> UIImage! {
let data = NSData(contentsOfURL: NSURL(string: url)!)
return UIImage(data: data!)
}
}
class MovieViewController: UIViewController, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var movies: [Movie] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchMovies()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MovieTableViewCell
let movie = movies[indexPath.row]
cell.movieTitleLabel.text = movie.title
cell.movieRatingView.rating = Double(movie.rating / 20)
cell.movieImageView.image = movie.image
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}
func fetchMovies() {
let movieURLString = "https://coderschool-movies.herokuapp.com/movies?api_key=xja087zcvxljadsflh214"
Alamofire.request(.GET, movieURLString).responseJSON { response in
let json = JSON(response.result.value!)
let movies = json["movies"].arrayValue
let queue = NSOperationQueue()
for movie in movies {
let title = movie["title"].string
let rating = movie["ratings"]["audience_score"].double
let imageURLString = movie["posters"]["thumbnail"].string
let movie = Movie()
movie.title = title!
movie.rating = rating!
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
})
queue.addOperation(operation)
self.movies.append(movie)
self.tableView.reloadData()
}
}
}
}
The problem is that: when I scroll down or up, the images will be reload, otherwise they are not reloaded. Only the title and rating will be
I only know the reason is these line
self.movies.append(movie)
self.tableView.reloadData()
are compiled before
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
})
queue.addOperation(operation)
But if I scroll down, it will like this:
I've already do it perfectly with AlamofireImage, but I really want to use NSOperation for diving with it.
I think you should make sure to reloadData() on the main thread. By default it's happening on a background thread and cannot update the UI.
dispatch_async(dispatch_get_main_queue(),{
self.tableView.reloadData()
})
I solved the problem by put the reloadData to the main thread, like so:
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.tableView.reloadData()
}
})
Now it works well.