UITableView ładuje się więcej podczas przewijania do dołu, jak aplikacja Facebook

96

Tworzę aplikację korzystającą z SQLite. Chcę wyświetlić listę użytkowników (UITableView) przy użyciu mechanizmu paginacji. Czy ktoś mógłby mi powiedzieć, jak załadować więcej danych do mojej listy, gdy użytkownik przewija do końca listy (jak na stronie głównej w aplikacji Facebook)?

rokridi
źródło

Odpowiedzi:

103

Możesz to zrobić, dodając sprawdzenie, gdzie jesteś w cellForRowAtIndexPath:metodzie. Ta metoda jest łatwa do zrozumienia i wdrożenia:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Classic start method
    static NSString *cellIdentifier = @"MyCell";
    MyCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell)
    {
        cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MainMenuCellIdentifier];
    }

    MyData *data = [self.dataArray objectAtIndex:indexPath.row];
    // Do your cell customisation
    // cell.titleLabel.text = data.title;

    BOOL lastItemReached = [data isEqual:[[self.dataArray] lastObject]]; 
    if (!lastItemReached && indexPath.row == [self.dataArray count] - 1)
    {
        [self launchReload];
    }
}

EDYCJA: dodano sprawdzenie ostatniej pozycji, aby zapobiec wywołaniom rekurencyjnym. Będziesz musiał zaimplementować metodę określającą, czy ostatni element został osiągnięty, czy nie.

EDIT2: wyjaśniono lastItemReached

shinyuX
źródło
9
Co jeśli użytkownik przewija w górę iw dół, więc cellForRowAtIndexPath nazywa się WIELE RAZY! ??
onmyway133
Po pierwszym przewinięciu do końca jego lista zostanie ponownie załadowana. Za każdym razem, gdy dotrze do dna, zostanie zebrana nowa porcja danych. Jeśli konieczne będzie zastosowanie jakiegoś określonego zabiegu, za jego wykonanie będzie odpowiadać launchReloadmetoda (na przykład tylko jedno asynchroniczne przeładowanie na raz)
shinyuX
4
Musiałem dodać flagę, aby zapobiec problemowi z rekurencją, gdy ostatni element został trafiony:if !lastItemReached && indexPath.row == dataArray!.hits.count - 1 {
Albert Bori
Jaka jest self.launchReloadmetoda?
suwak
1
@shinyuX nie działa dla mnie, "if" zawsze jest fałszywe ... ale jeśli (lastItemReached && indexPath.row == [self.dataArray count] - 1) true, DLACZEGO?
powiedział
69

Szybki

Metoda 1: Czy przewinięto do dołu

Oto odpowiedź Pedro Romão w wersji Swift . Gdy użytkownik przestaje przewijać, sprawdza, czy doszło do końca.

func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {

    // UITableView only moves in one direction, y axis
    let currentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height

    // Change 10.0 to adjust the distance from bottom
    if maximumOffset - currentOffset <= 10.0 {
        self.loadMore()
    }
}

Metoda 2: osiągnięto ostatni wiersz

A oto odpowiedź shinyuX w wersji Swift . Sprawdza, czy użytkownik dotarł do ostatniego wiersza.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    // set up cell
    // ...

    // Check if the last row number is the same as the last current data element
    if indexPath.row == self.dataArray.count - 1 {
        self.loadMore()
    }

}

Przykład loadMore()metody

Skonfigurowałem te trzy zmienne klasowe do pobierania partii danych.

// number of items to be fetched each time (i.e., database LIMIT)
let itemsPerBatch = 50

// Where to start fetching items (database OFFSET)
var offset = 0

// a flag for when all database items have already been loaded
var reachedEndOfItems = false

Jest to funkcja umożliwiająca załadowanie większej liczby elementów z bazy danych do widoku tabeli.

