我正在尝试完成我的HomeFeedController的分页,如下所示 .

import Foundation
import UIKit
import Alamofire
import AlamofireNetworkActivityIndicator
import SwiftLocation
import CoreLocation

class HomeFeedController: UICollectionViewController, UICollectionViewDelegateFlowLayout,UIGestureRecognizerDelegate {
    var isFinishedPaging = false
    let detailView = EventDetailViewController()
    let refreshControl = UIRefreshControl()
    var emptyLabel: UILabel?
    var allEvents = [Event]()
    //will containt array of event keys
    var eventKeys = [String]()

    var grideLayout = GridLayout(numberOfColumns: 2)

    let paginationHelper = PaginationHelper<Event>(serviceMethod: PostService.showEvent)

    override func viewDidLoad() {
        super.viewDidLoad()
        // we had to do this way because View Controller only has a view now Uicollection view does as well because it is a subclass of UI viewcontroller and because this controller also contains a collection view that is a child of the view controllers view we have to change the collection views background color because that's whats being presented to the screen
        collectionView?.backgroundColor = UIColor.white
        // may need to take this out
        //collectionView?.delegate = self
        //collectionView?.dataSource = self
        collectionView?.collectionViewLayout = grideLayout
        collectionView?.reloadData()
        self.collectionView?.contentInset = UIEdgeInsetsMake(20, 0, 0, 0)
        navigationItem.title = "Home Page"
        collectionView?.register(CustomCell.self, forCellWithReuseIdentifier: customCellIdentifier)

//        PostService.showEvent(location: User.current.location!) { (event) in
//            self.allEvents = event
//            print(self.allEvents)
//            
//            DispatchQueue.main.async {
//                self.collectionView?.reloadData()
//            }
//        }
        configureCollectionView()
        reloadHomeFeed()
    }


    func reloadHomeFeed() {
        self.paginationHelper.reloadData(completion: { [unowned self] (events) in
            self.allEvents = events

            if self.refreshControl.isRefreshing {
                self.refreshControl.endRefreshing()
            }

            DispatchQueue.main.async {
                self.collectionView?.reloadData()
            }
        })
    }


    func configureCollectionView() {
        // add pull to refresh
        refreshControl.addTarget(self, action: #selector(reloadHomeFeed), for: .valueChanged)
        collectionView?.addSubview(refreshControl)
    }

    // need to tell it how many cells to have

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return allEvents.count
    }

    let customCellIdentifier = "customCellIdentifier"
    // need to tell the collection view controller what type of cell we want to return
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: customCellIdentifier, for: indexPath) as! CustomCell
        let imageURL = URL(string: allEvents[indexPath.item].currentEventImage)
        print(imageURL ?? "")
        customCell.sampleImage.af_setImage(withURL: imageURL!)
        return customCell
    }
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        //let selectedEvent = self.imageArray[indexPath.row]
        //let eventDetailVC
        if let cell = collectionView.cellForItem(at: indexPath){
            detailView.eventImage = allEvents[indexPath.row].currentEventImage
            detailView.eventName = allEvents[indexPath.row].currentEventName
          //  print("Look here for event name")
           // print(detailView.eventName)
            detailView.eventDescription = allEvents[indexPath.row].currentEventDescription
            detailView.eventStreet = allEvents[indexPath.row].currentEventStreetAddress
            detailView.eventCity = allEvents[indexPath.row].currentEventCity
            detailView.eventState = allEvents[indexPath.row].currentEventState
            detailView.eventZip = allEvents[indexPath.row].currentEventZip
            detailView.eventKey = allEvents[indexPath.row].key!
            detailView.eventPromo = allEvents[indexPath.row].currentEventPromo!
            detailView.eventDate = allEvents[indexPath.row].currentEventDate!
            detailView.eventTime = allEvents[indexPath.row].currentEventTime!
            detailView.currentEvent = allEvents[indexPath.row]
            present(detailView, animated: true, completion: nil)
            //self.navigationController?.pushViewController(detailView, animated: true)

        }
         print("Cell \(indexPath.row) selected")
    }

    override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

        if indexPath.item >= allEvents.count - 1 {
            print("paginating for post")
            paginationHelper.paginate(completion: { [unowned self] (events) in
                self.allEvents.append(contentsOf: events)

                DispatchQueue.main.async {
                    self.collectionView?.reloadData()
                }
            })
        }else{
            print("Not paginating")
        }
    }


    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

    //will make surepictures keep same orientation even if you flip screen
    // will most likely look into portrait mode but still good to have
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        grideLayout.invalidateLayout()
    }
    //Will allow the first two cells that are displayed to be of varying widths
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        if indexPath.item == 0 || indexPath.item == 1 {
            return CGSize(width: view.frame.width, height: grideLayout.itemSize.height)
        }else{
            return grideLayout.itemSize
        }
    }


}


