TableView reloading too early - ios

I'm running into a weird issue where my tableView is reloading too early after retrieving JSON data. The strange thing is sometimes it reloads after getting all the required data to fill the tableView and other times it reloads before it can acquire the data. I'm not entirely sure why it's doing this although I do notice sometimes the data is returned as nil. Here is what I use to retrieve the data:
var genreDataArray: [GenreData] = []
var posterStringArray: [String] = []
var posterImageArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
GenreData.updateAllData(urlExtension:"list", completionHandler: { results in
guard let results = results else {
print("There was an error retrieving genre data")
return
}
self.genreDataArray = results
for movie in self.genreDataArray {
if let movieGenreID = movie.id
{
GenrePosters.updateGenrePoster(genreID: movieGenreID, urlExtension: "movies", completionHandler: {posters in
guard let posters = posters else {
print("There was an error retrieving poster data")
return
}
for poster in posters {
if let newPoster = poster {
if self.posterStringArray.contains(newPoster){
continue
} else {
self.posterStringArray.append(newPoster)
self.networkManager.downloadImage(imageExtension: "\(newPoster)",
{ (imageData)
in
if let image = UIImage(data: imageData as Data){
self.posterImageArray.append(image)
}
})
break// Use to exit out of array after appending the corresponding poster string
}
} else {
print("There was a problem retrieving poster images")//This gets called sometimes if the poster returns nil
continue
}
}
})
}
}
DispatchQueue.main.async {
self.genresTableView.reloadData()//This is reloading too early before the data can be retrieved
}
})
}

The data is being retrieved asynchronously, and thus your table view can sometimes reload without all the data. What you can do is have the table view reload at the end of the asynchronous data retrieval, or you can reload the cells individually as they come in instead of the whole table using
let indexPath = IndexPath(item: rowNumber, section: 0)
tableView.reloadRows(at: [indexPath], with: .top)

TRY THIS-:
var genreDataArray: [GenreData] = []
var posterStringArray: [String] = []
var posterImageArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
genredataArray.removeAll()
posterStringArray.removeAll()
posterImageArray.removeAll()
NOW HERE CALL YOUR CLASS FUNCTION AS ABOVE
}

I guess in that case, you should use
dispatch_async(dispatch_get_main_queue(),{
for data in json as! [Dictionary<String,AnyObject>]
{
//take data from json. . .
}
//reload your table -> tableView.reloadData()
})
You should get the main queue of the thread.

Related

How to retrieve firestore data and populate Tableview Rows/Sections from a dictionary?

