How to display tableView only after data fetched from web? (Swift) - ios

I encountered difficulties when loading the Collection views nested in table view cells. The content inside cells would only show after scrolling the table a couple of times. My approach was to use DispatchGroup() in order to fetch the data in a background thread but it didn't work. What is there to do in order to show all the information at once without scrolling through table?
ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
_tableView.isHidden = true
_tableView.dataSource = nil
_tableView.delegate = nil
SVProgressHUD.show()
view.backgroundColor = UIColor.flatBlack()
getData()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
UICollectionView and UITableView datasource / OtherMethods
func getData(){
dispatchGroup.enter()
backend.movieDelegate = self
backend.actorDelegate = self
backend.getMoviePopularList()
backend.getMovieTopRatedList()
backend.getMovieUpcomingList()
backend.getPopularActors()
backend.getMovieNowPlayingList()
dispatchGroup.leave()
}
func transferMovies(data: [String:[MovieModel]]) {
dispatchGroup.enter()
popularMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
dispatchGroup.enter()
popularActors = data
dispatchGroup.leave()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "DiscoverCell") as? DiscoverViewCell else { return UITableViewCell()}
cell.categoryLabel.text = cell.categories[indexPath.item]
//categories[indexPath.item]
cell._collectionView.delegate = self
cell._collectionView.dataSource = self
cell._collectionView.tag = indexPath.row
cell._collectionView.reloadData()
self.setUpCell(cell)
return cell
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", for: indexPath) as? MovieCollectionViewCell else { return UICollectionViewCell()}
if collectionView.tag == 0{
if let movieDetails = popularMovies["Popular"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 1{
if let movieDetails = popularMovies["Top rated"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 2{
if let movieDetails = popularMovies["Upcoming"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
} else if collectionView.tag == 3{
cell.movieTitleLabel.text = popularActors?[indexPath.item].name ?? ""
cell.moviePicture.image = popularActors?[indexPath.item].poster
}
} else if collectionView.tag == 4{
if let movieDetails = popularMovies["Now playing"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
}
return cell
}
MovieCollectionViewCell
class MovieCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var moviePicture: UIImageView!
#IBOutlet weak var movieTitleLabel: UILabel!
func updateMovieCollectionCell(movie: MovieModel){
moviePicture.image = movie.poster
movieTitleLabel.text = movie.name
}
}
DiscoverViewCell
class DiscoverViewCell: UITableViewCell {
#IBOutlet weak var categoryLabel: UILabel!
#IBOutlet weak var _collectionView: UICollectionView!
let categories = ["Popular", "Top Rated", "Upcoming", "Popular People", "Now playing"]
#IBAction func seeMoreAction(_ sender: Any) {
}
}
My intention is to show a loading animation until all the data is fetched and the display the table view cells containing the collection views with fetched data from web.
The desired result should look like this when opening the app

From what I can tell, you're using dispatchGroup incorrectly.
To summarize notify():
it runs the associated block when all currently queued blocks in the group are complete
the block is only run once and then released
if the group's queue is empty, the block is run immediately
The issue I see is that with the way your fetch code is written, the group thinks its queue is empty when you call notify. So the notify() block is run immediately and then you only see the cells populate when they are reloaded during scrolling.
There are two ways to populate a dispatchGroup's queue:
call DispatchQueue.async() and pass the group to directly enqueue a block and associate it with the group
manually call enter() when a block begins and leave() when it ends, which increases/decreases an internal counter on the group
The first way is safer, since you don't have to keep track of the blocks yourself, but the second one is more flexible if you don't have control over what queue a block is run on for example.
Since you're using enter/leave, you need to make sure that you call enter() for each separate work item (in your case, the asynchronous calls to backend), and only call leave() when each one those work items completes. I'm not sure how you're using the delegate methods, but there doesn't seem to one for each backend call, since there are 5 different calls and only 2 delegate methods. It also doesn't look like the delegate methods would be called if an error happened in the backend call.
I would recommend changing the backend calls to use completion blocks instead, but if you want to stick with the delegate pattern, here's how you might do it:
func getData(){
backend.movieDelegate = self
backend.actorDelegate = self
dispatchGroup.enter()
backend.getMoviePopularList()
dispatchGroup.enter()
backend.getMovieTopRatedList()
dispatchGroup.enter()
backend.getMovieUpcomingList()
dispatchGroup.enter()
backend.getPopularActors()
dispatchGroup.enter()
backend.getMovieNowPlayingList()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
func transferPopularMovies(data: [MovieModel]) {
popularMovies = data
dispatchGroup.leave()
}
func transferTopRatedMovies(data: [MovieModel]) {
topRatedMovies = data
dispatchGroup.leave()
}
func transferUpcomingMovies(data: [MovieModel]) {
upcomingMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
popularActors = data
dispatchGroup.leave()
}
func transferNowPlayingMovies(data: [MovieModel]) {
nowPlayingMovies = data
dispatchGroup.leave()
}
Don't forget to call the delegate methods even if there is an error to make sure the enter/leave calls are balanced. If you call enter more often than leave, the notify block never runs. If you call leave more often, you crash.

Try this...After getting your data from backend and assigning to moviedetails, set your delegate and datasource to self and reload table.

Set this line into the bottom of getData() function and run
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
and remove from viewdidload()

Related

Execution order of closures

Here's my code:
I'm just a beginner in programming with this language and I have a problem with a dynamic collection view
My problem is that the the first print is executed after the second one and I'm wondering why...
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var menuButton: UIBarButtonItem!
let db = Firestore.firestore()
let cellId: String = "cellId"
var news = [[String: Any]]()
override func viewDidLoad(){
super.viewDidLoad()
navigationItem.title = "Novità"
sideMenu()
customizeNavBar()
collectionView?.register(NovitaCellView.self, forCellWithReuseIdentifier: cellId)
}
override func didReceiveMemoryWarning(){
super.didReceiveMemoryWarning()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
db.collection("News").getDocuments(){(querySnapshot, err) in
if let err = err{
print("Errore: \(err)")
}else{
for document in (querySnapshot?.documents)!{
let cell = document.data()
self.news.append(cell)
print ("document number: \(self.news.count)")
}
}
}
print("exe return with value: \(self.news.count)")
return self.news.count
}
edit: I tried setting it into the viewDidLoad func as well as setting it both as a sync queue and an async queue and it either doesn't works.
edit 2: i made this working by adding the closure into a max priority queue and reloading the view after in the main thread but it takes a long time in order to work..
The idea is that this line
db.collection("News").getDocuments(){(querySnapshot, err) in
is asynchronous (runs in another queue other than the main queue ) it's a network request that will run after the below line (which runs in main queue)
You are going to background thread mode when you call getDocuments which have less priority than Main thread.So replace below part of code into viewDidLoad or a method which is called from viewDidLoad
db.collection("News").getDocuments(){(querySnapshot, err) in
if let err = err{
print("Errore: \(err)")
}else{
for document in (querySnapshot?.documents)!{
let cell = document.data()
self.news.append(cell)
print ("document number: \(self.news.count)")
}
DispatchQueue.main.async {
tableview.reloadData()
}
}
}
and when you get your data you go back to main thread and reload your tableview using this code.
DispatchQueue.main.async {
tableView.reloadData()
}
You can learn more about threading from apple documentation and this tutorial also.

Swift 2 + Parse: Array index out of range

SOMETIMES THE REFRESH WORKS SOMETIMES IT DOESN'T
I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//disable app while it does stuff
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we dont get indexing error???
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
}
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
and:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCellWithIdentifier("SinglePostCell", forIndexPath: indexPath) as! PostCell
//ERROR GETS REPORTED ON THE LINE BELOW
myCell.usernamePosted.text = usernames[indexPath.row]
myCell.messagePosted.text = messages[indexPath.row]
return myCell
}
You have a race condition given you are doing two background tasks, where the second depends on values returned from the first. getUser?.findObjectsInBackgroundWithBlockwill return immediately, and getPost.findObjectsInBackgroundWithBlock will start executing. The getPost should be inside the block for getUser, to ensure the sequence is correct.
Similarly, the following two lines should be inside the second block:
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
Given the error line, you probably also have a race condition between the two background tasks and displaying the tableView. I would be inclined to try:
func tableView(tableView:UITableView!, numberOfRowsInSection section:Int) {
return self.refresher.refreshing ? 0 : self.usernames.count
}
This way you won't touch self.usernames until the background refresh is finished (as long as you remember to put endRefreshing inside the second block, which is also put inside the first block).
I Believe that in self.users[user.objectId!] = user.username! the user.ObjectId is some random value assigned by parse which looks like this: "34xcf4". This is why you might be getting 'Array index out of range'.
There are two required methods for configuring a UITableView:
tableView(_:cellForRowAtIndexPath:)
and
tableView(_:numberOfRowsInSection:)
In your code you are presenting only one required method, if you don't implement the second method then it that may cause errors.
Check the documentation at:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath:
You are calling self.tableView.reloadData() on every addition to your array and doing so in a background thread.
As a general rule, you should not do UI updates in a background thread. When you clear self.messages and self.usernames, because you are in background thread, nothing prevents the tableview from trying to get a cell at an index that no longer has any data in the array.
If you want to keep your code in the background thread (risky as it may be), you should at least call .beginUpdates before reloading your arrays and wait until they're all done before calling reload and endUpdates.

Getting " unexpectedly found nil" error when setting "self imageView! image" in Swift

I am getting the above error in this collection view-detail view code.
Detail VC:
class DetailViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView?
var photo: NSDictionary?
var image1:UIImage = UIImage()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//downloadFromUrl(url)
if let photo = photo {
let url1 = (photo["images"]!["thumbnail"]!!["url"])! as! String
let url:NSURL = NSURL(string: url1 as String)!
//print(url)
downloadFromUrl(url)
}
}
func downloadFromUrl(url: NSURL) {
//self.imageView.hnk_setImageFromURL(url)
let config:NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session:NSURLSession = NSURLSession(configuration: config)
let request:NSURLRequest = NSURLRequest(URL: url)
let task = session.downloadTaskWithRequest(request) { (location, response, error) -> Void in
let data:NSData = NSData(contentsOfURL: location!)!
let image:UIImage = UIImage(data: data)!
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.image1 = image
print(self.image1)
self.imageView.image = self.image1
})
}
task.resume()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
And here is some methods in collection VC:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! PhotosViewCell
cell.backgroundColor = UIColor.brownColor()
cell.photo = self.photos[indexPath.row] as? NSDictionary
//self.p = self.photos[indexPath.row] as! NSDictionary
//print("Link is \(cell.photo!["link"]!)")
return cell
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let p = self.photos[indexPath.row] as! NSDictionary
let VC:DetailViewController = DetailViewController()
VC.photo = p
self.presentViewController(VC, animated: true, completion: nil)
}
I know I am probably getting this error because viewDidLoad() is not yet ready but what is the solution and I do need this dictionary inside viewDidLoad() because I need to call that downloadFromUrl() method.
Oh, I am not using prepareforsegue() method. tried that too, maybe I was doing wrong.
In the debug area, I am getting the following:
2016-01-06 13:30:55.075 AuthCheck[3623:74910] Warning: Attempt to present <AuthCheck.DetailViewController: 0x7ff103dfb6c0> on <AuthCheck.PhotosViewController: 0x7ff103c2fd90> whose view is not in the window hierarchy!
<UIImage: 0x7ff103cd1820>, {150, 150}
fatal error: unexpectedly found nil while unwrapping an Optional value
Which means correct image is being downloaded but it is not being able to set to imageView's image property. No idea why?
Will someone tell me the solution please?
You are getting a runtime error because you are force-unwrapping an optional that happens to contain no value (i.e., it is nil). Please check your optionals before unwrapping (it is good practice):
print(photo!) // <--Dangerous
if let photo = photo {
print(photo) // <--Safe
}
...unless your code path makes it 100% sure that the optional does contain a value.
You are downloading data from the internet; that should typically be an asynchronous operation, because it takes significant time and you don't want to freeze your main thread (user interface) while it completes.
So you need to perform it on the background, and use the obtained data inside a completion handler (i.e., block or closure).
ViewDidLoad() is called when the view controller is loaded, meaning that it happens when you create the instance, before you present the view controller
// viewDidLoad() is called right here
let VC:DetailViewController = DetailViewController()
// this happens after you try to print photo
VC.photo = p
This means that your optional variable photo is still null when you print it. To avoid the error, you could unwrap the variable with
if let photo = photo {
print(photo)
}
Although this wouldn't help you accomplish your goal. So, do your tasks in viewDidAppear rather than viewDidLoad, which is called after the view becomes visible.
override func viewDidAppear() {
super.viewDidAppear()
print(photo!)
downloadFromUrl(url)
}
Or, if you need to do the task before the view appears, create a separate function in DetailViewController and call it after assigning VC.photo.
func task() {
print(photo!)
downloadFromUrl(url)
}
then in CollectionView, do
let p = self.photos[indexPath.row] as! NSDictionary
let VC:DetailViewController = DetailViewController()
VC.photo = p
VC.task()
Actually a safer way to do it is to call downloadFromUrl(url) from the didSet method like this :
var photo: NSDictionary = NSDictionary() {
didSet {
// your code to get the url here you can test if photo is nil
downloadFromUrl(url)
}
}
But this is not the problem here. Since you don't use the storyboard to initialise your viewController (let VC = DetailViewController()) you never instantiate the content of your viewController (for exemple the imageView)

