Having troubles with pagination on swift collectionView - swift

My goal is load more info when the user reaches the end of my collectionViewPokemon, but the getPokes func I'm using to achieve this is being called X amount of times, corresponding to the while statement I implemented to get the info from other endpoints. That's far from the behavior I expected.
This is how my code looks:
Service class
class Service {
let baseUrl = "https://pokeapi.co/api/v2/pokemon"
let limit = 20
var offset = 0
var isPaginating = false
func getPokes(pagination: Bool = false,
handler: #escaping ([Pokemon]) -> Void) {
let topCalls = (limit*5)-limit
let originalPokes = [Pokemon]()
let morePokemons = [Pokemon]()
var endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
if pagination {
while offset < topCalls {
isPaginating = true
offset += limit
endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
self.mainApiCall(mainList: morePokemons, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
if pagination {
self.isPaginating = true
}
case .failure(let error):
print(error)
}
}
}
} else {
self.mainApiCall(mainList: originalPokes, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
case .failure(let error):
print(error)
}
}
}
}
ViewController class (the properties)
class ViewController: UIViewController,
UICollectionViewDelegateFlowLayout,
UICollectionViewDelegate,
UICollectionViewDataSource,
UIScrollViewDelegate {
private let service = Service()
private var pagingChecker = false
var generalList = [Pokemon]()
var pokemons = [Pokemon]()
var morePokemons = [Pokemon]()
. . .
}
viewDidLoad method (this is ok so far)
override func viewDidLoad() {
super.viewDidLoad()
setViews()
service.getPokes(pagination: false) { [self] (pokes) in
pokemons = pokes
generalList += pokemons
DispatchQueue.main.async {
collectionViewPokemon?.reloadData()
}
}
}
scrollViewDidScroll method (here's the error)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let position = scrollView.contentOffset.y
if position > ((collectionViewPokemon!.contentSize.height) - 200 - scrollView.frame.size.height) {
guard !service.isPaginating else { return }
if !pagingChecker {
service.getPokes(pagination: true) { [self] (morePokes) in
morePokemons = morePokes
generalList += morePokemons
DispatchQueue.main.async {
collectionViewPokemon?.reloadData()
}
}
} else {
pagingChecker = true
print("Done paging.")
}
}
}
The thing is I wanna fetch just one array of pokemons at a time, but instead I'm making all the endpoint calls at the same time, and also appending them to my generalList array, who owns my collectionView datasource and all that stuff.
PD: Idk if it'll help, but here's a screenshot of my console. The first msg is ok, but it should print the others only when the user reaches the end of the CollectionView, and well, that's not happening (instead, they're all coming back at the same time!)
I can't figure out how to get this done. I would really appreciate any advice or correction, thanks.

I figured out what was happening. I was using a while statement to repeat my Api call as long as it satisfy it's condition (limit<topCalls).
So, I worked out some ways to not using that while statement, and ended up using a function to construct every URL, and then, set up a shouldPage parameter that checked if that apiCall needed to be done or not.
This is how my code ended up:
let baseUrl = "https://pokeapi.co/api/v2/pokemon"
let limit = 20
var offset = 0
func getPokes(pageNumber: Int = 1,
shouldPage: Bool = true,
handler: #escaping ([Pokemon]) -> Void) {
let originalPokes = [Pokemon]()
let morePokemons = [Pokemon]()
let endpoint = buildEndpoint(shouldPage: shouldPage)
if shouldPage {
mainApiCall(mainList: morePokemons, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
case .failure(let error):
print(error)
}
}
} else {
mainApiCall(mainList: originalPokes, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
case .failure(let error):
print(error)
}
}
}
}
private func buildEndpoint(shouldPage: Bool = false) -> String {
var endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
if shouldPage {
offset += limit
endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
}
return endpoint
}
That's on my Service class. So, in my CollectionView Datasource, I call getPokes(shouldPage: false) on cellForItemAt, and in willDisplay cell I say shouldPage is true. That fixed the problem.

Related

Сache images one by one passing them into an array (Kingfisher)