func loadMore() {

    // don't bother doing another db query if already have everything
    guard !self.reachedEndOfItems else {
        return
    }

    // query the db on a background thread
    DispatchQueue.global(qos: .background).async {

        // determine the range of data items to fetch
        var thisBatchOfItems: [MyObjects]?
        let start = self.offset
        let end = self.offset + self.itemsPerBatch

        // query the database
        do {
            // SQLite.swift wrapper
            thisBatchOfItems = try MyDataHelper.findRange(start..<end)
        } catch _ {
            print("query failed")
        }

        // update UITableView with new batch of items on main thread after query finishes
        DispatchQueue.main.async {

            if let newItems = thisBatchOfItems {

                // append the new items to the data source for the table view
                self.myObjectArray.appendContentsOf(newItems)

                // reload the table view
                self.tableView.reloadData()

                // check if this was the last of the data
                if newItems.count < self.itemsPerBatch {
                    self.reachedEndOfItems = true
                    print("reached end of data. Batch count: \(newItems.count)")
                }

                // reset the offset for the next data query
                self.offset += self.itemsPerBatch
            }

        }
    }
}
Suragch
źródło
Użyłem metody 1, ponieważ chciałem pobrać więcej danych. Działa świetnie. Dzięki wam obojgu!
Bob Wakefield
37

Lepiej użyć willDisplayCellmetody, aby sprawdzić, która komórka zostanie załadowana. Gdy uzyskamy prąd indexPath.row, możemy załadować więcej ogniw. Spowoduje to załadowanie większej liczby komórek podczas przewijania w dół.

 - (void)tableView:(UITableView *)tableView 
       willDisplayCell:(UITableViewCell *)cell    
       forRowAtIndexPath:(NSIndexPath *)indexPath
{
    // check if indexPath.row is last row
    // Perform operation to load new Cell's.
}
Suraj Mirajkar
źródło
16
nie jest lepiej, ponieważ reloadData ponownie wywoła tę metodę, prawda?
Marcin
Czy to zadziała, jeśli będziemy mieć również sekcje?
Abdul Yasin
Tak, to zadziała dla sekcji, indexPath da ci wiersz i sekcję.
Suraj Mirajkar
24

Detale

  • Swift 5.1, Xcode 11.2.1

Rozwiązanie

Pracował z UIScrollView / UICollectionView / UITableView

import UIKit

class LoadMoreActivityIndicator {

    private let spacingFromLastCell: CGFloat
    private let spacingFromLastCellWhenLoadMoreActionStart: CGFloat
    private weak var activityIndicatorView: UIActivityIndicatorView?
    private weak var scrollView: UIScrollView?

    private var defaultY: CGFloat {
        guard let height = scrollView?.contentSize.height else { return 0.0 }
        return height + spacingFromLastCell
    }

    deinit { activityIndicatorView?.removeFromSuperview() }

    init (scrollView: UIScrollView, spacingFromLastCell: CGFloat, spacingFromLastCellWhenLoadMoreActionStart: CGFloat) {
        self.scrollView = scrollView
        self.spacingFromLastCell = spacingFromLastCell
        self.spacingFromLastCellWhenLoadMoreActionStart = spacingFromLastCellWhenLoadMoreActionStart
        let size:CGFloat = 40
        let frame = CGRect(x: (scrollView.frame.width-size)/2, y: scrollView.contentSize.height + spacingFromLastCell, width: size, height: size)
        let activityIndicatorView = UIActivityIndicatorView(frame: frame)
        activityIndicatorView.color = .black
        activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
        activityIndicatorView.hidesWhenStopped = true
        scrollView.addSubview(activityIndicatorView)
        self.activityIndicatorView = activityIndicatorView
    }

    private var isHidden: Bool {
        guard let scrollView = scrollView else { return true }
        return scrollView.contentSize.height < scrollView.frame.size.height
    }

