UITableView pagination, willDisplay loading infinitely - ios

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

Related

why table view cells generate before animation

I have an api that I parse and I present the data on my tableview that generates cells . I have make an animation that normally must act before as a present animation for the cells but this did not happen . the result is that the cells appear and suddenly disappear and then appear with the animation .
in the link you can find the gif that I upload the shows what happen .
https://gifyu.com/image/SEGkZ
here is the code :
The OpeningViewController is this :
import UIKit
import ViewAnimator
class OpeningViewController: UIViewController {
//MARK: - IBProperties
#IBOutlet var openingImg: UIImageView!
#IBOutlet var startButton: UIButton!
//MARK: - Properties
var nft : Nft?
//MARK: - Life Cyrcle
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animation = AnimationType.from(direction: .top, offset: 50)
openingImg.animate(animations: [animation] , delay: 0.3, duration: 2)
openingImg.layer.shadowColor = UIColor.black.cgColor
openingImg.layer.shadowOffset = CGSize(width: 0, height: 0)
openingImg.layer.shadowOpacity = 0.65
openingImg.layer.shadowRadius = 10
}
//MARK: - Methods
#IBAction func startApp(_ sender: Any) {
HapticsManager.shared.selectionVibrate()
let storyBoard = UIStoryboard(name: "Lobby", bundle: nil)
let controller = storyBoard.instantiateViewController(withIdentifier: "LobbyViewController") as! LobbyViewController
controller.modalTransitionStyle = .flipHorizontal
self.navigationController?.pushViewController(controller, animated: true)
}
}
The presentation happens in LobbyViewController :
import UIKit
import ViewAnimator
class LobbyViewController: UIViewController {
// MARK: - IBProperties
#IBOutlet weak var tableView: UITableView!
// MARK: - Properties
var data: [DataEnum] = []
var likes:[Int] = []
var numlikes: Int = 0
var nfts: [Nft] = []
let creators : [Creator] = []
var icons: [Icon] = []
var loadData = APICaller()
// MARK: - Life Cyrcle
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "AssetTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "AssetTableViewCell")
let nib2 = UINib(nibName: "CreatorsTableViewCell", bundle: nil)
tableView.register(nib2, forCellReuseIdentifier: "CreatorsTableViewCell")
tableView.dataSource = self //method to generate cells,header and footer before they are displaying
tableView.delegate = self //method to provide information about these cells, header and footer ....
downloadJSON {
self.tableView.reloadData()
print("success")
}
loadData.downloadData { (result) in
print(result)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animation = AnimationType.from(direction: .top, offset: 300)
UIView.animate(views: tableView.visibleCells,
animations: [animation], delay: 1, duration: 2)
}
//stelnei ta dedomena apo to kathe row ston PresentViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? PresentViewController {
if tableView.cellForRow(at: tableView.indexPathForSelectedRow!) is AssetTableViewCell {
destination.nft = nfts[tableView.indexPathForSelectedRow!.row-1]
destination.delegate = self
} else {
//add alert action
let alert = UIAlertController(title: "Invalid Touch", message: "You press wrong row. Choose one of the following list.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: {
return
})
}
}
}
// MARK: - Methods
func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "https://public.arx.net/~chris2/nfts.json")
URLSession.shared.dataTask(with: url!) { [self] data, response, error in
if error == nil {
do {
self.nfts = try JSONDecoder().decode([Nft].self, from: data!)
let creators = nfts.map { nft in
nft.creator
}
self.data.append(.type1(creators: creators))
self.nfts.forEach { nft in
self.data.append(.type2(nft: nft))
}
DispatchQueue.main.async {
completed()
}
}
catch {
print("error fetching data from api")
}
}
}.resume()
}
}
// MARK: - Extensions
extension LobbyViewController : UITableViewDelegate , UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? 100 : UITableView.automaticDimension
}
//gemizo ta rows tou table
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch self.data[indexPath.item] {
case .type1(let creators):
print("--->", creators)
let cell = tableView.dequeueReusableCell(withIdentifier: "CreatorsTableViewCell",
for: indexPath) as! CreatorsTableViewCell
cell.layer.cornerRadius = 15
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 0)
cell.layer.shadowOpacity = 0.8
cell.layer.shadowRadius = 15
cell.layer.cornerRadius = cell.frame.height/2
cell.updateCreators(creators)
return cell
case .type2(let nft):
let cell = tableView.dequeueReusableCell(withIdentifier: "AssetTableViewCell",
for: indexPath) as! AssetTableViewCell
cell.nameLabel?.text = nft.name
cell.nameLabel.layer.cornerRadius = cell.nameLabel.frame.height/2
cell.likesLabel?.text = "\((numlikes))"
let imgUrl = (nft.image_url)
print(imgUrl)
cell.iconView.downloaded(from: imgUrl)
cell.iconView.layer.cornerRadius = cell.iconView.frame.height/2
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "showDetails", sender: self)
}
}
extension LobbyViewController : TestDelegate{
func sendBackTheLikess(int: Int) {
numlikes = int
tableView.reloadData()
}
}
// MARK: - Enums
enum DataEnum {
case type1(creators: [Creator])
case type2(nft: Nft)
}
// MARK: - Struct
struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
The APICaller :
import Foundation
final class APICaller {
static let shared = APICaller()
public struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
public func downloadData(completion:#escaping (Result<[Nft], Error>) -> Void )
{
guard let url = URL(string:Constants.url)else{
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
//print(response)
print("here")
guard let data = data , error == nil else{
print("something went wrong with data")
return
}
print("here4")
//mexri edo exoume parei ta data kai tora me to do-catch tha ta kanoume convert se object
do{
//Decode the response
let nfts = try JSONDecoder().decode([Nft].self, from: data)
completion(.success(nfts))
print(nfts)
}catch{
completion(.failure(error))
}
}
task.resume()
}
}
Just move your animation from viewDidAppear(animated:) to tableView(_:willDisplay:forRowAt:) and call for each cell separately. Also don’t forget not to call this animation once it finished.

Expandable drop down with Multi select checkbox in Swift

check / uncheck the check box by tapping the cell in table view and how to know which cell has checked or unchecked inside Expandable drop down in Swift.
VBExpandVC
class VBExpandVC: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet var myTableView: UITableView!
struct Notification:Codable {
let notification:[Headings]
}
struct Headings:Codable {
var name:String
var status:Int
}
var names = [Headings]()
var expandTableview:VBHeader = VBHeader()
var cell : VCExpandCell!
override func viewDidLoad() {
super.viewDidLoad()
getNotifications()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
expandTableview = Bundle.main.loadNibNamed("VBHeader", owner: self, options: nil)?[0] as! VBHeader
let layer = expandTableview.viewHeader.layer
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 1)
layer.shadowOpacity = 0.4
expandTableview.lblDate.text = self.names[section].name
expandTableview.btnExpand.tag = section
expandTableview.btnExpand.addTarget(self, action: #selector(VBExpandVC.headerCellButtonTapped(_sender:)), for: UIControl.Event.touchUpInside)
let str:String = "\(self.names[section].status)"//arrStatus[section] as! String
if str == "0"
{
UIView.animate(withDuration: 2) { () -> Void in
self.expandTableview.imgArrow.image = UIImage(named :"switch")
}
}
else
{
UIView.animate(withDuration: 2) { () -> Void in
self.expandTableview.imgArrow.image = UIImage(named :"switch2")
}
}
return expandTableview
}
#objc func headerCellButtonTapped(_sender: UIButton)
{
print("header tapped at:")
print(_sender.tag)
var str:String = "\(self.names[_sender.tag].status)"
if str == "0"
{
self.names[_sender.tag].status = 1
}
else
{
self.names[_sender.tag].status = 0
}
// myTableView.reloadData()
myTableView.reloadSections([_sender.tag], with: .none)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
//Return header height as per your header hieght of xib
return 40
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
let str:Int = (names[section].status)
if str == 0
{
return 0
}
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? VCExpandCell
return cell;
}
func numberOfSections(in tableView: UITableView) -> Int
{
return self.names.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
//Return row height as per your cell in tableview
return 111
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("selected:\(indexPath.section)")
}
// getNotifications
func getNotifications(){
guard let url = URL(string: "https://www.json-generator.com/api/json/get/cgAhRPmZgy?indent=2") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
guard let data = data, error == nil, response != nil else {
return
}
do {
let headings = try JSONDecoder().decode(Notification.self, from: data)
self.names = headings.notification
DispatchQueue.main.async {
self.myTableView.reloadData()
}
} catch {
print(error)
}
}).resume()
}
// End
}
VCExpandCell
class VCExpandCell: UITableViewCell {
#IBOutlet weak var btnMobile: UIButton!
#IBOutlet weak var btnEmail: UIButton!
#IBOutlet weak var btnSms: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func btnMobileApp(_ sender: UIButton) {
print("mobile app checked")
print(sender.tag)
if sender.isSelected {
sender.isSelected = false
} else {
sender.isSelected = true
}
}
#IBAction func btnSMS(_ sender: UIButton) {
print("sms checked")
print(sender.tag)
if sender.isSelected {
sender.isSelected = false
} else {
sender.isSelected = true
}
}
#IBAction func btnEmail(_ sender: UIButton) {
print("email checked")
print(sender.tag)
if sender.isSelected {
sender.isSelected = false
} else {
sender.isSelected = true
}
}
}
enter image description here
In the above code, I have two major problems.
selected check box positions are changing when expanded the section and expanded another section
Unable to find selected check boxes by tapping the cell in table view inside Expand drop down.
Have a look ont his given url:
https://github.com/AssistoLab/DropDown

