paint-brush
TableView, CollectionView Infinite Scrolling: An Easy Way in Swiftby@mrigankgupta
3,361 reads
3,361 reads

TableView, CollectionView Infinite Scrolling: An Easy Way in Swift

by mrigankguptaDecember 17th, 2019
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

A framework for infinite scroll allows users to load content continuously, eliminating the need for user’s explicit actions. The trickiest part was the interfacing with network. It took some time to figure out how to separate it from the dependency of network stack. We need an Inter-actor/ Controller which maintains number of visible rows, maintain current page count and update all of these when it gets next page data array from the network. An interface between Pageable and network stack which has two methods. This is a clean way to separate Pageable library from your networking code as well as clean.

Company Mentioned

Mention Thumbnail
featured image - TableView, CollectionView Infinite Scrolling: An Easy Way in Swift
mrigankgupta HackerNoon profile picture
Pagination, also known as paging, is the process of dividing a document into discrete pages, either electronic pages or printed pages.”

It is most common technique to manage large data set at server/client side to distribute in chunks called as pages. In today’s time, Social media client apps improvised this by inventing “Infinite scroll”.

Infinite scrolling allows users to load content continuously, eliminating the need for user’s explicit actions. App loads some initial data and then load the rest of the data when the user reaches the bottom of the visible content. This data is divided in pages.

A framework for Infinite scrolling?

As apps size and content, are increasing day by day, we need to implement infinite scroll in more than one places in our app. Although implementing infinite scroll is not very tricky but it is a repetitive job and create redundant code inside TableView/CollectionView and yes it breaks the “DRY” principal too.

Whenever we have repeated code, there is always a chance of introducing a bug. I lately implemented infinite scroll in some of apps and I felt that at times you need to implement this in quite a few places, typically if you are working on a large e-commerce app. I was looking for a solution, a framework which does this job for me.

I was not able to find a non-intrusive and easy to integrate solution where I don’t need to sub-class my ViewController or use a special kind of tableView/collectionView for this and It can work with my network stack. In short, I don’t want to create direct dependency of a framework/library all over in my project code.

So I thought of giving it a try.

Initial, I was not clear on what and how part. My refactored efforts were scattered all over the place. While I ended up having a non-intrusive solution (what I played out as a requirement), I learned an important lesson after making mistakes.

Zoom out as far as you can until you are clear who is playing what role.

Initially I was relentlessly trying to decouple/refactored code without identifying my interface and participant objects or classes. You can only do “Separation of Concern” when you identify which class is going to do what.

What it takes to use your framework/Library

When I started refactoring my code for library, I had not laid out my interfaces well. Generally this is a common problem. We usually don’t developed solutions which are being designed for larger audience but used within few use cases. It really open our eyes when we start thinking in this direction. And yes, this should be second step when we develop a framework.

What part:

1. We need an Inter-actor/ Controller which

  • Maintains number of visible rows
  • Load next page at the end of scroll if required
  • Maintain current page count and update all of these when it gets next page data array from the network.

2. To accomplish all of this, It needs some supporting function on top of tableView/collectionView

3. An interface between network stack.

How Part:

The trickiest part was the interfacing with network. It took some time to figure out how to separate it from the dependency of network stack.

Most of pagination responses have the common structure which comprises of total Page count, current page and an array of elements. With this assumption, A struct can be defined for all responses over generic parameter Type.

public struct PageInfo<T> {
    var types: [T]
    var page: Int
    var totalPageCount: Int
    
    public init(types: [T], page: Int, totalPageCount: Int) {
        self.types = types
        self.page = page
        self.totalPageCount = totalPageCount
    }
}

Now with this PageInfo struct in place, We have a generic way of giving data back to Inter-actor/Controller for updating the results after getting it from web service. This solves one problem of puzzle, another piece of puzzle is to indicate Network stack to download next pages data as and when required.

So I designed an interface between Pageable and network stack which has two methods.

public protocol PageableService: class {
    /// Indicates when to load next page
    func load(pageInteractor: PageInteractor, page: Int) -> Void 
    /// Cancel request if required
    func cancelAllRequests()
}

Function LoadPage is called by PageInteractor when it figure out that there is more data to load. A typical implementation can be