    func start(closure: (() -> Void)?) {
        guard let scrollView = scrollView, let activityIndicatorView = activityIndicatorView else { return }
        let offsetY = scrollView.contentOffset.y
        activityIndicatorView.isHidden = isHidden
        if !isHidden && offsetY >= 0 {
            let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
            let offsetDelta = offsetY - contentDelta

            let newY = defaultY-offsetDelta
            if newY < scrollView.frame.height {
                activityIndicatorView.frame.origin.y = newY
            } else {
                if activityIndicatorView.frame.origin.y != defaultY {
                    activityIndicatorView.frame.origin.y = defaultY
                }
            }

            if !activityIndicatorView.isAnimating {
                if offsetY > contentDelta && offsetDelta >= spacingFromLastCellWhenLoadMoreActionStart && !activityIndicatorView.isAnimating {
                    activityIndicatorView.startAnimating()
                    closure?()
                }
            }

            if scrollView.isDecelerating {
                if activityIndicatorView.isAnimating && scrollView.contentInset.bottom == 0 {
                    UIView.animate(withDuration: 0.3) { [weak self] in
                        if let bottom = self?.spacingFromLastCellWhenLoadMoreActionStart {
                            scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0)
                        }
                    }
                }
            }
        }
    }

    func stop(completion: (() -> Void)? = nil) {
        guard let scrollView = scrollView , let activityIndicatorView = activityIndicatorView else { return }
        let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
        let offsetDelta = scrollView.contentOffset.y - contentDelta
        if offsetDelta >= 0 {
            UIView.animate(withDuration: 0.3, animations: {
                scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
            }) { _ in completion?() }
        } else {
            scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
            completion?()
        }
        activityIndicatorView.stopAnimating()
    }
}

Stosowanie

w tym

activityIndicator = LoadMoreActivityIndicator(scrollView: tableView, spacingFromLastCell: 10, spacingFromLastCellWhenLoadMoreActionStart: 60)

obsługa

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        activityIndicator.start {
            DispatchQueue.global(qos: .utility).async {
                sleep(3)
                DispatchQueue.main.async { [weak self] in
                    self?.activityIndicator.stop()
                }
            }
        }
    }
}

Pełna próbka

Nie zapomnij wkleić kodu rozwiązania.

import UIKit

class ViewController: UIViewController {

    fileprivate var activityIndicator: LoadMoreActivityIndicator!

    override func viewDidLoad() {
        super.viewDidLoad()
        let tableView = UITableView(frame: view.frame)
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

        tableView.dataSource = self
        tableView.delegate = self
        tableView.tableFooterView = UIView()
        activityIndicator = LoadMoreActivityIndicator(scrollView: tableView, spacingFromLastCell: 10, spacingFromLastCellWhenLoadMoreActionStart: 60)
    }
}

extension ViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 30
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = "\(indexPath)"
        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        activityIndicator.start {
            DispatchQueue.global(qos: .utility).async {
                for i in 0..<3 {
                    print("!!!!!!!!! \(i)")
                    sleep(1)
                }
                DispatchQueue.main.async { [weak self] in
                    self?.activityIndicator.stop()
                }
            }
        }
    }
}

Wynik

wprowadź opis obrazu tutaj

Wasilij Bodnarczuk
źródło
Działa idealnie. Ale mam nagłówek w moim widoku tabeli, po przeciągnięciu, aby załadować więcej, nagłówek przejdzie pod pasek nawigacyjny .. UIEdgeInsetsMake in loadMoreActionFinshed powinien być ustawiony na (62, 0, 0, 0) biorąc pod uwagę 66 = navbar.height + 22
Desmond
Powinien działać w CollectionView podczas przewijania w pionie.
Wasilij Bodnarchuk
Niewiarygodne ... Super!
Tà Truhoada
jakakolwiek wersja z obiektywnym c tego?
Syed Ali Salman
@VasilyBodnarchuk nie ma problemu, zrobię to i podzielę się tutaj dla innych
Syed Ali Salman
18
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
    NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
    if ((indexPath.section == lastSectionIndex) && (indexPath.row == lastRowIndex)) {
        // This is the last cell
        [self loadMore];
    }
}

Jeśli korzystasz z danych podstawowych i NSFetchedResultsController, loadMoremoże to wyglądać następująco:

