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: [])
}
}
}
}
Related
I am writing an application to display news from the server. Created a StartController class that only displays data from the ViewModel. There are no warnings in the project, but at startup, a blank screen.
final class StartController: UIViewController {
// MARK: - Private properties
private var viewModel: ViewModel?
private let identifier = "tapeCell"
private let tableView = CustomTableView(frame: .zero)
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
setUpConstraints()
reloadViewModel()
}
// MARK: - Private functions
private func reloadViewModel() {
viewModel?.configData { [weak self] in
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
}
private func setUpView() {
view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
tableView.delegate = self
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: identifier)
view.addSubview(tableView)
}
}
// MARK: - Delegate
extension StartController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
// MARK: - DataSource
extension StartController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel?.numberOfRowsInSection() ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
let VM = self.viewModel?.articleAtIndex(indexPath.row)
var content = cell.defaultContentConfiguration()
content.text = VM?.title
content.secondaryText = VM?.author
cell.contentConfiguration = content
return cell
}
}
All the logic takes place inside the ViewModel, downloading data from the network and transferring it to the StartController. But apparently, no data is being transmitted.
final class ViewModel {
// MARK: - Private properties
private let service = TapeService()
private var data: [Articles] = []
// MARK: - Functions
func configData(completion: #escaping() -> ()) {
service.tapeAdd { [weak self] result in
switch result {
case .success(let articles):
self?.data = articles
case .failure(let error):
print(error.localizedDescription)
}
}
}
func numberOfRowsInSection() -> Int {
return data.count
}
func articleAtIndex(_ index: Int) -> ArticleViewModel {
let article = self.data[index]
return ArticleViewModel(article)
}
}
// MARK: - ArticleViewModel
final class ArticleViewModel {
private let article: Articles
init(_ article: Articles) {
self.article = article
}
var title: String {
return self.article.title ?? " "
}
var author: String {
return self.article.author ?? " "
}
}
I would be very grateful if you could tell me what my mistake is and what needs to be done to display the data. Thanks!
I am using a tableView to take some surveys.
Header I use for a question. Footer for «back» and «next» buttons. And tableView cells for answer options.
Now I started to have a problem, with some user interaction: when you simultaneously click on the “next” button and select an answer, the answer options cease to be active, nothing can be selected. Although the buttons remain active.
Tell me in what direction to look for the problem and how you can debug this problem in order to understand what's wrong.
It all started after fixing bugs, when the application crashed when simultaneously (or almost) pressing the "next" button and choosing an answer. Because the didSelectRowAt method worked after I changed the current array of answer options, and the selected index in the previous question turned out to be larger than the size of the array with the answers to the new question.
class AssessmentVC: UIViewController {
#IBOutlet weak var tableView: UITableView!
var footer: FooterTableView?
var header: UIView?
var arrayAssessmnet = [AssessmentDM]()
var assessment: AssessmentDM!
var question: QuestionDM!
var viewSeperationHeader = UIView()
var arrayOptions: [Option]?
var countAssessment = 0
var numberAssessment = 0
var numberQuestion = 0
var countQuestion = 0
var numberQusttionForLabel = 1
var arrayQuestion = [QuestionDM]()
var arrayAnswers = [AnswerDM]()
var arrayEvents = [EventDM]()
override func viewDidLoad() {
super.viewDidLoad()
settingAssessment()
}
//MARK: - settingAssessment()
private func settingAssessment() {
let id = self.assessment.serverId
arrayQuestion = QuestionDM.getQuestions(id: id)
assessmentName.text = assessment.name
countQuestion = arrayQuestion.count
let day = self.assessment.day
arrayAnswers = AnswerDM.getAnswers(idAssessment: id, day: day)
settingQuestion(eventType: .start)
}
//MARK: - settingQuestion()
private func settingQuestion(eventType: EventType? = nil) {
let prevQuestion = question
question = arrayQuestion[numberQuestion]
timeQuestion = 0
footer!.grayNextButton()
//first question
if numberQuestion == 0 && numberAssessment == 0 {
footer!.previousButton.isHidden = true
} else {
footer!.previousButton.isHidden = false
}
arrayOptions = [Option]()
let sortOption = question.options!.sorted {$0.numberOption < $1.numberOption}
for option in sortOption {
arrayOptions?.append(Option(label: option.label, value: option.value))
}
tableView.rowHeight = UITableView.automaticDimension
tableView.reloadData()
heightTableView()
tableView.setContentOffset(.zero, animated: false)
}
//MARK: - heightTableView()
func heightTableView() {
}
//MARK: - UITableViewDataSource
extension AssessmentVC: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewSeperationHeader.isHidden = false
footer?.viewSeperationFooter.isHidden = false
tableView.separatorStyle = .singleLine
return question.options?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath as IndexPath) as AnswerAssessmentCell
cell.initCell(text: arrayOptions![indexPath.row].label, value: arrayOptions![indexPath.row].value, arrayValue: arrayAnswers[numberQuestion].response, isCheckbox: true)
return cell
}
}
//MARK: - UITableViewDelegate
extension AssessmentVC: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isChangAnswerInAssessment = true
if question.answerType == "Radio" || question.answerType == "Checkbox"{
selectRadioOrChekbox(indexPath: indexPath)
}
}
}
//MARK: - selectRadioOrChekbox
extension AssessmentVC {
private func selectRadioOrChekbox(indexPath: IndexPath) {
if question.answerType == "Radio" {
let cells = tableView.visibleCells as! Array<AnswerAssessmentCell>
for cell in cells {
cell.select = false
cell.isSelected = false
}
let cell = tableView.cellForRow(at: indexPath) as! AnswerAssessmentCell
cell.select = true
cell.isSelected = true
if arrayOptions?.count ?? 0 > indexPath.row {
arrayAnswers[numberQuestion].response = arrayOptions![indexPath.row].value
footer?.greenNextButton()
}
}
if question.answerType == "Checkbox" {
if arrayOptions?.count ?? 0 > indexPath.row {
//если нажато что-то, что должно сбросить "None"
// question.options![0].isSelect = false
let cells = tableView.visibleCells as! Array<AnswerAssessmentCell>
if cells[0].answerLabel.text == "None" {
cells[0].select = false
cells[0].isSelected = false
}
var array = arrayAnswers[numberQuestion].response?.components(separatedBy: ";")
array?.removeAll { $0 == "0"}
if array?.count == 0 {
arrayAnswers[numberQuestion].response = nil
} else {
arrayAnswers[numberQuestion].response = array?.joined(separator: ";")
}
let cell = tableView.cellForRow(at: indexPath) as! AnswerAssessmentCell
cell.select = !cell.select
cell.isSelected = cell.select
arrayAnswers[numberQuestion].response = array.joined(separator: ";")
if array.count == 0 {
arrayAnswers[numberQuestion].response = nil
footer?.grayNextButton()
} else {
footer?.greenNextButton()
}
}
}
}
}
//MARK: - Navigation between questions
extension AssessmentVC {
func nextQuestion() {
footer!.grayNextButton()
numberQuestion += 1
numberQusttionForLabel += 1
settingQuestion(eventType: .next)
} else {
}
func previousQuestion() {
numberQusttionForLabel -= 1
settingQuestion(eventType: .previous)
}
}
Some snippets that can help you :
// Answer type : use enum . Here the Strong/Codable is if you want to
// save using JSON encoding/decoding
enum AnswerType: String, Codable {
case checkBox = "CheckBox"
case radio = "Radio"
}
Setup of your cell :
class AnswerAssessmentCell: UITableViewCell {
...
// work with Option type
func initCell(option: Option, response: String?, answerType: AnswerType) {
// setup cell contents (labels)
// check for selected status
switch answerType {
case .checkBox:
// check if option is in response
// set isSelected according
break
case .radio:
// check if option is response
// set isSelected according
break
}
}
}
In table view data source :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! AnswerAssessmentCell
// Use the option to init the cell
// this will also set the selected state
let optionNumber = indexPath.row
cell.initCell(option: arrayOptions![optionNumber], response: arrayAnswers[numberQuestion].response, answerType: question.answerType)
return cell
}
In Table view delegate :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isChangAnswerInAssessment = true
let optionNumber = indexPath.row
switch question.answerType {
case .radio:
selectRadio(optionNumber: optionNumber)
case .checkBox:
selectCheckBox(optionNumber: optionNumber)
}
// Reload tableview to show changes
tableView.reloadData()
}
// Separate in 2 function for smaller functions
// in this function work only with model data, the reload data will do
// cell update
// only the footer view button cooler may need to be changed
private func selectRadio(optionNumber: Int) {
// Reset current response
// set response to optionNumber
// update footer button cooler if necessary
}
private func selectCheckBox(optionNumber: Int) {
// if option is in response
// remove option from response
// else
// add response to option
// update footer button cooler if necessary
}
Hope this can help you
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
I have fetched and parsed my Data from API in JSON format to FirstViewController and want to segue to the SecondViewController with the data selected at the concrete person from FirstViewController. The Problem is that I have an API with such example URL: https://www.example.com/api/?action=persons&ln=en which gives me all persons in this format:
[
{
"p_id": "4107",
"p_name": "Name1 Surname1",
"p_role": "Role1",
"general_image": "/imagedb/persons/4107/main/1.jpg"
},{
"p_id": "1978",
"p_name": "Name2 Surname2",
"p_role": "Role2",
"general_image": "/imagedb/persons/1978/main/1.jpg"
}, {...
...}
]
I am showing all these persons in my FirstViewController in CollectionView, which is working correctly, with images, names, roles. But also I need to show my SecondViewController with the data selected in FirstVC. My API for personByID is like this URL: https://www.example.com/api/?action=person&ln=en&personId=1978 which gives me JSON Data in this format:
{
"p_id": "1978",
"p_category": "[2]",
"p_name": "Name2 Surname2",
"p_role": "Role2",
"p_short": null,
"p_text": "long text...",
"p_date_start": "1922.02.05",
"p_date_end": "",
"p_profile_image": "1",
"p_status": "1",
"p_lang": "en",
"general_image": "/imagedb/persons/1978/main/1.jpg",
"photos": [
{
"image_id": "5",
"p_id": "1978",
"lang": "en",
"text": "some text...",
"general": "/imagedb/persons/1978/5.jpg",
"thumbs": "/imagedb/persons/1978/thumb/5.jpg"
},
{
"image_id": "7",
"p_id": "1978",
"lang": "en",
"text": "some text...",
"general": "/imagedb/persons/1978/7.jpg",
"thumbs": "/imagedb/persons/1978/thumb/7.jpg"
}
]
}
This is my Person Struct:
struct Person {
let id: String
let name: String
let role: String
fileprivate let imageURLString: String
var imageURL: URL? {
return URL(string: "https://www.example.com\(imageURLString)")
}
}
extension Person: JSONDecodable {
init(_ decoder: JSONDecoder) throws {
self.id = try decoder.value(forKey: "p_id")
self.name = try decoder.value(forKey: "p_name")
self.role = try decoder.value(forKey: "p_role")
self.imageURLString = try decoder.value(forKey: "general_image")
}
}
This is My FIRST VC:
import UIKit
class PersonListViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var personPages: [PagedResult<Person>] = [] {
didSet {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadPersons()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first else {
return
}
collectionView.deselectItem(at: selectedIndexPath, animated: animated)
}
var service = ExampleWebService()
private func loadPersons(page: Int = 0, resultsPerPage: Int = 5) {
service.persons(page: page, resultsPerPage: resultsPerPage) { (personPage) in
guard !self.loadedPersonPageNumbers.contains(page) else { return }
self.personPages.append(personPage)
self.updateLastIndexPath(personPage)
}
}
private(set) var lastIndexPath: IndexPath?
private func updateLastIndexPath(_ personPage: PagedResult<Person>) {
if personPage.results.isEmpty {
lastIndexPath = nil
}
else {
lastIndexPath = calculateLastIndexPath()
}
}
private func calculateLastIndexPath() -> IndexPath? {
guard let lastPage = personPages.last else { return nil }
let section = lastPage.pageNumber
let row = lastPage.results.count - 1
return IndexPath(row: row, section: section)
}
fileprivate var loadedPersonPageNumbers: [Int] {
return personPages.map { $0.pageNumber }
}
func person(at indexPath: IndexPath) -> Person? {
guard indexPath.section < personPages.count else {
return nil
}
guard indexPath.row < personPages[indexPath.section].results.count else {
return nil
}
let page = personPages[indexPath.section]
return page.results[indexPath.row]
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let personViewController = segue.destination as? PersonViewController,
let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first else {
return
}
personViewController.person = person(at: selectedIndexPath)
}
#IBAction func exitToPersonsView(segue: UIStoryboardSegue) {
}
}
extension PersonListViewController: UICollectionViewDelegate {
fileprivate var nextPageIndex: Int {
guard let lastPage = personPages.last else {
return 0
}
return lastPage.pageNumber.advanced(by: 1)
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath == lastIndexPath {
loadPersons(page: nextPageIndex)
}
}
}
extension PersonListViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return personPages.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return personPages[section].results.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: PersonListCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! PersonListCollectionViewCell
cell.person = person(at: indexPath)
return cell
}
}
extension PersonListViewController: UINavigationBarDelegate {
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}
And this is My Second VC:
import Foundation
import UIKit
final class PersonViewController: UIViewController {
#IBOutlet weak var imagesCollectionVIew: UICollectionView!
#IBOutlet weak var personRole: UILabel!
#IBOutlet weak var customNavigationBar: UINavigationBar!
var personImagesByID: [PagedResult<Person>] = [] {
didSet {
DispatchQueue.main.async {
self.imagesCollectionVIew.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadPersonImagesByID()
}
var person: Person?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let person = person {
title = person.name
personRole.text = person.role
}
customNavigationBar.topItem?.title = title
}
var service = ExampleWebService()
private func loadPersonImagesByID(page: Int = 0, resultsperPge: Int = 5) {
service.persons(page: page, resultsPerPage: resultsperPge) { (personPage) in
guard !self.loadedPersonPageNumbers.contains(page) else {
return
}
self.personImagesByID.append(personPage)
self.updateLastIndexPath(personPage)
}
}
private(set) var lastIndexPath: IndexPath?
private func updateLastIndexPath(_ personPage: PagedResult<Person>) {
if personPage.results.isEmpty {
lastIndexPath = nil
}
else {
lastIndexPath = calculateLastIndexPath()
}
}
private func calculateLastIndexPath() -> IndexPath? {
guard let lastPage = personImagesByID.last else {
return nil
}
let section = lastPage.pageNumber
let item = lastPage.results.count - 1
return IndexPath(row: item, section: section)
}
fileprivate var loadedPersonPageNumbers: [Int] {
return personImagesByID.map { $0.pageNumber }
}
func person(at indexPath: IndexPath) -> Person? {
guard indexPath.section < personImagesByID.count else {
return nil
}
guard indexPath.item < personImagesByID[indexPath.section].results.count else {
return nil
}
let page = personImagesByID[indexPath.section]
return page.results[indexPath.item]
}
}
extension PersonViewController: UICollectionViewDelegate {
fileprivate var nextPageIndex: Int {
guard let lastPage = personImagesByID.last else {
return 0
}
return lastPage.pageNumber.advanced(by: 1)
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath == lastIndexPath {
loadPersonImagesByID(page: nextPageIndex)
}
}
}
extension PersonViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: self.imagesCollectionVIew.frame.height - 17, height: self.imagesCollectionVIew.frame.height - 17)
}
}
extension PersonViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return personImagesByID.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return personImagesByID[section].results.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: PersonViewCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImagesCollectionViewCell", for: indexPath) as! PersonViewCollectionViewCell
cell.person = person(at: indexPath)
return cell
}
}
extension PersonViewController: UINavigationBarDelegate {
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}
Now my issue is that I am having a problem of how to write correctly my second struct PersonByID and when clicking on the person from First VC to show me data in Second VC from personByID URL Path.
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