Infinite scrolling UITableView with numbers - ios

I need to create infinite table with numbers, that is, when scrolling, new cells with numbers should be created.
I create APICaller with counter, pagination, arrays and while loop.
Also I create UITableView with func scrollViewDidScroll(_ scrollView: UIScrollView) which append new values in a table.
My ViewController with UITableView
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {
private let apiCaller = APICaller()
private let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.register(UITableViewCell.self,
forCellReuseIdentifier: "cell")
return tableView
}()
private var data = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.dataSource = self
tableView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
apiCaller.fetchData(pagination: false, completion: { [weak self] result in
switch result {
case.success(let data):
self?.data.append(contentsOf: data)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case.failure(_):
break
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = String(describing: data[indexPath.row])
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let position = scrollView.contentOffset.y
if position > (tableView.contentSize.height-100-scrollView.frame.size.height) {
guard !apiCaller.isPaginating else { return }
apiCaller.fetchData(pagination: true) { [weak self] result in
switch result {
case .success(let moreData):
self?.data.append(contentsOf: moreData)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case .failure(_):
break
}
}
}
}
}
In this case of APICaller I have only 100 cells, which corresponds to the loop constraint (but if i remove break from the while loop, nothing appears)
class APICaller {
private var counter = 0
var isPaginating = false
func fetchData(pagination: Bool = false, completion: #escaping (Result<[Int], Error>) -> Void) {
if pagination {
isPaginating = true
}
DispatchQueue.global().asyncAfter(deadline: .now() + (pagination ? 3 : 2), execute: {
var newData: [Int] = [0]
var originalData: [Int] = [0]
while true {
self.counter += 1
originalData.append(self.counter)
if self.counter == 100 {
break
}
}
completion(.success(pagination ? newData : originalData))
if pagination {
self.isPaginating = false
}
})
}
}
So, how can i get a table with infinite numbers?

The basic idea is right. When you scroll to a point where more rows are needed, fetch them. But doing this in the UIScrollViewDelegate is an expensive place to do that. I.e., that method is called for every pixel of movement and will result in many redundant calls.
Personally, I would advise moving this logic to the appropriate table view methods. For example, at a bare minimum, you might do it in the UITableViewDataSource method (i.e., if you are handling a row more than n rows from the end of your data set, fetch more data). E.g.,
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count + 1 // NB: one extra for the final “busy” cell
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.row + 50) >= data.count { // scrolled within 50 rows of end
fetch()
}
if indexPath.row >= data.count { // if at last row, show spinner
return tableView.dequeueReusableCell(withIdentifier: "busy", for: indexPath)
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = String(describing: data[indexPath.row])
return cell
}
}
A few things to note there:
I'm reporting one more row than I have thus far. This is for my “busy” cell (a cell with a spinning UIActivityIndicatorView). That way, if the user ever scrolls faster than the network response can handle, we at least show the user a spinner to let them know that we are fetching more data.
Thus, the cellForRowAt checks the row, and shows the “busy cell” if necessary.
And in this case, when I'm within 50 items of the end, I will initiate a fetch of the next batch of data.
Even better than the above, I would also marry the UITableViewDataSource implementation with a UITableViewDataSourcePrefetching. Thus, set the tableView.prefetchDataSource and then implement prefetchRowsAt:
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard
let maxIndexPath = indexPaths.max(by: { $0.row < $1.row }), // get the last row, and
maxIndexPath.row >= data.count // see if it exceeds what we have already fetched
else { return }
fetch(pagination: true)
}
}
By the way, a few notes on fetch:
func fetch(pagination: Bool = false) {
apiCaller.fetchData(pagination: pagination) { [weak self] result in
guard
let self = self,
case .success(let values) = result
else { return }
let oldCount = self.data.count
self.data.append(contentsOf: values)
let indexPaths = (oldCount ..< self.data.count).map { IndexPath(row: $0, section: 0) }
self.tableView.insertRows(at: indexPaths, with: .automatic)
}
}
Note, I would advise not “reloading” the whole table view, but rather just “inserting” the appropriate rows. I've also moved the “am I busy” logic into the APICaller, where it belongs:
class APICaller {
private var counter = 0
private var isInProgress = false
func fetchData(pagination: Bool = false, completion: #escaping (Result<[Int], Error>) -> Void) {
guard !isInProgress else {
completion(.failure(APIError.busy))
return
}
isInProgress = true
DispatchQueue.main.asyncAfter(deadline: .now() + (pagination ? 3 : 2)) { [weak self] in
guard let self = self else { return }
let oldCounter = self.counter
self.counter += 100
self.isInProgress = false
let values = Array(oldCounter ..< self.counter)
completion(.success(values))
}
}
}
extension APICaller {
enum APIError: Error {
case busy
}
}
I not only simplified the APICaller, but also made it thread-safe (by moving all state mutation and callbacks on the the main queue). If you start some asynchronous task on a background queue, dispatch the updates and callback to the main queue. But do not mutate objects from background threads (or if you must, add some synchronization logic).