// Load more
- (void)loadMore {
    [self.fetchedResultsController.fetchRequest setFetchLimit:newFetchLimit];
    [NSFetchedResultsController deleteCacheWithName:@"cache name"];
    NSError *error;
    if (![self.fetchedResultsController performFetch:&error]) {
        // Update to handle the error appropriately.
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }

    [self.tableView reloadData];
}
samwize
źródło
Próbuję to zaimplementować, ale używam tablicy wyników, a nie sqlite, zastanawiałem się, jak dodać więcej do obecnego NSMutableArray, który mam, a następnie przeładować dane, ponieważ w przeciwnym razie dane zostaną nadpisane ... Próbowałem this [nazwy addObjectsFromArray: [responseObject valueForKeyPath: @ "name"]]; ale to nie działa ... tutaj jest link do mojego stackoverflow.com/questions/23446780/ ...
Lion789
1
Jaki jest sens ponownego pobierania danych za każdym razem, gdy otrzymasz nowe? Jeśli frc jest poprawnie skonfigurowany, wystarczy jedno pobranie, zaktualizuje się w razie potrzeby. Pobieranie go za każdym razem, zakładając, że żądanie pobierania frc jest skonfigurowane w kontekście głównego wątku, zablokuje główny wątek, gdy trafi na dysk, co całkowicie nie jest dobre dla doświadczenia użytkownika, gdy użytkownik chce nowych danych.
MANIAK_dobrii
Pierwsza połowa tego była dla mnie bardzo pomocna, dzięki. (Nie używam FetchedResultsVC)
weienw
@MANIAK_dobrii jest poprawne. Jedną z kluczowych funkcji NSFetchedResultsController jest to, że oblicza dane stronicowania, dzięki czemu można bezpłatnie uzyskać wirtualne przewijanie po podłączeniu go do UITableView. Zaimplementowanie takiej funkcji loadMore powinno być konieczne tylko wtedy, gdy faktycznie wypełniasz swój magazyn CoreData większą ilością danych, w takim przypadku nie ma potrzeby wykonywania kolejnego performFetch, jeśli NSFetchedResultsController jest poprawnie skonfigurowany.
Ali Gangji
Te same problemy, co w przypadku innych odpowiedzi. reloadData powoduje, że występuje to wiele razy.
dyson powraca
11

Wdrożyłem jedno rozwiązanie, które znalazłem w stackoverflow i działa dobrze, ale myślę, że rozwiązanie shinyuX jest bardzo łatwe do wdrożenia i działa dobrze dla mojej propozycji. Jeśli ktoś chce innego rozwiązania, może skorzystać z tego poniżej.

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{

   // UITableView only moves in one direction, y axis
    CGFloat currentOffset = scrollView.contentOffset.y;
    CGFloat maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    //NSInteger result = maximumOffset - currentOffset;

    // Change 10.0 to adjust the distance from bottom
    if (maximumOffset - currentOffset <= 10.0) {
        [self loadOneMorePage];
        //[self methodThatAddsDataAndReloadsTableView];
    }
}
Pedro Romão
źródło
Myślę, że są różne scenariusze prezentacji widoku, w moim przypadku twoje rozwiązanie zadziałało, potrzebowałem czegoś takiego
Raheel Sadiq
Jeśli użytkownik rzuca się mocno, tj. Na wysokość 1,5 ekranu, dno można osiągnąć bez uruchamiania odświeżania.
wraca dyson
ale przewija listę do góry
Mansuu ....
8

Detale

  • Swift 5.1, Xcode 11.3.1

Rozwiązanie

Genetyczne rozszerzenie UITableView dla Loadmore.

dodaj to rozszerzenie UITableView + do nowego pliku

extension UITableView{

    func indicatorView() -> UIActivityIndicatorView{
        var activityIndicatorView = UIActivityIndicatorView()
        if self.tableFooterView == nil{
            let indicatorFrame = CGRect(x: 0, y: 0, width: self.bounds.width, height: 40)
            activityIndicatorView = UIActivityIndicatorView(frame: indicatorFrame)
            activityIndicatorView.isHidden = false
            activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
            activityIndicatorView.isHidden = true
            self.tableFooterView = activityIndicatorView
            return activityIndicatorView
        }else{
            return activityIndicatorView
        }
    }