Refresh pagination data in UITableView

I have implemented pagination in UITableView with WillDisplay method. Pagination process is working fine but if I need to reload a list on button click, then data is appending in the list. How to work around with this ?
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if (indexPath.row + 1 == playlistViewModel.numberOfRowsInSection()) {
if playlistViewModel.isReload != false {
pageIncrement += 1
playlistViewModel.playListingApi(enterView: false, page: pageIncrement)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
pageIncrement = 1
playlistViewModel.playListingApi(enterView: true, page: pageIncrement)
}
playlistViewModel.hitNextApiClosure = { [weak self] () in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self?.playlistViewModel.isReload = false
self?.playlistViewModel.playlistArray?.removeAll()
self?.playlistTableView.reloadData()
self?.pageIncrement = 1
self?.playlistViewModel.playListingApi(enterView: true, page: self?.pageIncrement ?? 1)
}
}
And ViewModel method is
func playListingApi(enterView: Bool, page: Int) {
self.isLoading = true
if (enterView){
playlistArray?.removeAll()
isReload = false
}
playlistService.getPlayList(page: "\(page)", limit: "20") { (result) in
self.isLoading = false
switch result {
case .success(let data):
self.playlist = data as? Playlist
guard let data = self.playlist?.data?.blocks else {
self.errorMessage = AlertMessage.somethingWentWrong
return
}
for playlistData in data {
self.playlistArray?.append(playlistData)
self.isReload = true
}
if (data.count == 0){
self.isReload = false
}
self.reloadTableBool = true
case .error(let message):
self.isReload = false
self.errorMessage = message
}
}
}
When you are reloading your tableView set page = 1 , empty tableView data source and reload tableView. Finally hitAPI for fresh set of data .
page = 1
mTblDataSource.removeAll()
mTableView.reloadData()
hitAPI()
Consider this one as a possible solution.
public class Pageable<T> {
public enum ObjectState {
case loading
case loaded
}
public private (set) var page: Int = 0
private var items: [T] = [T]()
private var state: ObjectState = .loading
private let itemsPerPage: Int
private var itemsReloaded: (() -> ())
public init(itemsPerPage: Int, items: [T] = [], itemsReloaded: #escaping (() -> ())) {
self.items = items
self.itemsPerPage = itemsPerPage
self.itemsReloaded = itemsReloaded
}
public var itemsCount: Int {
switch state {
case .loaded:
return items.count
case .loading:
return items.count + 1 // should be displaying cell with loading indicator
}
}
public var isLoaded: Bool {
return state == .loaded
}
public var isLoading: Bool {
return state == .loading
}
public func append(contentsOf items: [T]) {
state = items.count < itemsPerPage ? .loaded : .loading
self.items.append(contentsOf: items)
itemsReloaded()
}
public func incrementPage() {
page += 1
}
public func reset() {
page = 0
state = .loading
items = []
}
public func itemFor(_ index: Int) -> T? {
return items.indices.contains(index) ? items[index] : nil
}
}
struct Property {}
protocol SearchItemsDisplayLogic: class {
func reloadItemsViews()
}
protocol SearchItemsInteraction {
func loadMore(page: Int)
}
// MARK: View Related with UITableView example
lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(pullToRefresh(_:)), for: UIControl.Event.valueChanged)
return refreshControl
}()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return presenter.itemsCount
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
presenter.viewWillDisplayCellAt(indexPath.row)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if presenter.isLoadingCellNeeded(indexPath.row) {
return tableView.dequeueReusableCell(withIdentifier: "\(LoadingTableViewCell.self)", for: indexPath)
}
let cell = tableView.dequeueReusableCell(withIdentifier: "\(PropertyTableViewCell.self)", for: indexPath) as? PropertyTableViewCell
presenter.populate(cell: cell, indexPath: indexPath)
return cell ?? UITableViewCell(style: .default, reuseIdentifier: "\(UITableViewCell.self)")
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let property = presenter.property(indexPath.row) else {
return
}
}
protocol SearchItemsPresentation {
// MARK: Pagination logic
var itemsCount: Int { get }
// From the view.
func isLoadingCellNeeded(_ item: Int) -> Bool
func viewWillDisplayCellAt(_ item: Int)
func pullToRefresh()
func property(_ item: Int) -> Property?
// From the interactor.
func presentItems(items: [Property])
}
// MARK: - Presenter
class SearchItemsPresenter: SearchItemsPresentation {
weak var propertyDisplay: SearchItemsDisplayLogic?
lazy var interactor: SearchItemsInteraction? = {
return SearchItemsInteractor(presenter: self)
}()
var itemsCount: Int {
return pageable.itemsCount
}
private var pageable: Pageable<Property>!
init(viewController: SearchItemsDisplayLogic) {
self.propertyDisplay = viewController
pageable = Pageable(itemsPerPage: 15, itemsReloaded: {
self.propertyDisplay?.reloadItemsViews()
})
}
// TODO: presenter should not have UIKit!
func populate(cell: CellProtocol?, indexPath: IndexPath) {
guard let cell = cell else { return }
// populate
}
}
extension SearchItemsPresenter {
func property(_ index: Int) -> Property? {
return pageable.itemFor(index)
}
}
// MARK: Pageable
extension SearchItemsPresenter {
/// if it's loading show loading cell in the view.
func isLoadingCellNeeded(_ item: Int) -> Bool {
let isViewAtTheBottom = item == itemsCount - 1
return isViewAtTheBottom && pageable.isLoading
}
/// Called in `willDisplay` methods of the view.
func viewWillDisplayCellAt(_ item: Int) {
let isViewAtTheBottom = item == itemsCount - 1
if isViewAtTheBottom && pageable.isLoading {
interactor?.loadMore(page: pageable.page)
pageable.incrementPage()
}
}
func pullToRefresh() {
pageable.reset()
interactor?.loadMore(page: pageable.page)
pageable.incrementPage()
}
func presentItems(items: [Property]) {
pageable.append(contentsOf: items)
}
}
// MARK: - Interactor
class SearchItemsInteractor: SearchItemsInteraction {
private var presenter: SearchItemsPresentation
init(presenter: SearchItemsPresentation) {
self.presenter = presenter
}
func loadMore(page: Int) {
DispatchQueue.global(qos: .background).async {
sleep(1)
DispatchQueue.main.async {
// TODO: return some data
self.presenter.presentItems(items: [])
}
}
}
}

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.

