UITableView lädt mehr, wenn Sie wie bei einer Facebook-Anwendung nach unten scrollen


94

Ich entwickle eine Anwendung, die SQLite verwendet. Ich möchte eine Liste von Benutzern (UITableView) mit einem Paginierungsmechanismus anzeigen. Könnte mir bitte jemand sagen, wie ich mehr Daten in meine Liste laden soll, wenn der Benutzer zum Ende der Liste blättert (wie auf der Homepage der Facebook-Anwendung)?

Antworten:


102

Sie können dies tun, indem Sie überprüfen, wo Sie sich in der cellForRowAtIndexPath:Methode befinden. Diese Methode ist leicht zu verstehen und zu implementieren:

- (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];
    }
}

BEARBEITEN: Das letzte Element wurde überprüft, um Rekursionsaufrufe zu vermeiden. Sie müssen die Methode implementieren, die definiert, ob das letzte Element erreicht wurde oder nicht.

EDIT2: lastItemReached erklärt


9
Was ist, wenn der Benutzer nach oben und unten scrollt, sodass cellForRowAtIndexPath VIELE MAL heißt?
onmyway133

Wenn er zum ersten Mal nach unten scrollt, wird seine Liste neu geladen. Und jedes Mal, wenn er den Boden erreicht, wird ein neuer Datenblock gesammelt. Wenn eine bestimmte Behandlung angewendet werden muss, liegt es in launchReloadder Verantwortung der Methode, diese zu handhaben (zum Beispiel jeweils nur eine asynchrone Nachladeaktion)
shinyuX

4
Ich musste eine Flagge hinzufügen, um ein Rekursionsproblem zu verhindern, als der letzte Gegenstand getroffen wurde:if !lastItemReached && indexPath.row == dataArray!.hits.count - 1 {
Albert Bori

Was ist die self.launchReloadMethode?
Schieberegler

1
@shinyuX funktioniert bei mir nicht, "wenn" immer falsch ... aber wenn (lastItemReached && indexPath.row == [self.dataArray count] - 1) true, WARUM?
Sagte am

68

Schnell

Methode 1: Nach unten gescrollt

Hier ist die Swift-Version von Pedro Romãos Antwort . Wenn der Benutzer aufhört zu scrollen, prüft er, ob er den unteren Rand erreicht hat.

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()
    }
}

Methode 2: Letzte Zeile erreicht

Und hier ist die Swift-Version der Antwort von shinyuX . Es wird geprüft, ob der Benutzer die letzte Zeile erreicht hat.

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()
    }

}

Beispiel einer loadMore()Methode

Ich habe diese drei Klassenvariablen zum Abrufen von Datenstapeln eingerichtet.

// 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

Dies ist die Funktion zum Laden weiterer Elemente aus der Datenbank in die Tabellenansicht.

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
            }

        }
    }
}

Ich habe Methode 1 verwendet, weil ich mehr ziehen wollte, um mehr zu holen. Es funktioniert großartig. Vielen Dank an euch beide!
Bob Wakefield

37

Verwenden Sie besser die willDisplayCellMethode, um zu überprüfen, welche Zelle geladen wird. Sobald wir den Strom indexPath.rowals letzten erhalten haben, können wir mehr Zellen laden. Dadurch werden beim Scrollen nach unten mehr Zellen geladen.

 - (void)tableView:(UITableView *)tableView 
       willDisplayCell:(UITableViewCell *)cell    
       forRowAtIndexPath:(NSIndexPath *)indexPath
{
    // check if indexPath.row is last row
    // Perform operation to load new Cell's.
}

16
es ist nicht besser, da reloadData diese Methode wieder aufruft, oder?
Marcin

24

Einzelheiten

  • Swift 5.1, Xcode 11.2.1

Lösung

Arbeitete mit 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()
    }
}

Verwendung

drin

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

Handhabung

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()
                }
            }
        }
    }
}

Vollständige Probe

Vergessen Sie nicht, den Lösungscode einzufügen.

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()
                }
            }
        }
    }
}

Ergebnis

Geben Sie hier die Bildbeschreibung ein


Funktioniert perfekt. Aber ich habe einen Header in meiner Tabellenansicht. Nach dem Ziehen, um mehr zu laden, wird der Header unter die Navigationsleiste verschoben. UIEdgeInsetsMake in loadMoreActionFinshed sollte unter Berücksichtigung von 66 = navbar.height + 22
Desmond

Es sollte in CollectionView funktionieren, wenn Sie vertikal scrollen.
Vasily Bodnarchuk