Related

Scroll to the latest inserted row in UITableView using Realm Objects

I have the following code which is working fine, it gets a list of items from a list in Realm called groceryList and displays them on a UITableView in descending order based on the productName. What I would like to be able to do is scroll to the latest inserted row/item in the table, right now when a new item is inserted the user may not see it since the items are alphabetically reordered and the latest item may not be visible on the tableView.
How can I scroll to the latest inserted row/item in a UITableView?
Realm Objects:
class Item:Object{
#objc dynamic var productName:String = ""
#objc dynamic var isItemActive = true
#objc dynamic var createdAt = NSDate()
}
class ItemList: Object {
#objc dynamic var listName = ""
#objc dynamic var createdAt = NSDate()
let items = List<Item>()
}
UITableView:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
var allItems : Results<Item>!
var groceryList : ItemList!
override func viewDidLoad() {
super.viewDidLoad()
groceryList = realm.objects(ItemList.self).filter("listName = %#", "groceryList").first
updateResultsList()
}
func updateResultsList(){
if let list = groceryList{
allItems = activeList.items.sorted(byKeyPath: "productName", ascending: false)
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reusableCell", for: indexPath) as! CustomCell
let data = allItems[indexPath.row]
cell.displayProductName.text = data.productName
return cell
}
}
You can use Realm notifications to know when the data source Results has been modified, then update your table view from there and do the scrolling as well.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var allItems: Results<Item>!
var groceryList: ItemList!
var notificationToken: NotificationToken? = nil
deinit {
notificationToken?.invalidate()
}
override func viewDidLoad() {
super.viewDidLoad()
groceryList = realm.objects(ItemList.self).filter("listName = %#", "groceryList").first
updateResultsList()
observeGroceryList
}
func updateResultsList(){
if let list = groceryList {
allItems = activeList.items.sorted(byKeyPath: "productName", ascending: false)
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reusableCell", for: indexPath) as! CustomCell
let data = allItems[indexPath.row]
cell.displayProductName.text = data.productName
return cell
}
func observeGroceryList() {
notificationToken = allItems.observe { [weak self] (changes: RealmCollectionChange) in
switch changes {
case .initial:
self?.tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
self?.tableView.beginUpdates()
self?.tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
self?.tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
self?.tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
self?.tableView.endUpdates()
if let lastInsertedRow = insertions.last {
self?.tableView.scrollToRow(at: insertions.last, at: .none, animated: true)
}
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
print("\(error)")
}
}
}
}
Add below code as extension of tableview.
extension UITableView {
func scrollToBottom() {
let sections = numberOfSections-1
if sections >= 0 {
let rows = numberOfRows(inSection: sections)-1
if rows >= 0 {
let indexPath = IndexPath(row: rows, section: sections)
DispatchQueue.main.async { [weak self] in
self?.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
}
}
}
Now simply use it in your method:
func updateResultsList(){
if let list = groceryList{
allItems = activeList.items.sorted(byKeyPath: "productName", ascending: false
yourTableView.scrollToBottom()
}
}
Just use this method where you want, it should be scroll down.
yourTableView.scrollToBottom()

Pagination code being triggered upon launch instead of when user reaches bottom of page

I'm trying to implement an infinite scroll/pagination feature to a table view. I use scrollViewDidScroll to measure when the user reaches the bottom of the page, which then triggers a function to fetch the next batch of data. However I think the measurements are off because my fetchMoreEvents function is being triggered upon the launch of the app.
This is the pagination code (scrollViewDidScroll and fetchMoreEvents):
func fetchMoreEvents() {
fetchingMore = true
var page = 1
page += 1
let seatGeekApiUrl = URL(string: "https://api.seatgeek.com/2/events?venue.state=NY&page=\(page)&client_id=MTM5OTE0OTd8MTU0MjU2NTQ4MC4z")!
fetchData(url: seatGeekApiUrl) { (result: FetchResult<Welcome>) -> (Void) in
switch result {
case .success(let object): self.eventData.append(contentsOf: object.events)
print("\neventData: \n\n\(self.eventData)")
case .failure(let error):
print("\nError decoding JSON: \n\n\(error)")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
print("\nFetching next batch of events: (Page \(page))\n")
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height {
if !fetchingMore {
fetchMoreEvents()
}
}
}
Once fetchMoreEvents is triggered, I have it append my eventData array with the next page of results and reload the table view. My print statement confirms that it fetches page 2 of the data, but like I said that happens immediately instead of when I scroll down the page. Also, it never gets triggered again.
Is this an issue with the measurements in scrollViewDidScroll, or am I going wrong somewhere else?
These are the table view methods if they're applicable here:
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return eventData.count
} else if section == 1 && fetchingMore {
return 1
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "eventsCell", for: indexPath) as! EventsTableViewCell
let event = eventData[indexPath.row]
// Labels
cell.eventNameLabel.text = event.title
cell.eventVenueLabel.text = event.venue.nameV2
cell.eventAddressLabel.text = event.venue.address
cell.eventTimeLabel.text = dateFormatter.string(from: event.dateTimeLocal)
// Image
if let urlString = event.performers[0].image, let imageURL = URL(string: urlString) {
ImageService.getImage(url: imageURL) { (image) in
cell.eventImageView.image = image
}
}
else {
cell.eventImageView.image = UIImage(named: "noImageFound")
}
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingCell
cell.spinner.startAnimating()
return cell
}
}
Declare an Bool as property
var isAllRowSeeked: Bool = false
Put this logic in your scrollViewDidScroll
let height = scrollView.frame.size.height
let contentYoffset = scrollView.contentOffset.y
let distanceFromBottom = scrollView.contentSize.height - contentYoffset
if distanceFromBottom < height,
self.isAllRowSeeked == true {
// you've reached the end, you are now ready to load more data
self.isAllRowSeeked = false
}
Now in cellForRowAtIndexPath
if fetchingMore == true,
isLastSectionRow(indexPath: indexPath) {// it's the last row of this section
state.isAllRowSeeked = true
return paginatorUI?.getPaginatedLoadMoreCell()
} else {
return nil
}
Add the following method
public func isLastSectionRow(indexPath: IndexPath) -> Bool {
let lastSection = tableview.dataSource?.numberOfSections?(in: tableview)
?? 1
let lastRow = tableview.dataSource?.tableView(tableview,
numberOfRowsInSection: indexPath.section)
?? 0
return lastSection == (indexPath.section+1) && lastRow == (indexPath.row+1)
}
Actually this logic is borrowed from one of my pod, which you can use. Complete code for this pagination can be found here

UITableView pagination, willDisplay loading infinitely

I'm trying to implement a chat feature in my app which will mimic iMessages' pull to load more messages. My API sends 20 messages in each call along with pageIndex and other values to keep track of pages and messages.
I'm implementing pagination using TableView willDisplay and pull to refresh features.
I'm not able to add correct logic to load more messages in willDisplay and it's going into infinite loop. Can anyone point me to right direction by looking at below code?
import UIKit
class ChatViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextViewDelegate {
#IBOutlet weak var messagesTable: UITableView!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
var messages: Messages!
var messageArray = [Message]()
// Pagination
var isLoading = false
var pageSize = 10
var totalPages: Int!
var currentPage: Int!
// Pull To Refresh
let refreshControl = UIRefreshControl()
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
activityIndicator.startAnimating()
fetchMessages(page: 1, completed: {
self.totalPages = self.messages.pageCount
self.currentPage = self.messages.currentPage
// Sort message by ID so that latest message appear at the bottom.
self.messageArray = self.messages.messages!.sorted(by: {$0.id! < $1.id!})
self.messagesTable.reloadData()
// Scroll to the bottom of table
self.messagesTable.scrollToBottom(animated: false)
self.activityIndicator.stopAnimating()
})
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messageArray.count
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if !self.isLoading && indexPath.row == 0 {
self.isLoading = true
fetchMessages(page: self.currentPage, completed: {
if self.currentPage == 0 {
self.messageArray.removeAll()
}
self.messageArray.append(contentsOf: self.messages!.messages!)
self.messageArray = self.messageArray.sorted(by: {$0.id! < $1.id!})
self.messagesTable.reloadData()
// Scroll to the top
self.messagesTable.scrollToRow(at: indexPath, at: UITableViewScrollPosition.top, animated: true)
self.currentPage = self.currentPage + 1
})
self.isLoading = false
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as? MessageCell else {
fatalError("Can't find cell")
}
return cell
}
private func fetchMessages(page: Int, completed: #escaping () -> ()){
guard let url = URL(string: "http://example.com/....") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Error in fetching data..........")
print(error!.localizedDescription)
}
guard let data = data else { return }
if let str = String(data: data, encoding: .utf8) { print(str) }
do {
let resultData = try JSONDecoder().decode(messagesStruct.self, from: data)
DispatchQueue.main.async {
print("DispatchQueue.main.async")
self.messages = resultData.data!
completed()
}
} catch let jsonError {
print(jsonError)
}
}.resume()
}
//Pull to refresh
#objc func refresh(_ refreshControl: UIRefreshControl) {
fetchMessages(completed: {
self.messagesTable.reloadData()
})
refreshControl.endRefreshing()
}
}
willDisplayCell is not the safe place to check if tableView is actually scrolled to bottom rather use scrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.isLoading && (messagesTable.contentOffset.y >= (messagesTable.contentSize.height - messagesTable.frame.size.height)) {
self.isLoading = true
fetchMessages(page: self.currentPage, completed: {[weak self]
guard let strongSelf = self else {
return
}
strongSelf.isLoading = false
if strongSelf.currentPage == 0 {
strongSelf.messageArray.removeAll()
}
strongSelf.messageArray.append(contentsOf: strongSelf.messages!.messages!)
strongSelf.messageArray = strongSelf.messageArray.sorted(by: {$0.id! < $1.id!})
strongSelf.messagesTable.reloadData()
strongSelf.currentPage = strongSelf.currentPage + 1
})
}
}
Hope it helps