I'm trying to populate the Sections and Rows of my tableview using Firestore data that I've parsed and stored inside of a dictionary, that looks like this...
dataDict = ["Monday": ["Chest", "Arms"], "Wednsday": ["Legs", "Arms"], "Tuesday": ["Back"]]
To be frank, I'm not even sure if I'm supposed to store the data inside of a dictionary as I did. Is is wrong to do that? Also, since the data is being pulled asynchronously, how can I populate my sections and rows only after the dictionary is fully loaded with my network data? I'm using a completion handler, but when I try to print the results, of the dataDict, it prints out three arrays in succession, like so...
["Monday": ["Chest", "Arms"]]
["Tuesday": ["Back"], "Monday": ["Chest", "Arms"]]
["Tuesday": ["Back"], "Monday": ["Chest", "Arms"], "Wednsday": ["Legs", "Arms"]]
Whereas I expected it to return a single print of the array upon completion. What am I doing wrong?
var dataDict : [String:[String]] = [:]
//MARK: - viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
vcBackgroundImg()
navConAcc()
picker.delegate = self
picker.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
tableView.tableFooterView = UIView()
Auth.auth().addStateDidChangeListener { (auth, user) in
self.userIdRef = user!.uid
self.colRef = Firestore.firestore().collection("/users/\(self.userIdRef)/Days")
self.loadData { (done) in
if done {
print(self.dataDict)
} else {
print("Error retrieving data")
}
}
}
}
//MARK: - Load Data
func loadData(completion: #escaping (Bool) -> ()){
self.colRef.getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
completion(false)
}
else {
//Appending all Days collection documents with a field of "dow" to daysarray...
for dayDocument in snapshot!.documents {
self.daysArray.append(dayDocument.data()["dow"] as? String ?? "")
self.dayIdArray.append(dayDocument.documentID)
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
completion(false)
}
else {
//Assigning all Workouts collection documents belonging to selected \(dayDocument.documentID) to dictionary dataDict...
for document in snapshot!.documents {
if self.dataDict[dayDocument.data()["dow"] as? String ?? ""] == nil {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""] = [document.data()["workout"] as? String ?? ""]
} else {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""]?.append(document.data()["workout"] as? String ?? "")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
// print(self.dataDict)
}
completion(true)
}
}
}
self.dayCount = snapshot?.count ?? 0
}
}
}
I think it is just the flow of your program. Every time you loop through the collection, you add what it gets to the dictionary. So on the first pass, it will print that the dictionary has 1 item. On the second pass, it adds another item to the dictionary, and then prints the dictionary, which now has 2 items in it, so 2 items are printed. I don't think you are seeing unexpected behavior, it is just how you have your log statement ordered with how you are looping.
In other words, it makes sense that it is printing like that.
I agree with #ewizard's answer. The problem is in the flow of your program. You iterate through the documents and you fetch the documents in the collection for every iteration. You also reload the tableView and call completion closure multiple times, which you don't want to do.
To improve the flow of your program try using DispatchGroup to fetch your data and then put it together one once, when all the data is fetched. See example below to get the basic idea. My example is a very simplified version of your code where I wanted to show you the important changes you should preform.
func loadData(completion: #escaping (Bool) -> ()) {
self.colRef.getDocuments { (snapshot, err) in
// Handle error
let group = DispatchGroup()
var fetchedData = [Any]()
// Iterate through the documents
for dayDocument in snapshot!.documents {
// Enter group
group.enter()
// Fetch data
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
// Add your data to fetched data here
fetchedData.append(snapshot)
// Leave group
group.leave()
}
}
// Waits for until all data fetches are finished
group.notify(queue: .main) {
// Here you can manipulate fetched data and prepare the data source for your table view
print(fetchedData)
// Reload table view and call completion only once
self.tableView.reloadData()
completion(true)
}
}
}
I also agree with other comments that you should rethink the data model for your tableView. A much more appropriate structure would be a 2d array (an array of arrays - first array translates to the table view sections and the inner array objects translate to section items).
Here's an example:
// Table view data source
[
// Section 0
[day0, day1],
// Section 1
[day2, day3],
// Section 2
[day4, day5],
]
Example of usage:
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].count
}
}

Assign value received from #escaping to instance variable in another class in swift

