This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
555 lines
25 KiB
Swift
555 lines
25 KiB
Swift
import Foundation
|
|
import CocoaLumberjackSwift
|
|
|
|
internal let APIReadingListUpdateLimitForFullSyncFallback = 1000 // if we receive over this # of updated items, fall back to full sync
|
|
|
|
public enum APIReadingListError: String, Error, Equatable {
|
|
case generic = "readinglists-client-error-generic"
|
|
case notLoggedIn = "notloggedin"
|
|
case badtoken = "badtoken"
|
|
case notSetup = "readinglists-db-error-not-set-up"
|
|
case alreadySetUp = "readinglists-db-error-already-set-up"
|
|
case listLimit = "readinglists-db-error-list-limit"
|
|
case entryLimit = "readinglists-db-error-entry-limit"
|
|
case duplicateEntry = "readinglists-db-error-duplicate-page"
|
|
case needsFullSync = "readinglists-client-error-needs-full-sync"
|
|
case listDeleted = "readinglists-db-error-list-deleted"
|
|
case listEntryDeleted = "readinglists-db-error-list-entry-deleted"
|
|
case defaultListCannotBeUpdated = "readinglists-db-error-cannot-update-default-list"
|
|
case defaultListCannotBeDeleted = "readinglists-db-error-cannot-delete-default-list"
|
|
case noSuchProject = "readinglists-db-error-no-such-project"
|
|
case noSuchListEntry = "readinglists-db-error-no-such-list-entry"
|
|
case noSuchList = "readinglists-db-error-no-such-list"
|
|
case duplicateList = "readinglists-db-error-duplicate-list"
|
|
|
|
public var localizedDescription: String {
|
|
switch self {
|
|
case .listLimit:
|
|
return WMFLocalizedString("reading-list-api-error-list-limit", value: "This list is not synced because you have reached the limit for the number of synced lists.", comment: "You have too many lists.")
|
|
case .entryLimit:
|
|
return WMFLocalizedString("reading-list-api-error-entry-limit", value: "This entry is not synced because you have reached the limit for the number of entries in this list.", comment: "You have too many entries in this list.")
|
|
default:
|
|
return WMFLocalizedString("reading-list-api-error-generic", value: "An unexpected error occurred while syncing your reading lists.", comment: "An unexpected error occurred while syncing your reading lists.")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct APIReadingLists: Codable {
|
|
let lists: [APIReadingList]
|
|
let next: String?
|
|
let since: String?
|
|
enum CodingKeys: String, CodingKey {
|
|
case lists
|
|
case next
|
|
case since = "continue-from"
|
|
}
|
|
}
|
|
|
|
public struct APIReadingList: Codable {
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case name
|
|
case description
|
|
case created
|
|
case updated
|
|
case deleted
|
|
case isDefault = "default"
|
|
}
|
|
|
|
public let id: Int64
|
|
let name: String
|
|
let description: String
|
|
let created: String
|
|
let updated: String
|
|
let deleted: Bool?
|
|
let isDefault: Bool
|
|
}
|
|
|
|
struct APIReadingListEntries: Codable {
|
|
let entries: [APIReadingListEntry]
|
|
let next: String?
|
|
}
|
|
|
|
public struct APIReadingListEntry: Codable {
|
|
let id: Int64
|
|
let project: String
|
|
let title: String
|
|
let created: String
|
|
let updated: String
|
|
let listId: Int64?
|
|
let deleted: Bool?
|
|
}
|
|
|
|
struct APIReadingListChanges: Codable {
|
|
let lists: [APIReadingList]?
|
|
let entries: [APIReadingListEntry]?
|
|
let next: String?
|
|
let since: String?
|
|
enum CodingKeys: String, CodingKey {
|
|
case lists
|
|
case entries
|
|
case next
|
|
case since = "continue-from"
|
|
}
|
|
}
|
|
|
|
struct APIReadingListErrorResponse: Codable {
|
|
let type: String?
|
|
let title: String
|
|
let method: String?
|
|
let detail: String?
|
|
}
|
|
|
|
enum APIReadingListRequestType: String {
|
|
case setup, teardown
|
|
}
|
|
|
|
/* Note that because the reading list API does not support language variants,
|
|
* the articleURL will always have a nil language variant.
|
|
*
|
|
* The RemoteReadingListArticleKey type is a type alias for String.
|
|
* Since ReadingListsSyncOperation handles remote entries that don't have a variant,
|
|
* and local entries that do have a variant, this type makes it more clear when
|
|
* a non-variant aware key is being used.
|
|
*
|
|
* Also, if the remote API adds variant support, it should be straightforward to
|
|
* update the type alias from String to WMFInMemoryURLKey.
|
|
*/
|
|
typealias RemoteReadingListArticleKey = String
|
|
extension APIReadingListEntry {
|
|
var articleURL: URL? {
|
|
guard let site = URL(string: project) else {
|
|
return nil
|
|
}
|
|
return site.wmf_URL(withTitle: title)
|
|
}
|
|
|
|
var articleKey: RemoteReadingListArticleKey? {
|
|
return articleURL?.wmf_databaseKey
|
|
}
|
|
}
|
|
|
|
public class ReadingListsAPIController: Fetcher {
|
|
private let builder = Configuration.current.pageContentServiceBuilder(withWikiHost: "en.wikipedia.org")
|
|
private let basePathComponents = ["data", "lists"]
|
|
var lastRequestType: APIReadingListRequestType?
|
|
|
|
fileprivate func get<T: Codable>(path: [String], queryParameters: [String: Any]? = nil, completionHandler: @escaping (T?, URLResponse?, Error?) -> Swift.Void) {
|
|
let key = UUID().uuidString
|
|
let components = builder.components(byAppending: basePathComponents + path, queryParameters: queryParameters)
|
|
guard
|
|
let task = session.jsonDecodableTaskWithDecodableError(with: components.url, method: .get, completionHandler: { (result: T?, errorResult: APIReadingListErrorResponse?, response, error) in
|
|
if let errorResult = errorResult, let error = APIReadingListError(rawValue: errorResult.title) {
|
|
completionHandler(nil, nil, error)
|
|
} else {
|
|
completionHandler(result, response, error)
|
|
}
|
|
self.untrack(taskFor: key)
|
|
}) else {
|
|
return
|
|
}
|
|
track(task: task, for: key)
|
|
task.resume()
|
|
}
|
|
|
|
fileprivate func requestWithCSRF(path: [String], method: Session.Request.Method, bodyParameters: [String: Any]? = nil, completion: @escaping ([String: Any]?, URLResponse?, Error?) -> Void) {
|
|
let components = builder.components(byAppending: basePathComponents + path)
|
|
requestMediaWikiAPIAuthToken(for: components.url, type: .csrf) { (result) in
|
|
switch result {
|
|
case .failure(let error):
|
|
completion(nil, nil, error)
|
|
case .success(let token):
|
|
let tokenQueryParameters = ["csrf_token": token.value]
|
|
var componentsWithToken = components
|
|
componentsWithToken.appendQueryParametersToPercentEncodedQuery(tokenQueryParameters)
|
|
let identifier = UUID().uuidString
|
|
let task = self.session.jsonDictionaryTask(with: componentsWithToken.url, method: method, bodyParameters: bodyParameters, completionHandler: { (result, response, error) in
|
|
defer {
|
|
self.untrack(taskFor: identifier)
|
|
}
|
|
if let apiErrorType = result?["title"] as? String, let apiError = APIReadingListError(rawValue: apiErrorType), apiError != .alreadySetUp {
|
|
DDLogDebug("RLAPI FAILED: \(method.stringValue) \(path) \(apiError)")
|
|
} else {
|
|
#if DEBUG
|
|
if let error = error {
|
|
DDLogDebug("RLAPI FAILED: \(method.stringValue) \(path) \(error)")
|
|
} else {
|
|
DDLogDebug("RLAPI: \(method.stringValue) \(path)")
|
|
}
|
|
#endif
|
|
}
|
|
completion(result, response, error)
|
|
})
|
|
self.track(task: task, for: identifier)
|
|
task?.resume()
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func post(path: [String], bodyParameters: [String: Any]? = nil, completion: @escaping ([String: Any]?, URLResponse?, Error?) -> Void) {
|
|
requestWithCSRF(path: path, method: .post, bodyParameters: bodyParameters, completion: completion)
|
|
}
|
|
|
|
fileprivate func delete(path: [String], completion: @escaping ([String: Any]?, URLResponse?, Error?) -> Void) {
|
|
requestWithCSRF(path: path, method: .delete, completion: completion)
|
|
}
|
|
|
|
fileprivate func put(path: [String], bodyParameters: [String: Any]? = nil, completion: @escaping ([String: Any]?, URLResponse?, Error?) -> Void) {
|
|
requestWithCSRF(path: path, method: .put, bodyParameters: bodyParameters, completion: completion)
|
|
}
|
|
|
|
@objc func setupReadingLists(completion: @escaping (Error?) -> Void) {
|
|
let requestType = APIReadingListRequestType.setup
|
|
post(path: [requestType.rawValue]) { (result, response, error) in
|
|
self.lastRequestType = requestType
|
|
completion(error)
|
|
}
|
|
}
|
|
|
|
@objc func teardownReadingLists(completion: @escaping (Error?) -> Void) {
|
|
let requestType = APIReadingListRequestType.teardown
|
|
post(path: [requestType.rawValue]) { (result, response, error) in
|
|
self.lastRequestType = requestType
|
|
completion(error)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
Creates a new reading list using the reading list API
|
|
- parameters:
|
|
- name: The name for the new list
|
|
- description: The description for the new list
|
|
- completion: Called after the request completes
|
|
- listID: The list ID if it was created
|
|
- error: Any error preventing list creation
|
|
*/
|
|
func createList(name: String, description: String?, completion: @escaping (_ listID: Int64?,_ error: Error?) -> Swift.Void ) {
|
|
let bodyParams = ["name": name.precomposedStringWithCanonicalMapping, "description": description ?? ""]
|
|
// empty string path is required to add the trailing slash, server 404s otherwise
|
|
post(path: [""], bodyParameters: bodyParams) { (result, response, error) in
|
|
guard let id = result?["id"] as? Int64 else {
|
|
completion(nil, error ?? ReadingListError.unableToCreateList)
|
|
return
|
|
}
|
|
completion(id, nil)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Creates a new reading list using the reading list API
|
|
- parameters:
|
|
- lists: The names and descriptions for the new lists
|
|
- completion: Called after the request completes
|
|
- listIDs: The list IDs if they were created
|
|
- error: Any error preventing list creation
|
|
*/
|
|
func createLists(_ lists: [(name: String, description: String?)], completion: @escaping (_ listIDs: [(Int64?, Error?)]?,_ error: Error?) -> Swift.Void ) {
|
|
guard !lists.isEmpty else {
|
|
completion([], nil)
|
|
return
|
|
}
|
|
let bodyParams = ["batch": lists.map { ["name": $0.name.precomposedStringWithCanonicalMapping, "description": $0.description ?? ""] } ]
|
|
post(path: ["batch"], bodyParameters: bodyParams) { (result, response, error) in
|
|
guard let batch = result?["batch"] as? [[String: Any]] else {
|
|
guard lists.count > 1 else {
|
|
completion([(nil, error ?? APIReadingListError.generic)], nil)
|
|
return
|
|
}
|
|
DispatchQueue.global().async {
|
|
let taskGroup = WMFTaskGroup()
|
|
var listsByName: [String: (Int64?, Error?)] = [:]
|
|
for list in lists {
|
|
taskGroup.enter()
|
|
self.createList(name: list.name, description: list.description, completion: { (listID, error) in
|
|
taskGroup.leave()
|
|
listsByName[list.name] = (listID, error)
|
|
})
|
|
}
|
|
taskGroup.wait()
|
|
var listsOrErrors: [(Int64?, Error?)] = []
|
|
for list in lists {
|
|
guard let list = listsByName[list.name] else {
|
|
completion(nil, ReadingListError.unableToCreateList)
|
|
return
|
|
}
|
|
listsOrErrors.append(list)
|
|
}
|
|
completion(listsOrErrors, nil)
|
|
}
|
|
return
|
|
}
|
|
completion(batch.compactMap {
|
|
let id = $0["id"] as? Int64
|
|
var error: Error? = nil
|
|
if let errorString = $0["error"] as? String {
|
|
error = APIReadingListError(rawValue: errorString) ?? APIReadingListError.generic
|
|
}
|
|
return (id, error)
|
|
}, nil)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Adds a new entry to a reading list using the reading list API
|
|
- parameters:
|
|
- listID: The list ID of the list that is getting an entry
|
|
- project: The project name of the new entry
|
|
- title: The title of the new entry
|
|
- completion: Called after the request completes
|
|
- entryID: The entry ID if it was created
|
|
- error: Any error preventing entry creation
|
|
*/
|
|
func addEntryToList(withListID listID: Int64, project: String, title: String, completion: @escaping (_ entryID: Int64?,_ error: Error?) -> Swift.Void ) {
|
|
let title = title.precomposedStringWithCanonicalMapping
|
|
let project = project.precomposedStringWithCanonicalMapping
|
|
let bodyParams = ["project": project, "title": title]
|
|
// "" for trailing slash is required, server 404s otherwise
|
|
post(path: ["\(listID)", "entries", ""], bodyParameters: bodyParams) { (result, response, error) in
|
|
if let apiError = error as? APIReadingListError {
|
|
switch apiError {
|
|
case .duplicateEntry:
|
|
// TODO: Remove when error response returns ID
|
|
self.getAllEntriesForReadingListWithID(readingListID: listID, completion: { (entries, error) in
|
|
guard let entry = entries.first(where: { (entry) -> Bool in entry.title == title && entry.project == project }) else {
|
|
completion(nil, error ?? ReadingListError.unableToAddEntry)
|
|
return
|
|
}
|
|
completion(entry.id, nil)
|
|
})
|
|
default:
|
|
completion(nil, apiError)
|
|
}
|
|
return
|
|
} else if let error = error {
|
|
completion(nil, error)
|
|
return
|
|
}
|
|
|
|
guard let id = result?["id"] as? Int64 else {
|
|
completion(nil, ReadingListError.unableToAddEntry)
|
|
return
|
|
}
|
|
|
|
completion(id, nil)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Adds a new entry to a reading list using the reading list API
|
|
- parameters:
|
|
- listID: The list ID of the list that is getting an entry
|
|
- entries: The project and titles for each new entry
|
|
- completion: Called after the request completes
|
|
- entryIDs: The entry IDs if they were created
|
|
- error: Any error preventing entry creation
|
|
*/
|
|
func addEntriesToList(withListID listID: Int64, entries: [(project: String, title: String)], completion: @escaping (_ entryIDs: [(Int64?, Error?)]?,_ error: Error?) -> Swift.Void ) {
|
|
guard !entries.isEmpty else {
|
|
completion([], nil)
|
|
return
|
|
}
|
|
let bodyParams = ["batch": entries.map { ["project": $0.project.precomposedStringWithCanonicalMapping, "title": $0.title.precomposedStringWithCanonicalMapping] } ]
|
|
post(path: ["\(listID)", "entries", "batch"], bodyParameters: bodyParams) { (result, response, error) in
|
|
if let apiError = error as? APIReadingListError, apiError != .listDeleted {
|
|
guard entries.count > 1 else {
|
|
completion([(nil, apiError)], nil)
|
|
return
|
|
}
|
|
self.getAllEntriesForReadingListWithID(readingListID: listID, completion: { (remoteEntries, getAllEntriesError) in
|
|
var remoteEntriesByProjectAndTitle: [String: [String: APIReadingListEntry]] = [:]
|
|
for remoteEntry in remoteEntries {
|
|
remoteEntriesByProjectAndTitle[remoteEntry.project.precomposedStringWithCanonicalMapping, default: [:]][remoteEntry.title.precomposedStringWithCanonicalMapping] = remoteEntry
|
|
}
|
|
let results: [(Int64?, Error?)] = entries.map {
|
|
let project = $0.project.precomposedStringWithCanonicalMapping
|
|
let title = $0.title.precomposedStringWithCanonicalMapping
|
|
guard let remoteEntry = remoteEntriesByProjectAndTitle[project]?[title] else {
|
|
return (nil, apiError == .entryLimit ? apiError : APIReadingListError.generic)
|
|
}
|
|
return (remoteEntry.id, nil)
|
|
}
|
|
completion(results, nil)
|
|
})
|
|
return
|
|
} else if let error = error {
|
|
completion(nil, error)
|
|
return
|
|
}
|
|
|
|
guard let result = result else {
|
|
completion(nil, ReadingListError.unableToAddEntry)
|
|
return
|
|
}
|
|
|
|
guard let batch = result["batch"] as? [[String: Any]] else {
|
|
DDLogError("Unexpected result: \(result)")
|
|
completion(nil, ReadingListError.unableToAddEntry)
|
|
return
|
|
}
|
|
|
|
completion(batch.compactMap {
|
|
let id = $0["id"] as? Int64
|
|
var error: Error? = nil
|
|
if let errorString = $0["error"] as? String {
|
|
error = APIReadingListError(rawValue: errorString) ?? APIReadingListError.generic
|
|
}
|
|
return (id, error)
|
|
}, nil)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
Remove entry from reading list using the reading list API
|
|
- parameters:
|
|
- listID: The list ID of the list that will have an entry removed
|
|
- entryID: The entry ID to remove from the list
|
|
- completion: Called after the request completes
|
|
- error: Any error preventing entry deletion
|
|
*/
|
|
func removeEntry(withEntryID entryID: Int64, fromListWithListID listID: Int64, completion: @escaping (_ error: Error?) -> Swift.Void ) {
|
|
|
|
delete(path: ["\(listID)", "entries", "\(entryID)"]) { (result, response, error) in
|
|
guard error == nil else {
|
|
completion(error ?? ReadingListError.unableToRemoveEntry)
|
|
return
|
|
}
|
|
completion(nil)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Deletes a reading list using the reading list API
|
|
- parameters:
|
|
- listID: The list ID of the list to delete
|
|
- completion: Called after the request completes
|
|
- error: Any error preventing list deletion
|
|
*/
|
|
func deleteList(withListID listID: Int64, completion: @escaping (_ error: Error?) -> Swift.Void ) {
|
|
delete(path: ["\(listID)"]) { (result, response, error) in
|
|
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
|
completion(error ?? ReadingListError.unableToDeleteList)
|
|
return
|
|
}
|
|
completion(nil)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Updates a reading list using the reading list API
|
|
- parameters:
|
|
- listID: The list ID of the list to update
|
|
- name: The name of the list
|
|
- description: The description of the list
|
|
- completion: Called after the request completes
|
|
- error: Any error preventing list update
|
|
*/
|
|
func updateList(withListID listID: Int64, name: String, description: String?, completion: @escaping (_ error: Error?) -> Swift.Void ) {
|
|
put(path: ["\(listID)"], bodyParameters: ["name": name, "description": description ?? ""]) { (result, response, error) in
|
|
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
|
completion(error ?? ReadingListError.unableToDeleteList)
|
|
return
|
|
}
|
|
completion(nil)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
Gets updated lists and entries list API
|
|
- parameters:
|
|
- since: The continuation token for this whole list of updates. Lets the server know the current state of the device. Currently an ISO 8601 date string
|
|
- next: The continuation within this whole list of updates (since is the start of the whole list, next is the next page)
|
|
- nextSince: The paramater to use for "since" the next time you call this method to get the updates that have happened since this update.
|
|
- lists: Lists to append to the results
|
|
- entries: Entries to append to the results
|
|
- lists: All updated lists
|
|
- entries: All updated entries
|
|
- since: The date to use for the next update call
|
|
- error: Any error
|
|
*/
|
|
func updatedListsAndEntries(since: String, next: String? = nil, nextSince: String? = nil, lists: [APIReadingList] = [], entries: [APIReadingListEntry] = [], completion: @escaping (_ lists: [APIReadingList], _ entries: [APIReadingListEntry], _ since: String?, _ error: Error?) -> Swift.Void ) {
|
|
var queryParameters: [String: Any]? = nil
|
|
if let next = next {
|
|
queryParameters = ["next": next]
|
|
}
|
|
get(path: ["changes", "since", "\(since)"], queryParameters: queryParameters) { (result: APIReadingListChanges?, response, error) in
|
|
guard let result = result, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
|
completion([], [], nil, error ?? ReadingListError.generic)
|
|
return
|
|
}
|
|
var combinedLists = lists
|
|
if let lists = result.lists {
|
|
combinedLists.append(contentsOf: lists)
|
|
}
|
|
var combinedEntries = entries
|
|
if let entries = result.entries {
|
|
combinedEntries.append(contentsOf: entries)
|
|
}
|
|
let nextSince = nextSince ?? result.since
|
|
if let next = result.next {
|
|
if combinedLists.count + combinedEntries.count > APIReadingListUpdateLimitForFullSyncFallback {
|
|
completion([], [], nil, APIReadingListError.needsFullSync)
|
|
} else {
|
|
self.updatedListsAndEntries(since: since, next: next, nextSince: nextSince, lists: combinedLists, entries: combinedEntries, completion: completion)
|
|
}
|
|
} else {
|
|
completion(combinedLists, combinedEntries, nextSince, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Gets all reading lists from the API
|
|
- parameters:
|
|
- next: Optional continuation token for this list of results
|
|
- lists: Lists to append to the results
|
|
- lists: All lists
|
|
- since: The string to use for the next /changes/since call
|
|
- error: Any error
|
|
*/
|
|
public func getAllReadingLists(next: String? = nil, nextSince: String? = nil, lists: [APIReadingList] = [], completion: @escaping ([APIReadingList], String?, Error?) -> Swift.Void ) {
|
|
var queryParameters: [String: Any]? = nil
|
|
if let next = next {
|
|
queryParameters = ["next": next]
|
|
}
|
|
// empty string path is required to add the trailing slash, server 404s otherwise
|
|
get(path: [""], queryParameters: queryParameters) { (apiListsResponse: APIReadingLists?, response, error) in
|
|
guard let apiListsResponse = apiListsResponse else {
|
|
completion([], nil, error)
|
|
return
|
|
}
|
|
var combinedList = lists
|
|
combinedList.append(contentsOf: apiListsResponse.lists)
|
|
let nextSince = nextSince ?? apiListsResponse.since
|
|
if let next = apiListsResponse.next {
|
|
self.getAllReadingLists(next: next, nextSince: nextSince, lists: combinedList, completion: completion)
|
|
} else {
|
|
completion(combinedList, nextSince, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func getAllEntriesForReadingListWithID(next: String? = nil, entries: [APIReadingListEntry] = [], readingListID: Int64, completion: @escaping ([APIReadingListEntry], Error?) -> Swift.Void ) {
|
|
var queryParameters: [String: Any]? = nil
|
|
if let next = next {
|
|
queryParameters = ["next": next]
|
|
}
|
|
// "" for trailing slash is required, server 404s otherwise
|
|
get(path: ["\(readingListID)", "entries", ""], queryParameters: queryParameters) { (apiEntriesResponse: APIReadingListEntries?, response, error) in
|
|
guard let apiEntriesResponse = apiEntriesResponse else {
|
|
completion([], error)
|
|
return
|
|
}
|
|
var combinedList = entries
|
|
combinedList.append(contentsOf: apiEntriesResponse.entries)
|
|
if let next = apiEntriesResponse.next {
|
|
self.getAllEntriesForReadingListWithID(next: next, entries: combinedList, readingListID: readingListID, completion: completion)
|
|
} else {
|
|
completion(combinedList, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|