UITableView โหลดได้มากขึ้นเมื่อเลื่อนไปด้านล่างเช่นแอปพลิเคชัน Facebook


97

ฉันกำลังพัฒนาแอปพลิเคชันที่ใช้ SQLite ฉันต้องการแสดงรายชื่อผู้ใช้ (UITableView) โดยใช้กลไกการแบ่งหน้า ใครช่วยบอกวิธีโหลดข้อมูลเพิ่มเติมในรายการของฉันเมื่อผู้ใช้เลื่อนไปที่ท้ายรายการ (เช่นในโฮมเพจบนแอปพลิเคชัน Facebook)

คำตอบ:


103

คุณสามารถทำได้โดยเพิ่มการตรวจสอบว่าคุณอยู่ที่ไหนในcellForRowAtIndexPath:วิธีการ วิธีนี้เข้าใจง่ายและนำไปใช้ได้:

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

แก้ไข: เพิ่มการตรวจสอบรายการสุดท้ายเพื่อป้องกันการโทรซ้ำ คุณจะต้องใช้วิธีการกำหนดว่ารายการสุดท้ายมาถึงหรือไม่

EDIT2: อธิบาย lastItemReached


9
จะเกิดอะไรขึ้นถ้าผู้ใช้เลื่อนขึ้นและลงดังนั้น cellForRowAtIndexPath จึงเรียกว่า MANY TIMES!
onmyway133

ครั้งแรกที่เขาเลื่อนไปที่ด้านล่างรายการของเขาจะถูกโหลดซ้ำ และทุกครั้งที่เขาไปถึงจุดต่ำสุดจะมีการรวบรวมข้อมูลใหม่ หากต้องใช้วิธีการรักษาที่เฉพาะเจาะจงจะเป็นlaunchReloadความรับผิดชอบของวิธีการในการจัดการ (ตัวอย่างเช่นการดำเนินการรีโหลดแบบอะซิงโครนัสเพียงครั้งเดียวเท่านั้น)
shinyuX

4
ฉันต้องเพิ่มค่าสถานะเพื่อป้องกันปัญหาการเรียกซ้ำเมื่อรายการล่าสุดถูกตี:if !lastItemReached && indexPath.row == dataArray!.hits.count - 1 {
Albert Bori

อะไรคือสิ่งที่self.launchReloadวิธี?
ไลเดอร์

1
@shinyuX ไม่ได้ผลสำหรับฉัน "if" เป็นเท็จเสมอ ... แต่ถ้า (lastItemReached && indexPath.row == [self.dataArray count] - 1) จริงทำไม?
กล่าวเมื่อ

69

รวดเร็ว

วิธีที่ 1: เลื่อนไปด้านล่าง

นี่คือรุ่นสวิฟท์ของคำตอบโดรRomãoของ เมื่อผู้ใช้หยุดเลื่อนจะตรวจสอบว่าถึงด้านล่างหรือไม่

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

วิธีที่ 2: ถึงแถวสุดท้าย

และนี่ก็เป็นรุ่นสวิฟท์ของคำตอบของ shinyuX ตรวจสอบว่าผู้ใช้มาถึงแถวสุดท้ายหรือไม่

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

}

ตัวอย่างloadMore()วิธีการ

ฉันตั้งค่าตัวแปรคลาสทั้งสามนี้เพื่อดึงข้อมูลเป็นกลุ่ม

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

นี่คือฟังก์ชันในการโหลดรายการเพิ่มเติมจากฐานข้อมูลลงในมุมมองตาราง

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
            }

        }
    }
}

ฉันใช้วิธีที่ 1 เพราะฉันต้องการดึงเพื่อดึงข้อมูลเพิ่มเติม มันใช้งานได้ดี ขอบคุณทั้งคู่!
Bob Wakefield

37