I believe that I misunderstood some conception in Swift and can assign received array to my instance variable. Can somebody explain why overall my announcementsList array has 0 elements?
UIViewController.swift
var announcementsList: [Announcement] = []
override func viewDidLoad() {
super.viewDidLoad()
api.getAnnouncements(){ announcements in //<- announcements is array which has 12 elements
for ann in announcements{
self.announcementsList.append(ann)
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return announcementsList.count //<- have 0 here
}
API.swift
func getAnnouncements(completion: #escaping ([Announcement]) -> ()){
var announcements: [Announcement] = []
let url = URL(string: "https://api.ca/announcements")!
let task = self.session.dataTask(with: url) {
data, response, error in
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
guard let announcements_json = json!["announcements"] as? [[String: Any]]
else { return }
for announcement in announcements_json{
let title = announcement["title"] as! String
let desc = announcement["description"] as! String
announcements.append(Announcement(title: title,desc: desc))
}
}
completion(announcements)
}
task.resume()
}
P.S.: In my defence, I should say code works in Java pretty well
UPD
In UIViewController.swift if glance inside my announcementsList in viewWillDisappear() I will get my objects there. So I assume that tableView() started count elements earlier then they became reassigned in viewDidLoad().
The question now how to assign objects inide viewDidLoad() to a new array faster than tableView() start count them.
var announcementsList: [Announcement] = [] {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
api.getAnnouncements { announcements in
self.announcementsList = announcements
}
}
The operation is asynchronous , so the tableView reloads when the VC appears and at that moment the response isn't yet return
for ann in announcements{
self.announcementsList.append(ann)
}
self.tableView.reloadData()
BTW why not
self.announcementsList = announcements
self.tableView.reloadData()
Also don't know current thread of callback , so do
DispatchQueue.main.async {
self.announcementsList = announcements
self.tableView.reloadData()
}

Call reloadData() in the right place

I'm trying to fetch data from firebase and pass to tableview.
// Model
import UIKit
import Firebase
struct ProfInfo {
var key: String
var url: String
var name: String
init(snapshot:DataSnapshot) {
key = snapshot.key
url = (snapshot.value as! NSDictionary)["profileUrl"] as? String ?? ""
name = (snapshot.value as! NSDictionary)["tweetName"] as? String ?? ""
}
}
// fetch
var profInfo = [ProfInfo]()
func fetchUid(){
guard let uid = Auth.auth().currentUser?.uid else{ return }
ref.child("following").child(uid).observe(.value, with: { (snapshot) in
guard let snap = snapshot.value as? [String:Any] else { return }
snap.forEach({ (key,_) in
self.fetchProf(key: key)
})
}, withCancel: nil)
}
func fetchProf(key: String){
var outcome = [ProfInfo]()
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
outcome.append(info)
self.profInfo = outcome
self.tableView.reloadData()
}, withCancel: nil)
}
//tableview
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return profInfo.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "followCell", for: indexPath) as! FollowingTableViewCell
cell.configCell(profInfo: profInfo[indexPath.row])
return cell
}
However it returns one row but profInfo actually has two rows. when I implement print(self.profInfo) inside fetchProf it returns two values. But after passed to tableview, it became one. I'm not sure but I guess the reason is that I put reloadData() in the wrong place because I hit break point and reloadData() called twice. So, I think profInfo replaced by new value. I called in different places but didn't work. Am I correct? If so, where should I call reloadData()? If I'm wrong, how can I fix this? Thank you in advance!
You need to append the new data to the profinfo array. Simply replace the fetchProf method with this:-
func fetchProf(key: String){
var outcome = [ProfInfo]()
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
outcome.append(info)
self.profInfo.append(contentOf: outcome)
Dispatch.main.async{
self.tableView.reloadData()
}
} , withCancel: nil)
}
self.tableView.reloadData() must be called from the main queue. Try
DispatchQueue.main.async {
self.tableView.reloadData()
}
if you notice one thing in the following function you will see
func fetchProf(key: String){
var outcome = [ProfInfo]()
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
outcome.append(info)
//Here
/You are replacing value in self.profInfo
//for the first time when this is called it results In First profile info
//When you reload here first Profile will be shown
//Second time when it is called you again here replaced self.profInfo
//with second Outcome i.e TableView reloads and output shown is only second Profile
//you had initialised a Array self.profInfo = [ProfInfo]()
//But you just replacing array with Single value Actually you need to append data
// I think here is main issue
self.profInfo = outcome
//So try Appending data as
//self.profInfo.append(outcome) instead of self.profInfo = outcome
//Then reload TableView to get both outputs
self.tableView.reloadData()
}, withCancel: nil)
}
Table view showing one content because when table view reloaded then profile info not combine all data. You need to reload the table view after combining all data. This will help you.
// fetch
var profInfo = [ProfInfo]()
func fetchUid(){
guard let uid = Auth.auth().currentUser?.uid else{ return }
ref.child("following").child(uid).observe(.value, with: { (snapshot) in
guard let snap = snapshot.value as? [String:Any] else { return }
snap.forEach({ (key,_) in
self.fetchProf(key: key)
})
// When all key fetched completed the just reload the table view in the Main queue
Dispatch.main.async{
self.tableView.reloadData()
}
}, withCancel: nil)
}
func fetchProf(key: String){
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
self.profInfo.append(info) // Here just add the outcome object to profileinfo
}, withCancel: nil)
}
This way no need to handle another array.

Swift iOS: Firebase Paging