    func addLoading(_ indexPath:IndexPath, closure: @escaping (() -> Void)){
        indicatorView().startAnimating()
        if let lastVisibleIndexPath = self.indexPathsForVisibleRows?.last {
            if indexPath == lastVisibleIndexPath && indexPath.row == self.numberOfRows(inSection: 0) - 1 {
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    closure()
                }
            }
        }
        indicatorView().isHidden = false
    }

    func stopLoading(){
        indicatorView().stopAnimating()
        indicatorView().isHidden = true
    }
}

Teraz po prostu dodaj następujący wiersz kodu w metodzie UITableViewDelegate willDisplay Cell w Twoim ViewController i upewnij się, że tableView.delegate = self

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    // need to pass your indexpath then it showing your indicator at bottom 
    tableView.addLoading(indexPath) {
        // add your code here
        // append Your array and reload your tableview
        tableView.stopLoading() // stop your indicator
    }
}

Wynik

wprowadź opis obrazu tutaj

To wszystko… Mam nadzieję, że to pomocne. Dziękuję Ci

Yogesh Patel
źródło
Rzeczy do rozważenia. Po prostu dodaj „tableFooterView = nil” wewnątrz funkcji zatrzymywania ładowania, w przeciwnym razie obracający się wskaźnik nie przestanie się animować. Istnieje również właściwość w activityIndicator „hidesWhenStopped”, więc nie ma potrzeby ręcznego ustawiania ukrytego wskaźnika prawda / fałsz. Ale ogólnie wygląda świetnie :)
zramled
1
Dzięki za sugestię sprawdzę raz i edytuję tę odpowiedź :-)
Yogesh Patel
6

Użyj limitu i przesunięcia w swoich zapytaniach i wypełnij widok tabeli tą zawartością. Gdy użytkownik przewinie w dół, załaduj następny offset.

Zaimplementuj tableView:willDisplayCell:forRowAtIndexPath:metodę w swoim UITableViewDelegatei sprawdź, czy to ostatni wiersz

Retterdesdialogs
źródło
5

Poniższy link zawiera przykładowy kod. # Swift3

Użytkownik musi podciągnąć ostatnią komórkę widoku tabeli, co najmniej 2 komórki, aby pobrać więcej danych z serwera.

Znajdziesz komórkę procesową, która również pokazuje proces ładowania jak w ostatniej komórce.

Jest w Swift3

https://github.com/yogendrabagoriya/YBTableViewPullData

Jog
źródło
3

Jeszcze jedna opcja do użycia ( Swift 3 i iOS 10+):

class DocumentEventsTableViewController: UITableViewController, UITableViewDataSourcePrefetching {

     var currentPage: Int = 1
     let pageSize: Int = 10 // num of items in one page

     override func viewDidLoad() {
         super.viewDidLoad()

         self.tableView.prefetchDataSource = self
     }

     func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
         let upcomingRows = indexPaths.map { $0.row }

         if let maxIndex = upcomingRows.max() {

            let nextPage: Int = Int(ceil(Double(maxIndex) / Double(pageSize))) + 1

            if nextPage > currentPage {
                 // Your function, which attempts to load respective page from the local database
                 loadLocalData(page: nextPage)

                 // Your function, which makes a network request to fetch the respective page of data from the network
                 startLoadingDataFromNetwork(page: nextPage) 

                 currentPage = nextPage
             }
         }
     }
 }

W przypadku raczej małych stron (~ 10 pozycji) możesz chcieć ręcznie dodać dane dla stron 1 i 2, ponieważ nextPage może znajdować się gdzieś około 1-2, dopóki w tabeli nie będzie kilku elementów do dobrego przewinięcia. Ale będzie świetnie działać na wszystkich następnych stronach.