ควรใช้willDisplayCellวิธีตรวจสอบว่าเซลล์ใดจะถูกโหลด เมื่อเราได้รับกระแสindexPath.rowเป็นครั้งสุดท้ายเราสามารถโหลดเซลล์เพิ่มเติมได้ การดำเนินการนี้จะโหลดเซลล์เพิ่มเติมเมื่อเลื่อนลง

 - (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
มันไม่ดีไปกว่านี้แล้วเพราะ reloadData จะเรียก method นี้อีกครั้งใช่ไหม
Marcin

จะใช้งานได้หรือไม่ถ้าเรามีส่วนด้วย
อับดุลยาซีน

ใช่สิ่งนี้จะใช้ได้กับส่วนต่างๆ indexPath จะให้แถวและส่วนทั้งคู่
Suraj Mirajkar

25

รายละเอียด

  • Swift 5.1, Xcode 11.2.1

วิธีการแก้

ทำงานร่วมกับ 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)
        if #available(iOS 13.0, *)
        {
            activityIndicatorView.color = .label
        }
        else
        {
            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()
    }
}

การใช้งาน

ในนั้น

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

การจัดการ

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

ตัวอย่างเต็ม

อย่าลืมวางรหัสโซลูชัน

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

ผลลัพธ์

ป้อนคำอธิบายภาพที่นี่


ทำงานได้อย่างสมบูรณ์แบบ แต่ฉันมีส่วนหัวใน tableview ของฉันหลังจากลากเพื่อโหลดมากขึ้นส่วนหัวจะอยู่ใต้แถบนำทาง .. UIEdgeInsetsMake ใน loadMoreActionFinshed ควรตั้งค่าเป็น (62, 0, 0, 0) โดยพิจารณาจาก 66 = navbar.height + 22
Desmond

ควรทำงานใน CollectionView เมื่อคุณเลื่อนในแนวตั้ง
Vasily Bodnarchuk

ไม่น่าเชื่อ ... เจ๋ง!
Tà Truhoada

วัตถุประสงค์ -c รุ่นนี้หรือไม่
Syed Ali Salman

@VasilyBodnarchuk ไม่มีปัญหาฉันจะทำและแบ่งปันที่นี่เพื่อคนอื่น ๆ
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];
    }
}

หากคุณกำลังใช้ข้อมูลหลักและNSFetchedResultsControllerจากนั้นloadMoreจะมีลักษณะเหมือนดังต่อไปนี้:

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

ฉันกำลังพยายามใช้สิ่งนี้ แต่ฉันใช้อาร์เรย์ของผลลัพธ์ไม่ใช่ sqlite ฉันสงสัยว่าฉันจะเพิ่มมากขึ้นใน NSMutableArray ปัจจุบันที่ฉันมีแล้วโหลดข้อมูลซ้ำได้อย่างไรเพราะมิฉะนั้นข้อมูลจะถูกเขียนทับ ... ฉันพยายาม [ชื่อ addObjectsFromArray: [responseObject valueForKeyPath: @ "name"]]; แต่มันใช้ไม่ได้ ... นี่คือลิงค์ไปยังคำถามของฉันstackoverflow.com/questions/23446780/…
Lion789

1
อะไรคือจุดที่ต้องดึงข้อมูลอีกครั้งทุกครั้งที่คุณได้รับข้อมูลใหม่ หากกำหนดค่า frc อย่างถูกต้องการดึงข้อมูลเพียงครั้งเดียวก็เพียงพอแล้วระบบจะอัปเดตตามความจำเป็น การดึงข้อมูลทุกครั้งโดยสมมติว่าคำขอดึงข้อมูลของ frc ถูกกำหนดค่าเป็นบริบทเธรดหลักจะบล็อกเธรดหลักเมื่อกระทบดิสก์ซึ่งไม่ดีต่อประสบการณ์ของผู้ใช้โดยสิ้นเชิงเมื่อผู้ใช้ต้องการข้อมูลใหม่
MANIAK_dobrii

ครึ่งแรกนี้มีประโยชน์มากสำหรับฉันขอบคุณ (ไม่ใช้ FetchedResultsVC)
weienw