UIRefresh control endRefreshing doesn't work

When refresh control is triggered by swiping the tableview down, if there is no internet connection, a alert is shown and the refresh control is expected to end refreshing but it doesn't end refreshing even added in main thread
class JobsForCategoryVC: UIViewController {
//MARK:-Outlets
#IBOutlet weak var jobTableView: UITableView!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
//MARK:-Properties
var refreshControl:UIRefreshControl!
var jobCategory:JobCategoryDB!
var pageNumber:Int = 1
var downloadMore:Bool = true
var jobs = [JobModel]()
//MARK:-LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
setupView()
freshDownload()
}
func setupView(){
refreshControl = UIRefreshControl()
refreshControl.attributedTitle = NSAttributedString(string: "Loading fresh Jobs")
refreshControl.addTarget(self, action: #selector(self.freshDownload), for: .valueChanged)
jobTableView.addSubview(refreshControl)
}
func freshDownload(){
pageNumber = 1
downloadMore = true
downloadJobsFrom(top: true)
}
func downloadJobsFrom(top:Bool){
if !refreshControl.isRefreshing && top{
activityIndicator.startAnimating()
}
let url = URLStringList.getSearchCategoryJobString(pageNumber: pageNumber, categoryId: jobCategory.id!)
if let url = URL(string: url){
Alamofire.request(url, method: .get).responseJSON { (response) in
if response.result.isSuccess{
let json = response.result.value
let model = Mapper<JobModel>().mapArray(JSONArray: json as! [[String : Any]])
if model?.count == 0{
self.downloadMore = false
}
if let jobs = model{
if top{
self.jobs = jobs
}else{
self.jobs += jobs
}
self.jobTableView.reloadData()
self.pageNumber += 1
}
self.refreshControl.endRefreshing()
self.activityIndicator.stopAnimating()
}else{
self.activityIndicator.stopAnimating()
DispatchQueue.main.async(execute: {
self.refreshControl.endRefreshing()
self.jobTableView.reloadData()
})
if top{
showInternetConnectionAlert(viewController: self, activityIndicator: self.activityIndicator, completion: nil)
}
}
}
}
}
}
extension JobsForCategoryVC:UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if jobs.count > 0 {
jobTableView.backgroundView = nil
let cellCount = jobs.count + ((jobs.count-1)/(AdForNumberOfCells-1)) + 1
return cellCount
}
jobTableView.backgroundView = Bundle.main.loadNibNamed("PullToRefreshView", owner: nil, options: nil)?.first as? PullToRefreshView
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row % AdForNumberOfCells == 0{
if let cell = tableView.dequeueReusableCell(withIdentifier: "JobsAdTableViewCell", for: indexPath) as? JobsAdTableViewCell{
cell.controller = self
return cell
}
}else{
if let cell = tableView.dequeueReusableCell(withIdentifier: "JobsTableViewCell", for: indexPath) as? JobsTableViewCell{
let index = NSIndexPath(item: indexPath.row-(indexPath.row/AdForNumberOfCells)-1, section: 0)
cell.configure(job: jobs[index.row])
return cell
}
}
return UITableViewCell()
}
}
extension JobsForCategoryVC:UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let webView = storyboard?.instantiateViewController(withIdentifier: "WKWebVC") as? WKWebVC{
if indexPath.row % AdForNumberOfCells == 0 {
return
}
let index = NSIndexPath(item: indexPath.row-(indexPath.row/AdForNumberOfCells)-1, section: 0)
if let urlString = jobs[index.row].url{
webView.url = urlString
webView.titleString = jobs[index.row].title
present(webView, animated: true, completion: nil)
}
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == self.jobs.count - 1 && downloadMore{
downloadJobsFrom(top: false)
}
}
}
Simple way of doing this is
DispatchQueue.main.async(execute: {
self.jobTableView.reloadData()
self.refreshControl.endRefreshing()
self.activityIndicator.stopAnimating()
self.refreshControl.setContentOffset(CGPoint.zero, animated: true)
})
Set UITableView contentOffset to zero
In Swift 3.0
refreshControl.endRefreshing()
self.yourTableView.contentOffset = CGPoint.zero
Please add all UI procedures in the main thread.
The Success block => Turn your code into :
if let jobs = model{
if top{
self.jobs = jobs
}else{
self.jobs += jobs
}
self.pageNumber += 1
}
DispatchQueue.main.async(execute: {
self.refreshControl.endRefreshing()
self.activityIndicator.stopAnimating()
self.jobTableView.reloadData()
})
And the Fail Block :
DispatchQueue.main.async(execute: {
self.refreshControl.endRefreshing()
self.activityIndicator.stopAnimating()
self.jobTableView.reloadData()
})
No matter success or failure,all your UI Progress must included in the main thread.I think you forgot to include the UI Changes inside main thread in Success Block.Or you can do like that,
Alamofire.request(url, method: .get).responseJSON { (response) in
if response.result.isSuccess{
...
}else{
...
}
DispatchQueue.main.async(execute: {
self.refreshControl.endRefreshing()
self.activityIndicator.stopAnimating()
self.jobTableView.reloadData()
})
}
Take A Look :
RefreshControlDemo [Swift 3 Xcode 8]
i find same problem
if another use self.refresh.endRefreshing() not work
I introduce this code -> UIRefreshControl().endRefreshing() replace in state self.refresh.endRefreshing()
DispatchQueue.main.async {
UIRefreshControl().endRefreshing()
self.yourTableView.contentOffset = CGPoint.zero
}
thank you

Resources