I have to cache images one by one passing them into an array.
When I configure Controller, I got array of images from API. Images I got using Animation, every 0.1 sec I got new Image. But when I got all of them, I need to use cached images instead of load them again
Some variables for help
private var imagesCount = 0
private var nextImage = 0
private var imagesArray: [String] = []
private var isAnimating = true {
didSet {
if isAnimating {
animateImage()
}
}
}
Here I configure VC, and do imagesArray = images, then I will use only my array of urls
func configureView(images: ApiImages) {
guard let images = images.images else { return }
imagesArray = images
imagesCount = images.count
imageView.setImage(imageUrl: images[nextImage])
nextImage += 1
animateImage()
}
When I start my animation. Every 0.1 I get new one image, but after the end of cycle I want to use cached images
func animateImage() {
UIView.transition(with: self.imageView, duration: 0.1, options: .transitionCrossDissolve) { [weak self] in
guard let self = self else { return }
self.imageView.setImage(imageUrl: self.imagesArray[self.nextImage])
} completion: { [weak self] _ in
guard let self = self else { return }
if self.nextImage == self.imagesCount - 1{
//here is end of cycle
self.nextImage = 0
} else {
self.nextImage += 1
}
if self.isAnimating {
self.animateImage()
}
}
}
I use kingfisher, so what options I have to put here??
extension UIImageView {
func setImage(imageUrl: String, completion: (() -> ())? = nil) {
self.kf.setImage(with: URL(string: imageUrl), options: [.transition(.fade(0.5)), .alsoPrefetchToMemory]) { result in
completion?()
}
}
}
Thank you!

Firestore pagination using MVVM architecture swift

I don't quite understand what I am doing wrong since I am very new to MVVM. It worked in MVC architecture. I've setup my VM and am able to get the first set of results and even then that's not working properly. I get 4 results instead of 10 which is what LOADLIMIT is set as. I was able to get it to work in an MVC architecture without any issues. The VM function which triggers the query is called multiple (3) times instead of just once i.e. even prior to scrolling.
Here is my VM:
enum FetchRestaurant {
case success
case error
case location
case end
}
class ListViewModel {
let restaurant: [Restaurant]?
let db = Firestore.firestore()
var restaurantArray = [Restaurant]()
var lastDocument: DocumentSnapshot?
var currentLocation: CLLocation?
typealias fetchRestaurantCallback = (_ restaurants: [Restaurant]?, _ message: String?, _ status: FetchRestaurant) -> Void
var restaurantFetched: fetchRestaurantCallback?
var fetchRestaurant: FetchRestaurant?
init(restaurant: [Restaurant]) {
self.restaurant = restaurant
}
func fetchRestaurantCallback (callback: #escaping fetchRestaurantCallback) {
self.restaurantFetched = callback
}
func fetchRestaurants(address: String) {
print("address received: \(address)")
getLocation(from: address) { location in
if let location = location {
self.currentLocation = location
self.queryGenerator(at: location)
} else {
self.restaurantFetched?(nil, nil, .location)
}
}
}
func queryGenerator(at location: CLLocation) {
var query: Query!
if restaurantArray.isEmpty {
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).limit(to: Constants.Mealplan.LOADLIMIT)
} else {
print("last document:\(String(describing: lastDocument?.documentID))")
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).start(afterDocument: lastDocument!).limit(to: Constants.Mealplan.LOADLIMIT)
}
batchFetch(query: query)
}
func batchFetch(query: Query) {
query.getDocuments { (querySnapshot, error) in
if let error = error {
self.restaurantFetched?(nil, error.localizedDescription, .error)
} else if querySnapshot!.isEmpty {
self.restaurantFetched?(nil, nil, .end)
} else if !querySnapshot!.isEmpty {
let queriedRestaurants = querySnapshot?.documents.compactMap { querySnapshot -> Restaurant? in
return try? querySnapshot.data(as: Restaurant.self)
}
guard let restaurants = queriedRestaurants,
let currentLocation = self.currentLocation else {
self.restaurantFetched?(nil, nil, .end)
return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: restaurants, location: currentLocation))
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
self.restaurantFetched?(self.restaurantArray, nil, .success)
})
self.lastDocument = querySnapshot!.documents.last
}
}
}
func getLocation(from address: String, completionHandler: #escaping (_ location: CLLocation?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location else {
completionHandler(nil)
return
}
completionHandler(location)
}
}
}
And in the VC viewDidLoad:
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
I would really appreciate links or resources for implementing MVVM in Swift for a Firestore back-end. I'm coming up short on searches here and on Google. Even tried medium.
EDIT
class ListViewController: UITableViewController {
lazy var loadingShimmer: UIImageView = {
let image = UIImage(named: "shimmer_background")
let imageview = UIImageView(image: image)
imageview.contentMode = .top
imageview.translatesAutoresizingMaskIntoConstraints = false
return imageview
}()
var restaurantArray = [Restaurant]()
var planDictionary = [String: Any]()
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
setupTable()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = false
}
func setupTable() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Restaurant", style: .plain, target: nil, action: nil)
tableView.register(RestaurantCell.self, forCellReuseIdentifier: "Cell")
tableView.delegate = self
tableView.dataSource = self
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: -navigationBarHeight, right: 0)
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.addSubview(loadingShimmer)
loadingShimmer.topAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.topAnchor).isActive = true
loadingShimmer.leadingAnchor.constraint(equalTo: tableView.leadingAnchor).isActive = true
loadingShimmer.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true
loadingShimmer.startShimmering()
initialSetup()
}
func initialSetup() {
let addressOne = planDictionary["addressOne"] as! String + ", "
let city = planDictionary["city"] as! String + ", "
let postalCode = planDictionary["postalCode"] as! String
currentAddress = addressOne + city + postalCode
setupSearch()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching {
print("\(fetchMore), \(reachedEnd)")
if !fetchMore && !reachedEnd {
if let address = self.currentAddress {
print("address sent: \(address)")
listViewModel.fetchRestaurants(address: address)
}
}
}
}
}
That you're only getting back 4 results instead of 10 is not due to a faulty query or get-document request—those are coded properly. You're either losing documents when you parse them (some are failing Restaurant initialization), Constants.Mealplan.LOADLIMIT is wrong, or there aren't more than 4 documents in the collection itself that satisfy the query.
That the query is executed 3 times instead of once is also not due to anything in this code—viewDidLoad is only called once and geocodeAddressString only returns once. You're making a fetch request elsewhere that we can't see.
In the batchFetch method, you have a guard that returns out of the function without ever calling its completion handler. This will leave the UI in a state of limbo. I'd recommend always calling the completion handler no matter why the function returns.
You never manage the document cursor. If the get-document return has less documents than the load limit, then nil the last-document cursor. This way, when you attempt to get the next page of documents, guard against a nil cursor and see if there is even more to fetch.
There's no need to pass in an empty array and have your function fill it; simply construct and return an array of results within ListViewModel itself.
We can't see how you trigger pagination. Is it through a scroll delegate when the user reaches the bottom or through a button tap, for example? If it's through a scroll delegate, then I'd disable that for now and see how many returns you get—I suspect one, instead of 3.
What is the particular reason you've ditched MVC for MVVM here? With MVC, you can get pagination up with just a few lines of code. I think MVVM is overkill for iOS applications and would advise against using it unless you have a compelling reason.