UITableView Scrolls To Undesired Index or Position

Basically the task is to load new API response as the user scrolls to down in UITableView without stopping user interaction and maintaining the current position (live feeds like Facebook and Instagram).
What I want to do is that parsing and then viewing Rest API's response in UITableView with custom cells.
The issue is that when the user scrolls to bottom the API call is made and table view scrolls to some other position where the user is.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "xxxxx", for:indexPath) as! xxxxxxxx
return cell;
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView == tableView {
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height) {
print("Scroll Ended")
fetchDataFromApi()
}
}
}
func fetchDataFromApi() {
let jsonUrlString = "xxxxxxx"
guard let url = URL(string: jsonUrlString) else {
return
}
print(url)
URLSession.shared.dataTask(with: url) {(data,response,err) in
guard let data = data else {
return
}
do {
let apiResponseData = try JSONDecoder().decode(ApiResponse.self, from: data)
if apiResponseData.httpStatusCode == HttpStatusCode.OK {
if let apiResponse = apiResponseData.data{
self.array.append(contentsOf: apiResponse)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
else {
print("API Response Error : \(apiResponseData.httpStatusCode) \(apiResponseData.errorMessage)")
}
}catch let jsonErr {
print("Serialization Error \(jsonErr)")
}
}.resume()
}}
Try with this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "xxxxx", for:indexPath) as! xxxxxxxx
let lastRowIndex = tableView.numberOfRows(inSection: tableView.numberOfSections-1)
if (indexPath.row == lastRowIndex - 1) {
//print("last row selected")
fetchDataFromApi()
}
return cell;
}
If you are using this so no need to call this method
scrollViewDidEndDragging
try insertCellAtIndexPath replace for tableview.reloadData() in
if let apiResponse = apiResponseData.data{
self.array.append(contentsOf: apiResponse)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
......
There are several ways to implement this. The most favourable as far as I see it is to implement a paginated API and call the API once the UITableView's last item. You may use:
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let botEdge = scrollView.contentOffset.y + scrollView.frame.size.height;
if (botEdge >= scrollView.contentSize.height)
{
// Call API and reload TableView on completion handler.
}
}
Call the tableView reload on the completion handler of the API method.
Or you can do it in a slightly more awkward way:
public var indexPathsForVisibleRows: [NSIndexPath]? { get }
Get the array of index paths of the currently visible cells and scroll to the index after you reload the UITableView using:
tableView.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.None, animated: false)
It was because of dynamic cell sizes I was setting in table view