//responsible for populating each cell with content

class CustomCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    let sampleImage: UIImageView = {
        let firstImage = UIImageView()
        firstImage.clipsToBounds = true
        firstImage.translatesAutoresizingMaskIntoConstraints = false
        firstImage.contentMode = .scaleToFill
        firstImage.layer.masksToBounds = true
        return firstImage
    }()
    let nameLabel: UILabel = {
        let name = UILabel()
        name.text = "Custom Text"
        name.translatesAutoresizingMaskIntoConstraints = false
        return name
    }()
    func setupViews() {
        addSubview(sampleImage)
        backgroundColor = UIColor.white
        //addSubview(nameLabel)
        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": sampleImage]))
        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": sampleImage]))
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

// responsible for creating the grid layout that you see in the home view feed

class GridLayout: UICollectionViewFlowLayout {

    var numberOfColumns:Int = 2

    init(numberOfColumns: Int) {
        super.init()
        // controlls spacing inbetween them as well as spacing below them to next item
        self.numberOfColumns = numberOfColumns
        self.minimumInteritemSpacing = 3
        self.minimumLineSpacing = 5
    }
    // just needs to be here because swift tells us to
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override var itemSize: CGSize{
        get{
            if collectionView != nil {
                let collectionVieweWidth = collectionView?.frame.width
                let itemWidth = (collectionVieweWidth!/CGFloat(self.numberOfColumns)) - self.minimumInteritemSpacing
                let itemHeight: CGFloat = 200
                return CGSize(width: itemWidth, height: itemHeight)
            }
            return CGSize(width: 100, height: 100)
        }set{
            super.itemSize = newValue
        }
    }


}

由于下面列出的此功能,分页发生

import Foundation

protocol Keyed {
    var key: String? { get set }
}

// Create a new instance using a genetic type
//let paginationHelper = MGPaginationHelper<Post>()

// Generic class type



//   1. initial - no data has been loaded yet
//   2.  ready - ready and waiting for next request to paginate and load the next page
//   3.  loading - currently paginating and waiting for data from Firebase
//   4.  end - all data has been paginated

enum PaginationState
{
    case initial
    case ready
    case loading
    case end
}

class PaginationHelper<T : Keyed>
{



    // MARK: - Properties

    // 1. page size - Determines the number of posts that will be on each page
    // 2. serviceMethod - The service method that will return paginated data
    // 3. state - The current pagination state of the helper
    // 4. lastobjectKey - Firebase uses object keys to determine the last position of the page. We'lll need to use this as an offset for paginating.
    let pageSize: UInt
    let serviceMethod: (UInt, String?, @escaping (([T]) -> Void)) -> Void
    var state: PaginationState = .initial
    var lastObjectKey: String?

    // MARK: - Init
    //    Can change the default page size for our helper
    //    Set the service method that will be paginated and return data
    init(pageSize: UInt = 5, serviceMethod: @escaping (UInt, String?, @escaping (([T]) -> Void)) -> Void) {
        self.pageSize = pageSize
        self.serviceMethod = serviceMethod
    }


    // 1 Notice our completion parameter type. We use our generic type to enforce that we return type T.
    func paginate(completion: @escaping([T]) -> Void)
    {
        // 2 We switch on our helper's state to determine the behavior of our helper when paginate(completion:) is called
        switch state
        {
        // 3 For our initial state, we make sure that the lastObjectKey is nil use the fallthrough keyword to execute the ready case below.
        case .initial:
            lastObjectKey = nil
            fallthrough
        //4 For our ready state, we make sure to change the state to loading and execute our service method to return the paginated data.
        case .ready:
            state = .loading
            print(lastObjectKey)
            serviceMethod(pageSize, lastObjectKey) { [unowned self] (objects: [T]) in
                //5 We use the defer keyword to make sure the following code is executed whenever the closure returns. This is helpful for removing duplicate code.
                defer {
                    //6 If the returned last returned object has a key value, we store that in lastObjectKey to use as a future offset for paginating. Right now the compiler will throw an error because it cannot infer that T has a property of key. We'll fix that next.
                    if let lastObjectKey = objects.first?.key {
                        self.lastObjectKey = lastObjectKey
                        print(self.lastObjectKey)
                        print(lastObjectKey)
                    }
                    // 7 We determine if we've paginated through all content because if the number of objects returned is less than the page size, we know that we're only the last page of objects.
                    self.state = objects.count < Int(self.pageSize) ? .end : .ready
                }

                // 8 If lastObjectKey of the helper doesn't exist, we know that it's the first page of data so we return the data as is.
                guard let _ = self.lastObjectKey else {
                    print(self.lastObjectKey)
                    return completion(objects)
                }

                // 9 Due to implementation details of Firebase, whenever we page with the lastObjectKey, the previous object from the last page is returned. Here we need to drop the first object which will be a duplicate post in our timeline. This happens whenever we're no longer on the first page.
                print(objects.last?.key)
                let newObjects = Array(objects.dropLast())
              //  print(newObjects)
                completion(newObjects)

            }

        //10 If the helper is currently paginating or has no more content, the helper returns and doesn't do anything.
        case . loading, .end:
            return
        }
    }

    //  resets the pagination helper to it's initial state
    func reloadData(completion: @escaping ([T]) -> Void)
    {
        state = .initial
        paginate(completion: completion)
    }


}

此分页用于操作的服务方法是这样的

static func showEvent(pageSize: UInt, lastPostKey: String? = nil,completion: @escaping ([Event]) -> Void) {
        //getting firebase root directory
        print(lastPostKey)
        var currentEvents = [Event]()
        let eventsByLocationRef = Database.database().reference().child("eventsbylocation").child(User.current.location!)

        //let ref = Database.database().reference().child("events")
        var query = eventsByLocationRef.queryOrderedByKey().queryLimited(toFirst: pageSize)
        if let lastPostKey = lastPostKey {
            print(lastPostKey)
            query = query.queryEnding(atValue: lastPostKey)
        }
        query.observeSingleEvent(of: .value, with: { (snapshot) in
            print(snapshot)
            print(snapshot.value)
            guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else{
                return
            }

            allObjects.forEach({ (snapshot) in
              print(snapshot.value)
                EventService.show(forEventKey: snapshot.value as! String, completion: { (event) in
                    currentEvents.append(.init(currentEventKey: snapshot.value as! String, dictionary: (event?.eventDictionary)!))
                  //  print(currentEvents)
                    print(currentEvents.count)
                    completion(currentEvents)
                })

            })


        })

    }

好吧,现在我已经提供了可以解释问题的方法 . 当我为showEvent函数打印出快照时,我得到了这个订单

Snap (37%2e7,-122%2e4) {
    event = MIA;
    event2 = CCDS;
    event3 = MIA2;
    event4 = MIA3;
    event5 = CCDS;
}

这是基于我的数据库打印的快照,看起来像这样

"events" : {
    "CCDS" : {
      "attend:count" : 2,
      "event:city" : "San Francisco",
      "event:date" : {
        "end:date" : "08/09/2017",
        "end:time" : "7:00 PM",
        "start:date" : "08/09/2017",
        "start:time" : "5:00 PM"
      },
      "event:description" : "Happy hour is more joyful in the summer thanks to Center City District Sips, which offers discounted drinks and appetizers every Wednesday evening.  Catch up with old friends and make a few new ones as Center City’s best bars and restaurants host the summer’s happiest hour every Wednesday from 5-7 p.m.  Enjoy $5 cocktails, $4 wine, $3 beers and half-price appetizers at dozens and dozens of bars and restaurants.",
      "event:imageURL" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_flyers%2FCCDS-compressor.jpg?alt=media&token=bcce3968-1cca-4890-a3d0-d8064bd0d1da",
      "event:name" : "center city district sips",
      "event:promo" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_promo_vid%2FMIA%2FBudweiser%20Made%20In%20America%20Festival%202013%20August%2031%20-%20September%201.mp4?alt=media&token=9b4b9d4f-2d6d-4762-a5fd-c72edb943ac4",
      "event:state" : "PA",
      "event:street:address" : "660 Chestnut St",
      "event:zip" : 19130
    },
    "DD" : {
      "attend:count" : 2,
      "event:city" : "New York",
      "event:date" : {
        "end:date" : "08/26/2017",
        "end:time" : "5:00 PM",
        "start:date" : "08/26/2017",
        "start:time" : "1:00 PM"
      },
      "event:description" : "Help us celebrate the hard work and creativity of the students and demo the iOS apps and games they've built in only 8 weeks! Developers, entrepreneurs, friends, and industry professionals are all welcome to attend",
      "event:imageURL" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_flyers%2Fmakeschooldemo-compressor.jpg?alt=media&token=1e75d18d-1949-48e8-a208-2ca88cde395b",
      "event:name" : "demo day",
      "event:promo" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_promo_vid%2FDD%2FDemo%20Night%202017%20-%20Make%20School%20Product%20College.mp4?alt=media&token=d9adb4b1-5689-4b15-96b2-e18138701526",
      "event:state" : "PA",
      "event:street:address" : "394 Broadway",
      "event:zip" : 10013
    },
    "MIA" : {
      "attend:count" : 23,
      "event:city" : "San Francisco",
      "event:date" : {
        "end:date" : "09/03/2017",
        "end:time" : "7:00 PM",
        "start:date" : "09/02/2017",
        "start:time" : "12:00 PM"
      },
      "event:description" : "Budweiser Made in America Festival is an annual music festival held in Philadelphia and formerly simultaneously held in Los Angeles.Sponsored by Anheuser–Busch and produced by Live Nation, the event features several stages that continuously host live music from a wide range of genres including hip hop, rock, pop, R&B, and EDM.",
      "event:imageURL" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_flyers%2FMadeInAmerica-compressor.jpg?alt=media&token=1ac6e794-6a1f-4f8a-bdb0-afc91f8ba6ae",
      "event:name" : "made in america",
      "event:promo" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_promo_vid%2FMIA%2FBudweiser%20Made%20In%20America%20Festival%202013%20August%2031%20-%20September%201.mp4?alt=media&token=9b4b9d4f-2d6d-4762-a5fd-c72edb943ac4",
      "event:state" : "PA",
      "event:street:address" : "Ben Franklin Parkway",
      "event:zip" : 19130
    },
    "MIA2" : {
      "attend:count" : 2,
      "event:city" : "Philadelphia",
      "event:date" : {
        "end:date" : "09/03/2017",
        "end:time" : "7:00 PM",
        "start:date" : "09/02/2017",
        "start:time" : "12:00 PM"
      },
      "event:description" : "Budweiser Made in America Festival is an annual music festival held in Philadelphia and formerly simultaneously held in Los Angeles. Sponsored by Anheuser–Busch and produced by Live Nation, the event features several stages that continuously host live music from a wide range of genres including hip hop, rock, pop, R&B, and EDM. This is different regardless of the same name in database",
      "event:imageURL" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_flyers%2Fmadeinamerica2.jpg?alt=media&token=bd5eb5e4-4e22-4412-a34b-2dc6c9ae561e",
      "event:name" : "made in america",
      "event:promo" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_promo_vid%2FMIA%2FBudweiser%20Made%20In%20America%20Festival%202013%20August%2031%20-%20September%201.mp4?alt=media&token=9b4b9d4f-2d6d-4762-a5fd-c72edb943ac4",
      "event:state" : "PA",
      "event:street:address" : "Ben Franklin Parkway",
      "event:zip" : 19130
    },
    "MIA3" : {
      "attend:count" : 8,
      "event:city" : "Philadelphia",
      "event:date" : {
        "end:date" : "09/03/2017",
        "end:time" : "7:00 PM",
        "start:date" : "09/02/2017",
        "start:time" : "12:00 PM"
      },
      "event:description" : "Budweiser Made in America Festival is an annual music festival held in Philadelphia and formerly simultaneously held in Los Angeles. Sponsored by Anheuser–Busch and produced by Live Nation, the event features several stages that continuously host live music from a wide range of genres including hip hop, rock, pop, R&B, and EDM.",
      "event:imageURL" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_flyers%2Fmadeinamerica3-compressor.jpg?alt=media&token=9166e2fb-ac9d-46d5-b06f-60cffc337b15",
      "event:name" : "made in america",
      "event:promo" : "https://firebasestorage.googleapis.com/v0/b/eventful-3d558.appspot.com/o/event_promo_vid%2FMIA%2FBudweiser%20Made%20In%20America%20Festival%202013%20August%2031%20-%20September%201.mp4?alt=media&token=9b4b9d4f-2d6d-4762-a5fd-c72edb943ac4",
      "event:state" : "PA",
      "event:street:address" : "Ben Franklin Parkway",
      "event:zip" : 19130
    }
  },
  "eventsbylocation" : {
    "37%2e7,-122%2e4" : {
      "event0" : "MIA",
      "event1" : "CCDS",
      "event2" : "MIA2",
      "event3" : "MIA3",
      "event4" : "CCDS",
      "event5" : "MIA",
      "event6" : "MIA2",
      "event7" : "MIA3",
      "event8" : "CCDS",
      "event9" : "MIA"
    },
    "40%2e7,-74%2e0" : {
      "event" : "DD"
    }
  },

基于该函数,它应以相同的顺序传递每个函数并以相同的顺序返回它们 . 我使用foreach循环实现了这一点,我传入了snapshot.value,这将是MIA的第一个快照

Snap (37%2e7,-122%2e4) {
        event = MIA;
        event2 = CCDS;
        event3 = MIA2;
        event4 = MIA3;
        event5 = CCDS;
    }

它们以正确的顺序传递到我的showEvent函数中,但是当完成处理程序返回事件时,它会按顺序返回

MIA
CCDS
CCDS
MIA2
MIA3

然后它从最后一个掉落 . 现在这导致了很多错误,因为1.我按此顺序传递了它们

MIA
CCDS
MIA2
MIA3
CCDS
  • 当我尝试为接下来的5分页时,最后一个对象键被设置为MIA3,导致我的数据库调用返回null,这阻止了我的分页过程 .

所以简而言之,我的问题是

为什么它以不同的顺序返回,我怎样才能确保它以我传递的顺序返回,这样我的分页才能正常工作?

我愿意支付任何可以解决这个问题的人 . 我知道这是分页助手,因为在我添加之前我的代码工作得很好 . 我只是想让它起作用,因为它将有助于我的表现