I have this Firebase data:
I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code
let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....
})
When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"
What am I missing here?
Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
To start on a specific key, you will have to append your query with queryStartingAtValue(_:).
Sample usage:
var count = numberOfItemsPerPage
var query ref.queryOrderedByKey()
if startKey != nil {
query = query.queryStartingAtValue(startKey)
count += 1
}
query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
// Handle error
return
}
if startKey != nil && !children.isEmpty {
children.removeFirst()
}
// Do something with children
})
I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.
Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.
Now, the model looks like so...
class FeedsModel: Decodable {
var postId: String!
var authorId: String! //The author of the post
var timestamp: Double = 0.0 //We'll use it sort the posts.
//And other properties like 'likesCount', 'postDescription'...
}
We're going to get the posts in the recent first fashion using this function
class func getFeedsWith(lastKey: String?, completion: #escaping ((Bool, [FeedsModel]?) -> Void)) {
let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
//Last key would be nil initially(for the first page).
query.observeSingleEvent(of: .value) { (snapshot) in
guard snapshot.exists(), let value = snapshot.value else {
completion(false, nil)
return
}
do {
let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
//We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
var feeds = model.map { $0.value }
//Leaving the keys aside to get the array [FeedsModel]
feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp })
//Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
if lastKey != nil { feeds = Array(feeds.dropFirst()) }
//Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
completion(true, feeds)
//We get our data sorted and ready here.
} catch let error {
print("Error occured while decoding - \(error.localizedDescription)")
completion(false, nil)
}
}
}
Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...
class FeedsViewController: UIViewController {
//MARK: - Properties
#IBOutlet weak var feedsTableView: UITableView!
var dataArray = [FeedsModel]()
var isFetching = Bool()
var previousKey = String()
var hasFetchedLastPage = Bool()
//MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
//Any other stuffs..
self.getFeedsWith(lastKey: nil) //Initial load.
}
//....
func getFeedsWith(lastKey: String?) {
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
self.isFetching = false
guard status, let feeds = data else {
//Handle errors
return
}
if self.dataArray.isEmpty { //It'd be, when it's the first time.
self.dataArray = feeds
self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
} else {
self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
//To make sure if we've fetched the last page and we're in no need to call this function anymore.
self.dataArray += feeds
//Appending the next page's feed. As we're getting the feeds in the recent first manner.
self.feedsTableView.reloadData()
}
}
}
//MARK: - TableView Delegate & DataSource
//....
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
let lastKey = self.dataArray[indexPath.row].postId
guard lastKey != self.previousKey else { return }
//Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
self.getFeedsWith(lastKey: lastKey)
self.previousKey = lastKey ?? ""
}
//....
}

Use Json data from network call in UITableView

I am trying to use the data from my network call to display in the UItableview as cell names
Here is my current view controller
import UIKit
import Alamofire
import SwiftyJSON
class ViewController: UIViewController, UITableViewDataSource{
var articles = [Article]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//get data from network call
loaddata()
//end view did load
}
func loaddata(){
Alamofire.request(.GET, "http://ip-address/test.json")
.responseJSON { response in
// print(response.request) // original URL request
// print(response.response) // URL response
//print(response.data) // server data
//print(response.result) // result of response serialization
/*if let JSON = response.result.value {
print("JSON: \(JSON)")
}*/
//get json from response data
let json = JSON(data: response.data!)
//print(json)
//for loop over json and write all article titles articles array
for (key, subJson) in json["Articles"] {
if let author = subJson["title"].string {
let artTitle = Article(name: author)
self.articles.append(artTitle!)
}
/*if let content = subJson["content"].string {
// self.Content.append(content)
}*/
}
// print("\(self.titles)")
//print("\(self.Content[0])")
//print(self.articles)
//set variable to articles number 6 to check append worked
let name = self.articles[6].name
//print varibale name to check
print("\(name)")
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//let num = articles.count
// print(num)
//return number of rows
return articles.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
//let (artTitle) = articles[indexPath.row]
// Fetches the appropriate article for the data source layout.
let article = articles[indexPath.row]
//set cell text label to article name
cell.textLabel?.text = article.name
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//end of class
}
Here is the Article.swift file
class Article {
// MARK: Properties
var name: String
// MARK: Initialization
init?(name: String) {
// Initialize stored properties.
self.name = name
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty {
return nil
}
}
}
I think I am almost there, here is what I can do so far
The network call with alamofire is working
I can get the article title using swiftJson
I then append the article title
I can then print from articles after the for loop so I know its working.
I just can't set it to the cell name
Is this because the UItableview is loaded before the data is loaded and hence article will be empty at that point?
Can you point me towards best practice for something such as this/ best design patterns and help me load data in
After you get the data from the request, call reloadData on your tableView on the main thread like so.
dispatch_async(dispatch_get_main_queue(), {
[unowned self] in
self.tableView.reloadData()
})
This will make the tableView refresh all of its contents, and your data should show up then.
Alamofire.request(.GET, Urls.menu).responseJSON { response in
if let jsonData = response.data {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
let json = JSON(data: jsonData)
//for loop over json and write all article titles articles array
newArticles = [Articles]()
for (key, subJson) in json["Articles"] {
if let author = subJson["title"].string {
if let artTitle = Article(name: author) {
newArticles.append(artTitle)
}
}
}
dispatch_async(dispatch_get_main_queue()) {
self.articles += newArticles
tableView.reloadData()
/*if let content = subJson["content"].string {
// self.Content.append(content)
}*/
}
// print("\(self.titles)")
//print("\(self.Content[0])")
//print(self.articles)
//set variable to articles number 6 to check append worked
let name = self.articles[6].name
//print varibale name to check
print("\(name)")
}
}
}

Resources