Vitalii
źródło
1
Działa to tylko w przypadku danych tylko do odczytu. Nie działa Jeśli masz takie funkcje, jak usunięcie jakiegoś wiersza i załadowanie więcej, ponieważ rozmiar strony został tutaj naprawiony i nie możesz załadować więcej, nawet jeśli jest więcej danych po zaktualizowaniu źródła.
EI Captain v2.0
2
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    if (news.count == 0) {
        return 0;
    } else {
        return news.count +  1 ;
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    @try {

        uint position = (uint) (indexPath.row);
        NSUInteger row = [indexPath row];
        NSUInteger count = [news count];

        //show Load More
        if (row == count) {
            UITableViewCell *cell = nil;

            static NSString *LoadMoreId = @"LoadMore";
            cell = [tableView dequeueReusableCellWithIdentifier:LoadMoreId];
            if (cell == nil) {
                cell = [[UITableViewCell alloc]
                        initWithStyle:UITableViewCellStyleDefault
                      reuseIdentifier:LoadMoreId];
            }
            if (!hasMoreLoad) {
                cell.hidden = true;
            } else {

                cell.textLabel.text = @"Load more items...";
                cell.textLabel.textColor = [UIColor blueColor];
                cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
                NSLog(@"Load more");
                if (!isMoreLoaded) {
                    isMoreLoaded = true;
                    [self performSelector:@selector(loadMoreNews) withObject:nil afterDelay:0.1];
                }
            }

            return cell;

        } else {
            NewsRow *cell = nil;

            NewsObject *newsObject = news[position];
            static NSString *CellIdentifier = @"NewsRow";
            cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

            if (cell == nil) {
                // Load the top-level objects from the custom cell XIB.
                NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:CellIdentifier owner:self options:nil];
                // Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
                cell = topLevelObjects[0];
                // Configure the cell...

            }

            cell.title.text = newsObject.title;             
            return cell;
        }

    }
    @catch (NSException *exception) {
        NSLog(@"Exception occurred: %@, %@", exception, [exception userInfo]);
    }
    return nil;
}

bardzo dobre wyjaśnienie tego postu.

http://useyourloaf.com/blog/2010/10/02/dynamically-loading-new-rows-into-a-table.html

proste, musisz dodać ostatni wiersz i ukryć go, a gdy wiersz tabeli trafi w ostatni wiersz, pokaż wiersz i załaduj więcej elementów.

vlad sol
źródło
1

powinieneś sprawdzić ios UITableViewDataSourcePrefetching.

class ViewController: UIViewController {
    @IBOutlet weak var mytableview: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        mytableview.prefetchDataSource = self
    }

 func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        print("prefetchdRowsAtIndexpath \(indexPaths)")
    }

    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
        print("cancelPrefetchingForRowsAtIndexpath \(indexPaths)")
    }


}
Bhavesh.iosDev
źródło
1

do ładowania z API, to działa dla mnie, Xcode 10 , swift 4.2 :

1- utwórz nowy plik Swift i zrób tak:

//
//  apiTVCController.swift
//  ApiTestingTableView
//
//  Created by Hooma7n on 4/7/19.
//  Copyright © 2019 Hooma7n. All rights reserved.
//

import Foundation
import Alamofire

class apiget {

    var tableData : [Datum] = []
    var loadin : [Datum] = []
    var testfortotal : Int?


    func getfromapi(completionHandler : ((_ isSucess : Bool) -> Void)?) {
        let url = "https://reqres.in/api/users?page=1"
        Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
            .responseJSON(completionHandler : { response in
                switch response.result {
                case .success(let data):
                    guard let jsonData = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted) else {return}
                    let decoder = JSONDecoder()
                    guard let result = try? decoder.decode(Welcome.self, from: jsonData) else {return}
                    self.tableData = result.data ?? []
                    self.testfortotal = result.total ?? 0
                    completionHandler?(true)

                //                    print(result)
                case .failure(let error):
                    print(error)
                }
            })
    }

    var pagecounter : Int = 2


    func loadmore(completionHandler : ((_ isSucess : Bool) -> Void)?){

        let url = "https://reqres.in/api/users?page=\(pagecounter)"
        Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
            .responseJSON(completionHandler : { response in
                switch response.result {
                case .success(let data):
                    guard let jsonData = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted) else {return}
                    let decoder = JSONDecoder()
                    guard let myresult = try? decoder.decode(Welcome.self, from: jsonData) else {return}
                    self.loadin = myresult.data ?? []
                    self.tableData.append(contentsOf: myresult.data ?? [])
                    completionHandler?(true)
                    print(self.pagecounter)
                    self.pagecounter += 1

                //                    print(myresult)
                case .failure(let error):
                    print(error)
                }
            })

    }

}