Unglaublich ... Cool!
Tà Truhoada

Gibt es eine objektive Version davon?
Syed Ali Salman

@ VasilyBodnarchuk kein Problem, ich werde es tun und hier für andere teilen
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];
    }
}

Wenn Sie Core Data und verwenden NSFetchedResultsController, loadMorekönnte dies wie folgt aussehen:

// 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];
}

Ich versuche dies zu implementieren, aber ich verwende eine Reihe von Ergebnissen und nicht SQLite. Ich habe mich gefragt, wie ich mehr in das aktuelle NSMutableArray einfügen und dann die Daten neu laden soll, da die Daten sonst überschrieben werden ... Ich habe es versucht this [names addObjectsFromArray: [responseObject valueForKeyPath: @ "name"]]; aber es funktioniert nicht ... hier ist ein Link zu meiner Frage stackoverflow.com/questions/23446780/…
Lion789

1
Was bringt es, Daten jedes Mal neu abzurufen, wenn Sie neue Daten erhalten? Wenn frc richtig konfiguriert ist, reicht ein einzelner Abruf aus. Es wird bei Bedarf entsprechend aktualisiert. Wenn Sie es jedes Mal abrufen, vorausgesetzt, die Abrufanforderung von frc ist für einen Haupt-Thread-Kontext konfiguriert, wird der Haupt-Thread blockiert, wenn er auf die Festplatte trifft. Dies ist für die Benutzererfahrung absolut nicht gut, wenn der Benutzer neue Daten wünscht.
MANIAK_dobrii

Die erste Hälfte davon war sehr hilfreich für mich, danke. (Nicht mit FetchedResultsVC)
weienw

@MANIAK_dobrii ist korrekt. Eine der Hauptfunktionen des NSFetchedResultsController besteht darin, dass er die Auslagerungsdaten berechnet, sodass Sie beim Verbinden mit einer UITableView kostenlos virtuell scrollen können. Die Implementierung einer solchen loadMore-Funktion sollte nur erforderlich sein, wenn Sie Ihren CoreData-Speicher tatsächlich mit mehr Daten füllen. In diesem Fall ist kein weiterer performFetch erforderlich, wenn Ihr NSFetchedResultsController korrekt konfiguriert ist.
Ali Gangji

Gleiche Probleme wie bei anderen Antworten. reloadData bewirkt, dass dies mehrmals auftritt.
Dyson kehrt

11

Ich habe eine Lösung implementiert, die ich in stackoverflow gefunden habe, und sie funktioniert gut, aber ich denke, die Lösung von shinyuX ist sehr einfach zu implementieren und funktioniert gut für meinen Vorschlag. Wenn jemand eine andere Lösung wünscht, kann er diese unten verwenden.

- (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];
    }
}

Ich denke, es gibt verschiedene Szenarien für die Ansichtspräsentation. In meinem Fall hat Ihre Lösung funktioniert. Ich brauchte so etwas
Raheel Sadiq,

Wenn der Benutzer hart schleudert, dh 1,5 Bildschirme hoch, kann der Boden erreicht werden, ohne dass eine Aktualisierung ausgelöst wird.
Dyson kehrt

aber es
scrollt

7

Einzelheiten

  • Swift 5.1, Xcode 11.3.1

Lösung

Genetische UITableView-Erweiterung für Loadmore.

Fügen Sie diese UITableView + -Erweiterung in Ihre neue Datei ein

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
    }
}

Fügen Sie nun einfach die folgende Codezeile in die UITableViewDelegate-Methode hinzu, um Cell in Ihrem ViewController anzuzeigen, und stellen Sie sicher, dass tableView.delegate = self ist

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
    }
}

Ergebnis

Geben Sie hier die Bildbeschreibung ein

Das war's .. Hoffe das hilfreich. Danke


Dinge, die man beachten muss. Fügen Sie einfach 'tableFooterView = nil' in die Stoploading-Funktion ein, sonst hört die sich drehende Anzeige nicht auf zu animieren. Außerdem befindet sich im activityIndicator 'hidesWhenStopped' eine Eigenschaft, sodass Sie den Indikator nicht manuell auf true / false setzen müssen. Aber insgesamt sieht es toll aus :)
zramled

1
Vielen Dank für den Vorschlag, den ich einmal überprüfen und diese Antwort bearbeiten werde :-)
Yogesh Patel

6