SwiftUI app freezing when using multiple product identifiers in StoreKit

I'm currently learning Swift and following some tutorials but I'm stuck on a StoreKit issue.
The code works when I provide a single productIdentifier, but when I provide more than 1 in the Set, the entire app hangs on loading. This is in the iOS Simulator, and on a device. I've got 2 identifiers in the set, and both of these work individually, but not at the same time. My code looks the same as the original tutorial (video) so I don't know where I'm going long.
Entire Store.swift file below. Problem appears to be in the fetchProducts function, but I'm not sure. Can anyone point me in the right direction?
import StoreKit
typealias FetchCompletionHandler = (([SKProduct]) -> Void)
typealias PurchaseCompletionHandler = ((SKPaymentTransaction?) -> Void)
class Store: NSObject, ObservableObject {
#Published var allRecipes = [Recipe]() {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
for index in self.allRecipes.indices {
self.allRecipes[index].isLocked = !self.completedPurchases.contains(self.allRecipes[index].id)
}
}
}
}
private let allProductIdentifiers = Set(["com.myname.ReceipeStore.test", "com.myname.ReceipeStore.test2"])
private var completedPurchases = [String]()
private var productsRequest: SKProductsRequest?
private var fetchedProducts = [SKProduct]()
private var fetchCompletionHandler: FetchCompletionHandler?
private var purchaseCompletionHandler: PurchaseCompletionHandler?
override init() {
super.init()
startObservingPaymentQueue()
fetchProducts { products in
self.allRecipes = products.map { Recipe(product: $0) }
}
}
private func startObservingPaymentQueue() {
SKPaymentQueue.default().add(self)
}
private func fetchProducts(_ completion: #escaping FetchCompletionHandler) {
guard self.productsRequest == nil else { return }
fetchCompletionHandler = completion
productsRequest = SKProductsRequest(productIdentifiers: allProductIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
private func buy(_ product: SKProduct, competion: #escaping PurchaseCompletionHandler) {
purchaseCompletionHandler = competion
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
}
extension Store {
func product(for identififier: String) -> SKProduct? {
return fetchedProducts.first(where: { $0.productIdentifier == identififier })
}
func purchaseProduct(_ product: SKProduct) {
buy(product) { _ in }
}
}
extension Store: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
var shouldFinishTransactions = false
switch transaction.transactionState {
case .purchased, .restored:
completedPurchases.append(transaction.payment.productIdentifier)
shouldFinishTransactions = true
case .failed:
shouldFinishTransactions = true
case .deferred, .purchasing:
break
#unknown default:
break
}
if shouldFinishTransactions {
SKPaymentQueue.default().finishTransaction(transaction)
DispatchQueue.main.async {
self.purchaseCompletionHandler?(transaction)
self.purchaseCompletionHandler = nil
}
}
}
}
}
// loading products from the store
extension Store: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let loadedProducts = response.products
let invalidProducts = response.invalidProductIdentifiers
guard !loadedProducts.isEmpty else {
print("Could not load the products!")
if !invalidProducts.isEmpty {
print("Invalid products found: \(invalidProducts)")
}
productsRequest = nil
return
}
// cache the feteched products
fetchedProducts = loadedProducts
// notify anyone waiting on the product load (swift UI view)
DispatchQueue.main.async {
self.fetchCompletionHandler?(loadedProducts)
self.fetchCompletionHandler = nil
self.productsRequest = nil
}
}
}```
It looks like you're running all of your requests on the main DispatchQueue, this will block other main queue work until completed. You should consider handling some of these tasks with a custom concurrent queue. This bit of sample code should get the ball rolling.
func requestProducts(_ productIdentifiers: Set<ProductIdentifier>, handler: #escaping ProductRequestHandler) {
// Set request handler
productRequest?.cancel()
productRequestHandler = handler
// Request
productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest?.delegate = self
productRequest?.start()
}
func requestPrices() {
// Retry interval, 5 seconds, set this to your liking
let retryTimeOut = 5.0
var local1: String? = nil
var local2: String? = nil
let bundleIdentifier = Bundle.main.bundleIdentifier!
let queue = DispatchQueue(label: bundleIdentifier + ".IAPQueue", attributes: .concurrent)
// Request price
queue.async {
var trying = true
while(trying) {
let semaphore = DispatchSemaphore(value: 0)
requestProducts(Set(arrayLiteral: SettingsViewController.pID_1000Credits, SettingsViewController.pID_2000Credits)) { (response, error) in
local1 = response?.products[0].localizedPrice
local2 = response?.products[1].localizedPrice
semaphore.signal()
}
// We will keep checking on this thread until completed
_ = semaphore.wait(timeout: .now() + retryTimeOut)
if(local2 != nil) { trying = false }
}
// Update with main thread once request is completed
DispatchQueue.main.async {
self.price1 = local1 ?? "$0.99"
self.price2 = local2 ?? "$1.99"
}
}
}
extension SKProduct {
// Helper function, not needed for this example
public var localizedPrice: String? {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = self.priceLocale
return formatter.string(from: self.price)
}

Pagination with Firebase firestore - swift 4

I'm trying to paginate data (infinitely scroll my tableview) using firestore. I've integrated the code google gives for pagination as best I can, but I'm still having problems getting the data to load in correctly.
The initial set of data loads into the tableview as wanted. Then, when the user hits the bottom of the screen, the next "x" amount of items are load in. But when the user hits the bottom of the screen the second time, the same "x" items are simply appended to the table view. The same items keep getting added indefinitely.
So its the initial 3 "ride" objects, followed by the next 4 "ride" objects repeating forever.
123 4567 4567 4567 4567...
How do I get the data to load in correctly?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height {
// Bottom of the screen is reached
if !fetchingMore {
beginBatchFetch()
}
}
}
func beginBatchFetch() {
// Array containing "Ride" objcets is "rides"
fetchingMore = true
// Database reference to "rides" collection
let ridesRef = db.collection("rides")
let first = ridesRef.limit(to: 3)
first.addSnapshotListener { (snapshot, err) in
if let snapshot = snapshot {
// Snapshot isn't nil
if self.rides.isEmpty {
// rides array is empty (initial data needs to be loaded in).
let initialRides = snapshot.documents.compactMap({Ride(dictionary: $0.data())})
self.rides.append(contentsOf: initialRides)
self.fetchingMore = false
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
self.tableView.reloadData()
})
print("first rides loaded in")
}
} else {
// Error
print("Error retreiving rides: \(err.debugDescription)")
return
}
// reference to lastSnapshot
guard let lastSnapshot = snapshot!.documents.last else{
// The collection is empty
return
}
let next = ridesRef.limit(to: 4).start(afterDocument: lastSnapshot)
next.addSnapshotListener({ (snapshot, err) in
if let snapshot = snapshot {
if !self.rides.isEmpty {
let newRides = snapshot.documents.compactMap({Ride(dictionary: $0.data())})
self.rides.append(contentsOf: newRides)
self.fetchingMore = false
DispatchQueue.main.asyncAfter(deadline: .now() + 7, execute: {
self.tableView.reloadData()
})
print("new items")
return
}
} else {
print("Error retreiving rides: \(err.debugDescription)")
return
}
})
}
}
So here's the solution I've come up with! It is very likely that this solution makes multiple calls to firestore, creating a large bill for any real project, but it works as a proof of concept I guess you could say.
If you have any recommendations or edits, please feel free to share!
var rides = [Ride]()
var lastDocumentSnapshot: DocumentSnapshot!
var fetchingMore = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
//print("offsetY: \(offsetY) | contHeight-scrollViewHeight: \(contentHeight-scrollView.frame.height)")
if offsetY > contentHeight - scrollView.frame.height - 50 {
// Bottom of the screen is reached
if !fetchingMore {
paginateData()
}
}
}
// Paginates data
func paginateData() {
fetchingMore = true
var query: Query!
if rides.isEmpty {
query = db.collection("rides").order(by: "price").limit(to: 6)
print("First 6 rides loaded")
} else {
query = db.collection("rides").order(by: "price").start(afterDocument: lastDocumentSnapshot).limit(to: 4)
print("Next 4 rides loaded")
}
query.getDocuments { (snapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if snapshot!.isEmpty {
self.fetchingMore = false
return
} else {
let newRides = snapshot!.documents.compactMap({Ride(dictionary: $0.data())})
self.rides.append(contentsOf: newRides)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.tableView.reloadData()
self.fetchingMore = false
})
self.lastDocumentSnapshot = snapshot!.documents.last
}
}
}
A little late in the game, but I would like to share how I do it, using the query.start(afterDocument:) method.
class PostsController: UITableViewController {
let db = Firestore.firestore()
var query: Query!
var documents = [QueryDocumentSnapshot]()
var postArray = [Post]()
override func viewDidLoad() {
super.viewDidLoad()
query = db.collection("myCollection")
.order(by: "post", descending: false)
.limit(to: 15)
getData()
}
func getData() {
query.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
querySnapshot!.documents.forEach({ (document) in
let data = document.data() as [String: AnyObject]
//Setup your data model
let postItem = Post(post: post, id: id)
self.postArray += [postItem]
self.documents += [document]
})
self.tableView.reloadData()
}
}
}
func paginate() {
//This line is the main pagination code.
//Firestore allows you to fetch document from the last queryDocument
query = query.start(afterDocument: documents.last!)
getData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postArray.count
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Trigger pagination when scrolled to last cell
// Feel free to adjust when you want pagination to be triggered
if (indexPath.row == postArray.count - 1) {
paginate()
}
}
}
Result like so:
Here is a reference.
My solution was similar to #yambo, however, I tried to avoid making extra calls to the database. After the first call to the database, I get back 10 objects and when it is time to load the new page I kept a reference of how many objects and I checked if the count + 9 is in the range of my new count.
#objc func LoadMore() {
let oldCount = self.uploads.count
guard shouldLoadMore else { return }
self.db.getNextPage { (result) in
switch result {
case .failure(let err):
print(err)
case .success(let newPosts):
self.uploads.insert(contentsOf: newPosts, at: self.uploads.count)
if oldCount...oldCount+9 ~= self.uploads.count {
self.shouldLoadMore = false
}
DispatchQueue.main.async {
self.uploadsView.collectionView.reloadData()
}
}
}
}
Simple, Fast and easy way is...
class FeedViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, FeedCellDelegate {
private var quotes = [Quote]() {
didSet{ tbl_Feed.reloadData() }
}
var quote: Quote?
var fetchCount = 10
#IBOutlet weak var tbl_Feed: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
fetchPost()
}
// MARK: - API
func fetchPost() {
reference(.Quotes).limit(to: getResultCount).getDocuments { (snapshot, error) in
guard let documents = snapshot?.documents else { return }
documents.forEach { (doc) in
let quotes = documents.map {(Quote(dictionary: $0.data()))}
self.quotes = quotes
}
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let currentOffset = scrollView.contentOffset.y
let maxxOffset = scrollView.contentSize.height - scrollView.frame.size.height
if maxxOffset - currentOffset <= 300 { // Your cell size 300 is example
fetchCount += 5
fetchPost()
print("DEBUG: Fetching new Data")
}
}
}

can't generate tableview using rxcoca

I have an app, that shows current weather. Data is downloaded via Alamofire from forecast.io. Result forms a table with forecast. I used simple tableviewdelegate and tableviewDatasource, everything works. But now I wanted to learn some reactive, using rxswift and rxcocoa. After some googling, and tutorial from raywanderlich: I changed my code to:
var week = Variable<[DailyWeather]>([])
override func viewDidLoad() {
super.viewDidLoad()
week.asObservable().subscribe { (e) in
self.generateTable()
}.addDisposableTo(disposeBag)
// tableView.delegate = self
// tableView.dataSource = self
tableView.backgroundColor = UIColor(red:0.81, green:0.81, blue:0.81, alpha:1)
refreshControl.tintColor = UIColor(red:1, green:1, blue:1, alpha:1)
self.tableView.addSubview(self.refreshControl)
manager.delegate = self
manager.requestWhenInUseAuthorization()
updateLocation()
}
func downloadData(_ completion: #escaping DownloadComplete) {
var weatherURL: String {
if pre == "ru" {
return("\(BASE_URL)\(API_KEY)/\(latitudeNew),\(longitudeNew)?units=si&lang=ru")
} else {
return("\(BASE_URL)\(API_KEY)/\(latitudeNew),\(longitudeNew)?units=si")
}
}
print(weatherURL)
if let url = URL(string: weatherURL) {
let request = Alamofire.request(url)
request.validate().responseJSON { response in
switch response.result {
case .success:
self.week.value = []
self.weekly = []
if let data = response.result.value as! [String: AnyObject]! {
self.weatherDict = data["currently"] as! [String: AnyObject]!
self.currentDict = CurrentWeather(weatherDictionary: self.weatherDict)
self.dailyArray = data["daily"]?["data"] as! [[String: AnyObject]]!
for dailyWeather in self.dailyArray {
let daily = DailyWeather(dailyWeatherDict: dailyWeather)
self.weekly.append(daily)
}
for x in 0...7 {
if x == 0 {
} else {
self.week.value.append(self.weekly[x])
}
}
completion()
}
case .failure(let error):
self.showAlert("You are offline", message: "Enable network connection and try again")
print("Alamofire error: \(error)")
}
}
}
}
func generateTable() {
week.asObservable().bindTo(tableView.rx.items(cellIdentifier: "WeatherViewCell", cellType: WeatherViewCell.self)) { (index, weather, cell) in
cell.configureCell(daily: weather, index: index)
}.addDisposableTo(disposeBag)
}
But I receive this fatal error:
fatal error: Failure converting from Optional(<UIView: 0x7facad60f620; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x60800002e520>>) to UITableViewDataSource: file /Users/ruslansabirov/Desktop/swift/Havo4/Pods/RxCocoa/RxCocoa/RxCocoa.swift, line 146
(lldb)
Pls, help, what I'm doing wrong?
Check table view delegate and datasource methods.
if tableview delegate is set in your code then you must implement the delegate method of tableview while using RXSwift otherwise application got crash.
you only need to call func generateTable() once. The tableview will update automatically. As it is a data bindingoverride
func viewDidLoad() {
super.viewDidLoad()
self.generateTable()
tableView.backgroundColor = UIColor(red:0.81, green:0.81, blue:0.81, alpha:1)
refreshControl.tintColor = UIColor(red:1, green:1, blue:1, alpha:1)
self.tableView.addSubview(self.refreshControl)
manager.delegate = self
manager.requestWhenInUseAuthorization()
updateLocation()
}