Proper Placement of dispatchGroup to reloadData

I have a tableview function that is pulling data from a database to render cells. I want to accomplish the goal of not reloading my tableview so much. I learned that dispatch groups would be the way to go beause I don't want to return to the completion block that reloads the tableView until all the data has been pulled however when I use the dispatchGroup it never reaches the completion it just stops. The placement of my variables may be in the wrong place but i just can't really see where I should put it. I have been moving it to different places and still nothing.
import UIKit
import Firebase
class FriendsEventsView: UITableViewController{
var cellID = "cellID"
var friends = [Friend]()
var attendingEvents = [Event]()
//label that will be displayed if there are no events
var currentUserName: String?
var currentUserPic: String?
var currentEventKey: String?
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Friends Events"
view.backgroundColor = .white
// Auto resizing the height of the cell
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "close_black").withRenderingMode(.alwaysOriginal), style: .done, target: self, action: #selector(self.goBack))
tableView.register(EventDetailsCell.self, forCellReuseIdentifier: cellID)
self.tableView.tableFooterView = UIView(frame: CGRect.zero)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.global(qos: .background).async {
print("This is run on the background queue")
self.fetchEventsFromServer { (error) in
if error != nil {
print(error)
return
} else {
DispatchQueue.main.async {
self.tableView.reloadData()
print("This is run on the main queue, after the previous code in outer block")
}
}
}
}
}
#objc func goBack(){
dismiss(animated: true)
}
override func numberOfSections(in tableView: UITableView) -> Int {
// print(friends.count)
return friends.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// print(friends[section].events.count)
return friends[section].collapsed ? 0 : friends[section].events.count
}
func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! EventDetailsCell? ?? EventDetailsCell(style: .default, reuseIdentifier: cellID)
// print(indexPath.row)
cell.details = friends[indexPath.section].events[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as? CollapsibleTableViewHeader ?? CollapsibleTableViewHeader(reuseIdentifier: "header")
// print(section)
header.arrowLabel.text = ">"
header.setCollapsed(friends[section].collapsed)
print(friends[section].collapsed)
header.section = section
// header.delegate = self
header.friendDetails = friends[section]
return header
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
func fetchEventsFromServer(_ completion: #escaping (_ error: Error?) -> Void ){
//will grab the uid of the current user
guard let myUserId = Auth.auth().currentUser?.uid else {
return
}
let ref = Database.database().reference()
//checking database for users that the current user is following
ref.child("following").child(myUserId).observeSingleEvent(of: .value, with: { (followingSnapshot) in
//handling potentail nil or error cases
guard let following = followingSnapshot.children.allObjects as? [DataSnapshot]
else {return}
//validating if proper data was pulled
let group = DispatchGroup()
for followingId in following {
group.enter()
UserService.show(forUID: followingId.key, completion: { (user) in
PostService.showFollowingEvent(for: followingId.key, completion: { (event) in
self.attendingEvents = event
var friend = Friend(friendName: (user?.username)!, events: self.attendingEvents, imageUrl: (user?.profilePic)!)
self.friends.append(friend)
})
})
}
this loop should return to the completon block in viewWillAppear following the execution of this if statement
if self.friends.count == following.count{
group.leave()
let result = group.wait(timeout: .now() + 0.01)
//will return this when done
completion(nil)
}
}) { (err) in
completion(err)
print("Couldn't grab people that you are currently following: \(err)")
}
}
Any help is greatly appreciated
You want to place the group.leave() inside of the PostService.showFollowingEvent callback.
Now you call enter following.count-times, but you call leave only once. For the group to continue you have to leave the group as many times as you entered it:
for followingId in following {
group.enter()
UserService.show(forUID: followingId.key, completion: { (user) in
PostService.showFollowingEvent(for: followingId.key, completion: { (event) in
self.attendingEvents = event
var friend = Friend(friendName: (user?.username)!, events: self.attendingEvents, imageUrl: (user?.profilePic)!)
self.friends.append(friend)
// leave here
group.leave()
})
})
}
Moreover, I would not recommend using group.wait since you are facing a possible deadlock. If any of the callbacks that are supposed to call group.leave are happening on the same thread as group.wait was called, they will never get called and you will end up with the frozen thread. Instead, use group.notify:
group.notify(queue: DispatchQueue.main) {
if self.friends.count == following.count {
completion(nil)
}
}
This will allow the execution on the main thread, but once all the tasks are finished, it will execute the provided callback closure.

Resources