func load(pageInteractor: PageInteractor, page: Int) -> Void {
    guard let resource: Resourse<PagedResponse<[User]>> = try? prepareResource(page: page,
                                                                               pageSize: pageSize,
                                                                               pathForREST: "/api/users")
        else { return }
    // Construct PageInfo to be returned
    var info: PageInfo<User>?
    networkManager.fetch(res: resource) { (res) in
        switch res {
        case let .success(result):
            info = PageInfo(types: result.types,
                            page: result.page,
                            totalPageCount: result.totalPageCount)
        case let .failure(err):
            print(err)
        }
        // Require to call method on PageInteractor for updating result.
        pgInteractor.returnedResponse(info)
    }
}

First method func loadPage(_ page: Int) -> Void indicates when to request which page. As It loads next set of data, the loaded data needs to be sent to PageInteractor for updating result.

Now calling PageInteractor, is a requirement which can be missed as API is not enforcing it.

Completion blocks are here!

Completion block helps in situations like this. So we can update an interface with completion block. As PageInfo<T> operates on generic parameter, This requires to create a generic interface to adopt by.

public protocol PageableService: class {
    func loadPage<Item: Decodable>(_ page: Int,
                                   completion: @escaping (PageInfo<Item>?) -> Void)
    func cancelAllRequests()
}

This create a clear requirement for user to send data back to Page Interactor as well as gives a clean way to separate Pageable library from your networking code.

Power of KeyPath:

Pageable gives a feature where duplicate entries from server can be avoided at client side if somehow new entries are being added dynamically at server end. More can be found here.

To use this feature your pagination data should have a unique Id in response array so that duplicates can be identified. To make this feature usable, It requires a user to give a power to provide a generic way to identify that field.

Earlier due to inability to provide this information to PageInteractor, User needs to implement interface PageDataSource methods which checks items in dictionary and then add in final array to avoid duplication. This is very redundant task for user and ideally handled by PageInteractor only.

extension UserView: PageDataSource {
    
    func addUniqueItems(for items: [AnyObject]) -> Range<Int> {
        let startIndex = pgInteractor.count()
        if let items = items as? [User] {
            for new in items {
                if pgInteractor.dict[String(new.id)] == nil {
                    pgInteractor.dict[String(new.id)] = String(new.id)
                    pgInteractor.array.append(new) // adding items in array to be displayed
                }
            }
        }
        return startIndex..<pgInteractor.count()
    }
    
    func addAll(items: [AnyObject]) {
        if let items = items as? [User] {
            pgInteractor.array = items
            for new in items {
                pgInteractor.dict[String(new.id)] = String(new.id)
            }
        }
    }
}

KeyPath are a way to reference properties without invoking them. It can be composed to make different path and used to get/set underlying properties.

With the KeyPath passed in the initialiser of PageInteractor, implementation of both methods can move entirely in PageInteractor. This will relive user from not implementing PageDataSource.

extension PageInteractor {
    /**
     Server can add/remove items dynamically so it might be a case that
     an item which appears in previous request can come again due to
     certain element below got removed. This could result as duplicate items
     appearing in the list. To mitigate it, we would be creating a parallel dictionary
     which can be checked for duplicate items
     
     - Parameter items: items to be added
     - Parameter keypath: In case if duplicate entries has to be filter out,
     It requires keypath of unique items in model data.
     */
    open func addUniqueFrom(items: [Element], keypath: KeyPath<Element, KeyType>?) -> Range<Int> {
        let startIndex = count()
        if let keypath = keypath {
            for new in items {
                let key = new[keyPath: keypath]
                if dict[key] == nil {
                    dict[key] = key
                    array.append(new)
                }
            }
        }
        return startIndex..<count()
    }
    /** Add all items, If there is empty list in table view
     - Parameter items: items to be added
     - Parameter keypath: In case if duplicate entries has to be filter out,
     It requires keypath of unique items in model data.
     */
    open func addAll(items: [Element], keypath: KeyPath<Element, KeyType>?) {
        array = items
        guard let keypath = keypath else {
            return
        }
        for new in items {
            let key = new[keyPath: keypath]
            dict[key] = key
        }
    }
    
}

With default implementation embedded in PageInteractor, All it takes is to provide KeyPath of unique data type to PageInteractor to use this feature.

let pageInteractor: PageInteractor<UserModelInt> = PageInteractor(firstPage: firstReqIndex, service: service, keyPath: \UserModel.id)

Conclusion

While developing infinite scrolling as a framework, was fun and turned out to be an easy solution to integrate with few steps to follow. I learned an important lesson on what part. There is always a room of improvement when you look back to your solution. I did a few iterations while finalising interface for Pageable and now I feel next time I can avoid few of those.

You can find library here at Github, feel free to give it a try/feedback !