@MANIAK_dobrii ถูกต้อง คุณสมบัติหลักอย่างหนึ่งของ NSFetchedResultsController คือการคำนวณข้อมูลการเพจเพื่อให้คุณสามารถเลื่อนเสมือนได้ฟรีเมื่อคุณเชื่อมต่อกับ UITableView การใช้ฟังก์ชัน loadMore ดังกล่าวควรมีความจำเป็นก็ต่อเมื่อคุณกำลังเติมที่เก็บ CoreData ของคุณด้วยข้อมูลเพิ่มเติมซึ่งในกรณีนี้ไม่จำเป็นต้องทำ performFetch อื่นหาก NSFetchedResultsController ของคุณได้รับการกำหนดค่าอย่างถูกต้อง
Ali Gangji

ปัญหาเดียวกับคำตอบอื่น ๆ reloadData ทำให้สิ่งนี้เกิดขึ้นหลายครั้ง
dyson กลับมา

11

ฉันได้ใช้โซลูชันหนึ่งที่พบใน stackoverflow และใช้งานได้ดี แต่ฉันคิดว่าโซลูชันของ shinyuX ใช้งานง่ายมากและทำงานได้ดีสำหรับข้อเสนอของฉัน หากมีใครต้องการโซลูชันอื่นสามารถใช้วิธีนี้ได้ด้านล่าง

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

ฉันคิดว่ามีสถานการณ์ที่แตกต่างกันสำหรับการนำเสนอมุมมองในกรณีของฉันโซลูชันของคุณใช้งานได้ฉันต้องการสิ่งนี้
Raheel Sadiq

หากผู้ใช้เหวี่ยงแรง ๆ เช่นสูง 1.5 หน้าจอสามารถเข้าถึงด้านล่างได้โดยไม่ต้องรีเฟรช
dyson กลับ

แต่มันเลื่อนรายการไปด้านบน
มันซู ....

8

รายละเอียด

  • Swift 5.1, Xcode 11.3.1

วิธีการแก้

Genetic UITableView Extension For Load เพิ่มเติม

เพิ่ม UITableView + Extension นี้ในไฟล์ใหม่ของคุณ

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

ตอนนี้เพียงเพิ่มบรรทัดของโค้ดต่อไปนี้ใน UITableViewDelegate Method willDisplay Cell ใน ViewController ของคุณและตรวจสอบให้แน่ใจว่า 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
    }
}

ผลลัพธ์

ป้อนคำอธิบายภาพที่นี่

นั่นแหล่ะ .. หวังว่านี่จะเป็นประโยชน์ ขอขอบคุณ


สิ่งที่ต้องพิจารณา เพียงเพิ่ม 'tableFooterView = nil' เข้าไปใน func การหยุดโหลดมิฉะนั้นตัวบ่งชี้ที่หมุนจะไม่หยุดเคลื่อนไหว นอกจากนี้ยังมีคุณสมบัติใน activityIndicator 'hidesWhenStopped' ดังนั้นคุณไม่จำเป็นต้องตั้งค่าตัวบ่งชี้ที่ซ่อนจริง / เท็จด้วยตนเอง แต่โดยรวมแล้วดูดีมาก :)
zramled

1
ขอบคุณสำหรับคำแนะนำฉันจะตรวจสอบหนึ่งครั้งและแก้ไขคำตอบนี้ :-)
Yogesh Patel

6

ใช้ขีด จำกัด และชดเชยในแบบสอบถามของคุณและเติมเต็ม tableview ของคุณด้วยเนื้อหานั้น เมื่อผู้ใช้เลื่อนลงให้โหลดออฟเซ็ตถัดไป

ใช้tableView:willDisplayCell:forRowAtIndexPath:วิธีการในของคุณUITableViewDelegateและตรวจสอบเพื่อดูว่าเป็นแถวสุดท้ายหรือไม่


5

ลิงค์ด้านล่างจะให้โค้ดตัวอย่าง # Swift3

ผู้ใช้ต้องดึงเซลล์มุมมองตารางสุดท้ายขึ้นมาอย่างน้อยความสูง 2 เซลล์เพื่อดึงข้อมูลเพิ่มเติมจากเซิร์ฟเวอร์

คุณจะพบเซลล์กระบวนการเพื่อแสดงกระบวนการโหลดเช่นเดียวกับในเซลล์สุดท้าย

ใน Swift3

https://github.com/yogendrabagoriya/YBTableViewPullData


3

อีกหนึ่งทางเลือกในการใช้งาน ( Swift 3และ 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
             }
         }
     }
 }