Catch event in view from another class

I have async task with request where i fetching products every 3 seconds in class Item.
class Item: NSManagedObject {
var is_fetching:Bool = false;
func fetchProducts(q: String) {
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
self.is_fetching = true;
//some code
if ((response as! NSHTTPURLResponse).statusCode == 202) {
sleep(3)
self.fetchProducts(q)
return
}
if ((response as! NSHTTPURLResponse).statusCode == 200) {
self.is_fetching = false;
}
})
task.resume()
}
}
And i have UITableViewController where i show data from response. How do i update my cells when status code is 200:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:
indexPath) as! CartTableViewCell
if item.is_fetching {
cell.fetchIndicator.startAnimating();
} else {
cell.fetchIndicator.stopAnimating();
cell.fetchIndicator.hidden = true;
}
}
You can do it in few ways.
NSNotificationCenter (simplest).
You can post notifications, that will trigger your controller's methods. Looks like this:
// if response.code == 200
NSNotificationCenter.defaultCenter().postNotificationName("kSomeConstatnString", object: nil)
...
// in viewDidLoad of your controller:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateTable", object: nil)
// you also need implement updateTable() func inside your controller
// or if you need just update table
NSNotificationCenter.defaultCenter().addObserver(self.tableView, selector: "reloadData", object: nil)
// do not forget to delete observer (for instance in -deinit method)
NSNotificationCenter.defaultCenter().removeObserver(self)
// or tableView. also you can specify for which selector, if you use couple of them.
Delegate pattern.
You can describe your protocol, make your controller implement this protocol and save it as instance in your model object. Then just call methods from delegate. Details here.
Block callbacks.
Create block for action and call it from your model. For example:
// inside controller
model.refreshCallback = { Void in
self.tableView.reloadData() // or whatever
}
// inside model
var refreshCallback: (() -> Void)?
...
// if result.code == 200
if let callback = refreshCallback {
callback()
}
Use one of the UITableView’s reload functions, perhaps:
func reloadRowsAtIndexPaths(_ indexPaths: [NSIndexPath],
withRowAnimation animation: UITableViewRowAnimation)
This will cause it to ask again for the cell in question. Make sure you do this on the main thread.

Pull to Refresh: data refresh is delayed

I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!

Resources