extension apiget {

    struct Welcome: Codable {
        let page, perPage, total, totalPages: Int?
        var data: [Datum]?

        enum CodingKeys: String, CodingKey {
            case page
            case perPage = "per_page"
            case total
            case totalPages = "total_pages"
            case data
        }
    }

    struct Datum: Codable {
        let id: Int?
        let firstName, lastName: String?
        let avatar: String?

        enum CodingKeys: String, CodingKey {
            case id
            case firstName = "first_name"
            case lastName = "last_name"
            case avatar
        }
    }


}

2- w pliku ViewController (kontroler tableView):

//
//  apiTVC.swift
//  ApiTestingTableView
//
//  Created by Hooma7n on 4/7/19.
//  Copyright © 2019 Hooma7n. All rights reserved.
//

import UIKit
import Alamofire

class apiTVC: UITableViewController {

    var datamodel = apiget()

    override func viewDidLoad() {
        super.viewDidLoad()

        datamodel.getfromapi(completionHandler: {finish in
            if finish {self.tableView.reloadData()
            }

        })

    }


    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return datamodel.tableData.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! apiTableViewCell
        cell.firstNameLabel.text = datamodel.tableData[indexPath.row].firstName
        cell.lastNameLabel.text = datamodel.tableData[indexPath.row].lastName
        cell.dateLabel.text = "\(datamodel.tableData[indexPath.row].id ?? 0)"
        cell.profileImageView.loadImage(fromURL: datamodel.tableData[indexPath.row].avatar ?? "")

        return cell

    }

    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        let lastElement = datamodel.tableData.count - 1
        let total = datamodel.testfortotal ?? 12
        if indexPath.row == lastElement && datamodel.tableData.count < total{

            datamodel.loadmore(completionHandler: {finish in
                if finish {

                    self.tableView.reloadData()

                }})
        }
    }
}

jeśli używasz tableView w Twoim viewController ustaw delegata , własne źródło danych w viewDidLoad.

hooma7n
źródło
0

Chcę tylko podzielić się tym podejściem:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    NSLog(@"%@", [[YourTableView indexPathsForVisibleRows] lastObject]);
    [self estimatedTotalData];
}

- (void)estimatedTotalData
{
    long currentRow = ((NSIndexPath *)[[YourTableView indexPathsForVisibleRows] lastObject]).row;

    long estimateDataCount = 25;

    while (currentRow > estimateDataCount)
    {
        estimateDataCount+=25;
    }

    dataLimit = estimateDataCount;

    if (dataLimit == currentRow+1)
    {
        dataLimit+=25;
    }

    NSLog(@"dataLimit :%ld", dataLimit);

    [self requestForData];

    // this answers the question..
    //
    if(YourDataSource.count-1 == currentRow)
    {
        NSLog(@"LAST ROW"); //loadMore data
    }
}

NSLog(...); wynik będzie wyglądał następująco:

<NSIndexPath: 0xc0000000002e0016> {length = 2, path = 0 - 92}
dataLimit :100
<NSIndexPath: 0xc000000000298016> {length = 2, path = 0 - 83}
dataLimit :100
<NSIndexPath: 0xc000000000278016> {length = 2, path = 0 - 79}
dataLimit :100
<NSIndexPath: 0xc000000000238016> {length = 2, path = 0 - 71}
dataLimit :75
<NSIndexPath: 0xc0000000001d8016> {length = 2, path = 0 - 59}
dataLimit :75
<NSIndexPath: 0xc0000000001c0016> {length = 2, path = 0 - 56}
dataLimit :75
<NSIndexPath: 0xc000000000138016> {length = 2, path = 0 - 39}
dataLimit :50
<NSIndexPath: 0xc000000000120016> {length = 2, path = 0 - 36}
dataLimit :50
<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}
dataLimit :25
<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}
dataLimit :25

Jest to dobre do wyświetlania danych przechowywanych lokalnie. Początkowo deklaruję dataLimit na 25, co oznacza, że ​​uitableview będzie miało 0-24 (początkowo).