สำหรับหน้าที่ค่อนข้างเล็ก (~ 10 รายการ) คุณอาจต้องการเพิ่มข้อมูลสำหรับหน้า 1 และ 2 ด้วยตนเองเนื่องจาก nextPage อาจอยู่ที่ประมาณ 1-2 จนกว่าตารางจะมีรายการบางส่วนให้เลื่อนได้ดี แต่จะใช้งานได้ดีสำหรับหน้าถัดไปทั้งหมด


1
สิ่งนี้ใช้ได้กับข้อมูลแบบอ่านอย่างเดียวเท่านั้น ไม่ทำงานหากคุณมีฟังก์ชันเช่นลบบางแถวและโหลดเพิ่มเติมเนื่องจาก pageSize ได้รับการแก้ไขที่นี่และไม่สามารถโหลดเพิ่มเติมได้แม้ว่าจะมีข้อมูลเพิ่มเติมหลังจากที่คุณอัปเดตแหล่งที่มา
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;
}

คำอธิบายที่ดีมากในโพสต์นี้

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

ง่ายๆคุณต้องเพิ่มแถวสุดท้ายและซ่อนและเมื่อแถวของตารางชนแถวสุดท้ายกว่าแสดงแถวและโหลดรายการเพิ่มเติม


1

คุณควรตรวจสอบ 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)")
    }


}

1

สำหรับการโหลดจาก API มันใช้งานได้สำหรับฉัน Xcode 10รวดเร็ว4.2 :

1- สร้างไฟล์ New Swift และทำสิ่งนี้:

//
//  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- ในไฟล์ ViewController ของคุณ (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()

                }})
        }
    }
}

ถ้าใช้ tableView ใน viewController ของคุณชุดผู้ร่วมประชุม , แหล่งข้อมูลด้วยตนเองใน viewDidLoad


0

แค่อยากแบ่งปันแนวทางนี้:

- (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(...); ผลลัพธ์จะเป็นดังนี้:

<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

เหมาะสำหรับการแสดงข้อมูลที่จัดเก็บในเครื่อง ตอนแรกฉันประกาศ dataLimit ถึง 25 นั่นหมายความว่า uitableview จะมี 0-24 (เริ่มแรก)

หากผู้ใช้เลื่อนไปด้านล่างและมองเห็นเซลล์สุดท้ายdataLimitจะถูกเพิ่มด้วย 25 ...

หมายเหตุ: นี่เป็นเหมือนการเพจข้อมูล UITableView :)


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

หลังจากแสดงแถวสุดท้ายแล้วให้แทรกแถวเช่น beginUpdates .. และใช้การหน่วงเวลาเพื่อไม่ให้เกิดความผิดพลาด
Sahila Mirajkar

0

วิธีที่ดีที่สุดในการแก้ปัญหานี้คือเพิ่มเซลล์ที่ด้านล่างของตารางและเซลล์นี้จะมีตัวบ่งชี้

อย่างรวดเร็วคุณต้องเพิ่มสิ่งนี้:

  1. สร้างเซลล์ใหม่ประเภทเซลล์การโหลดสิ่งนี้จะถือตัวบ่งชี้ ดูโค้ดด้านล่าง
  2. ดูจำนวนแถวและเพิ่ม 1 เข้าไป (ใช้สำหรับโหลดเซลล์)
  3. คุณต้องตรวจสอบใน rawAtIndex ถ้า idexPath.row == yourArray.count จากนั้นส่งคืน Loading cell

ดูโค้ดด้านล่าง:

import UIKit

class LoadingCell: UITableViewCell {

@IBOutlet weak var indicator: UIActivityIndicatorView!


}

สำหรับมุมมองตาราง: 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

}

หากคุณสังเกตว่าเซลล์โหลดของฉันสร้างขึ้นจากไฟล์ nib วิดีโอนี้จะอธิบายสิ่งที่ฉันทำ


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

สำหรับ Xcode 10.1, Swift 4.2

วิดีโอนี้ดูเหมือนเป็นการสอนที่ยอดเยี่ยม!

เริ่มต้น / โครงการที่สมบูรณ์: 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()
        })
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.