Created
April 19, 2018 10:31
-
-
Save brocoo/2ec7006f5592bb8c9f6e3a4f505f51f6 to your computer and use it in GitHub Desktop.
Generic data source storing pages of content, providing an efficient way to handle pagination.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// MARK: - | |
public struct PaginatedList<T: Decodable> { | |
// MARK: - Action | |
enum Mutation { | |
case created | |
case newPageAdded(Int) | |
} | |
// MARK: - Properties | |
private var contents: [T] | |
private var pages: [CountableRange<Int>] | |
private(set) var lastContentUpdate: Mutation | |
private(set) var hasMoreContent: Bool | |
// MARK: - Static properties | |
static var empty: PaginatedList { return PaginatedList() } | |
// MARK: - Initializer | |
private init() { | |
pages = [] | |
contents = [] | |
lastContentUpdate = .created | |
hasMoreContent = false | |
} | |
public init(with page: Page<T>) throws { | |
guard page.pageIndex == 0 else { throw PaginatedListError.initialPageIndexNotZero(page.pageIndex) } | |
contents = page.asArray | |
pages = [(0..<contents.endIndex)] | |
lastContentUpdate = .created | |
hasMoreContent = page.hasNextPage | |
} | |
// MARK: - Helper functions / properties | |
public mutating func append(_ page: Page<T>) throws { | |
let nextPageIndex = pages.count | |
if nextPageIndex > 0 { | |
guard nextPageIndex == page.pageIndex else { throw PaginatedListError.wrongNextPageIndex(page.pageIndex, expected: nextPageIndex) } | |
let startIndex = contents.endIndex | |
contents.append(contentsOf: page.asArray) | |
pages.append((startIndex..<contents.endIndex)) | |
lastContentUpdate = .newPageAdded(nextPageIndex) | |
hasMoreContent = page.hasNextPage | |
} else { | |
self = try PaginatedList(with: page) | |
} | |
} | |
public func appending(_ page: Page<T>) throws -> PaginatedList<T> { | |
var newPagedList = self | |
try newPagedList.append(page) | |
return newPagedList | |
} | |
public func contents(atPage i: Int) -> [T] { | |
return Array(contents[pages[i]]) | |
} | |
public func indexes(forPage i: Int) -> CountableRange<Int> { | |
return pages[i] | |
} | |
} | |
// MARK: - | |
extension PaginatedList: RandomAccessCollection { | |
// MARK: - RandomAccessCollection | |
public var startIndex: Int { | |
return contents.startIndex | |
} | |
public var endIndex: Int { | |
return contents.endIndex | |
} | |
public func index(after i: Int) -> Int { | |
return contents.index(after: i) | |
} | |
public subscript(index: Int) -> T { | |
return contents[index] | |
} | |
} | |
// MARK: - | |
enum PaginatedListError: Error { | |
// MARK: - Errors | |
case initialPageIndexNotZero(Int) | |
case wrongNextPageIndex(Int, expected: Int) | |
} | |
// MARK: - | |
extension Error { | |
// MARK: - | |
var isPaginationRelated: Bool { | |
return self is PaginatedListError | |
} | |
} | |
// MARK: - | |
public struct Page<T: Decodable> { | |
// MARK: - Properties | |
let pageIndex: Int | |
let totalResults: Int | |
let totalPages: Int | |
private let results: [T] | |
// MARK: - Computed properties | |
var nextPageIndex: Int { return pageIndex + 1 } | |
var hasNextPage: Bool { return nextPageIndex < totalPages } | |
var asArray: [T] { return results } | |
// MARK: - Initializer | |
init(pageIndex: Int, totalResults: Int, totalPages: Int, results: [T]) { | |
self.pageIndex = pageIndex | |
self.totalResults = totalResults | |
self.totalPages = totalPages | |
self.results = results | |
} | |
} | |
// MARK: - | |
extension Page: RandomAccessCollection { | |
// MARK: - RandomAccessCollection | |
public var startIndex: Int { | |
return results.startIndex | |
} | |
public var endIndex: Int { | |
return results.endIndex | |
} | |
public func index(after i: Int) -> Int { | |
return results.index(after: i) | |
} | |
public subscript(index: Int) -> T { | |
return results[index] | |
} | |
} | |
// MARK: - | |
extension Page: Decodable { | |
// MARK: - Keys | |
private enum CodingKeys: String, CodingKey { | |
case page | |
case totalResults = "total_results" | |
case totalPages = "total_pages" | |
case results | |
} | |
// MARK: - Initializer | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let pageIndex = try container.decode(Int.self, forKey: .page) - 1 | |
let totalResults = try container.decode(Int.self, forKey: .totalResults) | |
let totalPages = try container.decode(Int.self, forKey: .totalPages) | |
let results = try container.decode([T].self, forKey: .results) | |
self.init(pageIndex: pageIndex, totalResults: totalResults, totalPages: totalPages, results: results) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sure!
Please check out my repo: WhatFilm, I use it in there.