Jeśli użytkownik przewinął w dół i ostatnia komórka jest widoczna, dataLimitzostanie dodana 25 ...

Uwaga: to bardziej przypomina stronicowanie danych UITableView :)

0yeoj
źródło
0
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {

NSInteger sectionsAmount = [tableView numberOfSections];
NSInteger rowsAmount = [tableView numberOfRowsInSection:[indexPath section]];
if ([indexPath section] == sectionsAmount - 1 && [indexPath row] == rowsAmount - 1) {
    //get last row
    if (!isSearchActive && !isFilterSearchActive) {
        if (totalRecords % 8 == 0) {
            int64_t delayInSeconds = 2.0;
            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
            dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {


            [yourTableView beginUpdates];
            [yourTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
            [yourTableView endUpdates];
            });
        }
    }
}
}
Sahila Mirajkar
źródło
po wyświetleniu ostatniego wiersza wstaw wiersze, tj. beginUpdates..i użyj pewnego opóźnienia, aby uniknąć awarii.
Sahila Mirajkar
0

Najlepszym sposobem rozwiązania tego problemu jest dodanie komórki na dole tabeli, a ta komórka będzie zawierać wskaźnik.

Szybko musisz dodać to:

  1. Utwórz nową komórkę typu cellLoading spowoduje zatrzymanie wskaźnika. Spójrz na poniższy kod
  2. Spójrz na liczbę wierszy i dodaj do niej 1 (to jest do ładowania komórki).
  3. musisz sprawdzić w rawAtIndex, jeśli idexPath.row == yourArray.count, a następnie zwrócić komórkę Loading.

spójrz na kod poniżej:

import UIKit

class LoadingCell: UITableViewCell {

@IBOutlet weak var indicator: UIActivityIndicatorView!


}

W przypadku widoku tabeli: numOfRows:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return  yourArray.count + 1
}

cellForRawAt indexPath:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    if indexPath.row == users.count  {
        // need to change
        let loading = Bundle.main.loadNibNamed("LoadingCell", owner: LoadingCell.self , options: nil)?.first as! LoadingCell
        return loading

    }

    let yourCell = tableView.dequeueReusableCell(withIdentifier: "cellCustomizing", for: indexPath) as! UITableViewCell

    return yourCell

}

Jeśli zauważysz, że moja komórka ładująca jest tworzona z pliku nib. Te filmy wyjaśniają, co zrobiłem.

Mohammed Ali Khaled
źródło
0
let threshold = 100.0 // threshold from bottom of tableView
var isLoadingMore = false // flag


func scrollViewDidScroll(scrollView: UIScrollView) {
    let contentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    if !isLoadingMore && (maximumOffset - contentOffset <= threshold) {
        // Get more data - API call
        self.isLoadingMore = true

        // Update UI
        dispatch_async(dispatch_get_main_queue()) {
            tableView.reloadData()
            self.isLoadingMore = false
        }
    }
  }
Alok SInha
źródło
0

W przypadku Xcode 10.1, Swift 4.2

Ten film wydaje się świetnym samouczkiem!

Projekt początkowy / kompletny: https://github.com/RobCanton/Swift-Infinite-Scrolling-Example

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    var tableView:UITableView!

    var fetchingMore = false
    var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        initTableView()
    }

    func initTableView() {
        tableView = UITableView(frame: view.bounds, style: .plain)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
        tableView.delegate = self
        tableView.dataSource = self

        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false

        let layoutGuide = view.safeAreaLayoutGuide
        tableView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = true
        tableView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true

        tableView.reloadData()
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath)
            cell.textLabel?.text = "Item \(items[indexPath.row])"
            return cell
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height

        if offsetY > contentHeight - scrollView.frame.height * 4 {
            if !fetchingMore {
                beginBatchFetch()
            }
        }
    }

    func beginBatchFetch() {
        fetchingMore = true
        print("Call API here..")
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.50, execute: {
            print("Consider this as API response.")
            let newItems = (self.items.count...self.items.count + 12).map { index in index }
            self.items.append(contentsOf: newItems)
            self.fetchingMore = false
            self.tableView.reloadData()
        })
    }
}
Shuvo Joseph
źródło