Verwenden Sie Limit und Offset in Ihren Abfragen und füllen Sie Ihre Tabellenansicht mit diesem Inhalt. Wenn der Benutzer einen Bildlauf nach unten durchführt, laden Sie den nächsten Versatz.

Implementieren Sie die tableView:willDisplayCell:forRowAtIndexPath:Methode in Ihrem UITableViewDelegateund prüfen Sie, ob es sich um die letzte Zeile handelt


5

Der folgende Link enthält einen Beispielcode. # Swift3

Der Benutzer muss die letzte Tabellenansichtszelle aufrufen, mindestens die Höhe von 2 Zellen, um mehr Daten vom Server abzurufen.

In der Prozesszelle wird auch der Ladevorgang wie in der letzten Zelle angezeigt.

Es ist in Swift3

https://github.com/yogendrabagoriya/YBTableViewPullData


3

Eine weitere Option ( Swift 3 und 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
             }
         }
     }
 }

Bei eher kleinen Seiten (~ 10 Elemente) möchten Sie möglicherweise manuell Daten für die Seiten 1 und 2 hinzufügen, da nextPage möglicherweise zwischen 1 und 2 liegt, bis die Tabelle einige Elemente enthält, die gut gescrollt werden können. Aber es wird für alle nächsten Seiten großartig funktionieren.


1
Dies funktioniert nur für schreibgeschützte Daten. Funktioniert nicht Wenn Sie Funktionen wie das Löschen einer Zeile und das Laden von mehr haben, da pageSize hier behoben wurde und nicht mehr geladen werden kann, selbst wenn nach dem Aktualisieren Ihrer Quelle mehr Daten vorhanden sind.
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;
}

sehr gute Erklärung zu diesem Beitrag.

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

Einfach müssen Sie die letzte Zeile hinzufügen und ausblenden. Wenn die Tabellenzeile die letzte Zeile erreicht, müssen Sie die Zeile anzeigen und mehr Elemente laden.


1

Sie sollten ios UITableViewDataSourcePrefetching überprüfen.

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)")
    }


}

1

Zum Laden von einer API funktioniert es für mich, Xcode 10 , Swift 4.2 :

1- Erstellen Sie eine neue Swift-Datei und gehen Sie folgendermaßen vor:

//
//  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- in Ihrer ViewController-Datei (tableView Controller):

//
//  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()

                }})
        }
    }
}

Wenn Sie tableView in Ihrem ViewController-Set- Delegaten verwenden , geben Sie die Datenquelle selbst in viewDidLoad ein.


0

Ich möchte nur diesen Ansatz teilen:

- (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(...); Ausgabe wäre so etwas wie:

<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

Dies ist gut für die Anzeige lokal gespeicherter Daten. Anfangs deklariere ich das dataLimit auf 25, was bedeutet, dass uitableview (anfangs) 0-24 hat.

Wenn der Benutzer nach unten gescrollt hat und die letzte Zelle sichtbar ist, dataLimitwird mit 25 ...

Hinweis: Dies ähnelt eher einem UITableView-Daten-Paging :)


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];
            });
        }
    }
}
}

Fügen Sie nach dem Anzeigen der letzten Zeile Zeilen ein, z. B. beginUpdates, und verwenden Sie eine Verzögerung, um keinen Absturz zu erzielen.
Sahila Mirajkar

0

Der beste Weg, um dieses Problem zu lösen, besteht darin, eine Zelle am unteren Rand Ihrer Tabelle hinzuzufügen. Diese Zelle enthält die Anzeige.

In Kürze müssen Sie Folgendes hinzufügen:

  1. Neue Zelle vom Typ cellLoading erstellen Diese Anzeige enthält die Anzeige. Schauen Sie sich den Code unten an
  2. Sehen Sie sich die Anzahl der Zeilen an und fügen Sie 1 hinzu (dies dient zum Laden der Zelle).
  3. Sie müssen im rawAtIndex einchecken, ob idexPath.row == yourArray.count, und dann die Ladezelle zurückgeben.

Schauen Sie sich den folgenden Code an:

import UIKit

class LoadingCell: UITableViewCell {

@IBOutlet weak var indicator: UIActivityIndicatorView!


}

Für die Tabellenansicht: 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

}

Wenn Sie feststellen, dass meine Ladezelle aus einer NIB-Datei erstellt wurde. Diese Videos erklären, was ich getan habe.


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
        }
    }
  }

0

Für Xcode 10.1 Swift 4.2

Dieses Video scheint ein großartiges Tutorial zu sein!

Starter / Komplettes Projekt: 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()
        })
    }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.