Subsequent ordered HTTP calls - swift

I'm building a simple iOS client for HackerNews. I'm using their APIs, according to which I'll be able to get the ordered post IDs (sorted by new, best and top) and a single post item passing the ID to the request. The problem I'm facing is the following: how can I, once I get the IDs array, make an HTTP call for every post in an ordered fashion? With the way I currently implemented it, I'm not having any luck.
E.g. say the IDs array is [3001, 3002, 3003, 3004]. I tried calling the method to get those posts inside a for loop issuing dispatch groups and dispatch semaphores, but I still get them unordered, like the call for item 3003 completes before 3002, and so on.
The methods I'm using:
#Published var posts: [Post] = []
func getPosts(feedType: FeedType){
posts = []
self.getFeedIDs(feedType: feedType).subscribe{ ids in
let firstFifteen = ids[0...15]
let dGroup = DispatchGroup()
let dQueue = DispatchQueue(label: "network-queue")
let dSemaphore = DispatchSemaphore(value: 0)
dQueue.async {
for id in firstFifteen{
dGroup.enter()
self.getPost(id: id).subscribe{ post in
self.posts.append(post)
dSemaphore.signal()
dGroup.leave()
}
dSemaphore.wait()
}
}
}
}
func getFeedIDs(feedType: FeedType) -> Observable<[Int]> {
return self.execute(url: URL(string: "https://hacker-news.firebaseio.com/v0/\(feedType)stories.json")!)
}
func getPost(id: Int) -> Observable<Post>{
return self.execute(url: URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")!)
}
func execute <T: Decodable>(url: URL) -> Observable<T> {
return Observable.create { observer -> Disposable in
let task = URLSession.shared.dataTask(with: url) { res, _, _ in
guard let data = res, let decoded = try? JSONDecoder().decode(T.self, from: data) else {
return
}
observer.onNext(decoded)
observer.onCompleted()
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
Any help would be greatly appreciated.

The semaphore makes no sense and is inefficient anyway.
Use the same pattern which Apple suggests in conjunction with TaskGroups: Collect the data in a dictionary and after being notified sort the data by the dictionary keys
func getPosts(feedType: FeedType){
var postData = [Int:Post]()
posts = []
self.getFeedIDs(feedType: feedType).subscribe{ ids in
let firstFifteen = ids[0...15]
let dGroup = DispatchGroup()
for (index, element) in firstFifteen.enumerated() {
dGroup.enter()
self.getPost(id: element).subscribe{ post in
postData[index] = post
dGroup.leave()
}
}
dGroup.notify(queue: .main) {
for key in postData.keys.sorted() {
posts.append(postData[key]!)
}
}
}
}

Related

Swift Test case Falling with Expectation

I am trying to run test case for Failure response . I have an empty json file into project and named it FailureResponse . This file is empty . I trying to count the number of array is empty for example ..
XCTAssertTrue(schools.count==0)
It should pass the test because the json file is empty .
same result fields like school name and School location etc but the problem is it showing error ..
testFailure(): Asynchronous wait failed: Exceeded timeout of 6 seconds, with unfulfilled expectations: "waiting for response".
View Model code...
import Foundation
import Combine
class ViewModel {
private let networkManager = NetworkManager()
#Published private(set) var school = [School]()
func getSchools() {
loadMoreSchools()
}
func loadMoreSchools() {
let newURL = NetworkURLs.baseURL
networkManager
.getModel([School].self, from: newURL) { [weak self] result in
switch result {
case .success(let schoolResponse):
self?.school = schoolResponse
print(schoolResponse)
case .failure(let error):
print(error)
}
}
}
func getSchoolName(by row: Int) -> String {
let schoolName = school[row]
return schoolName.schoolName.uppercased()
}
func getSchoolLocation(by row: Int) -> String {
return "\(school[row].location)"
}
}
Here is my Mock service call ..
class MockService: NetworkManagerProtocol {
var data: Data?
func getModel<Model>(_ type: Model.Type, from url: String, completion: #escaping (Result<Model, Alomafire_Project.NetworkError>) -> ()) where Model : Decodable, Model : Encodable {
if let data = data {
do {
let result = try JSONDecoder().decode(type, from: data)
completion(.success(result))
} catch (let error){
print(error)
}
}
}
}
Here is code for call the local Jason ..
func getData(json: String) throws -> Data {
guard let url = Bundle(for: Alomafire_ProjectTests.self).url(forResource: json, withExtension: "json")
else { return Data() }
return try Data(contentsOf: url)
}
Here is the test case ....
func testFailure() throws {
// Given
mockService.data = try getData(json: "FailureResponse")
var schools: [School] = []
let expectation = expectation(description: "waiting for response")
// When
viewModel?
.$school
.dropFirst()
.sink(receiveValue: { result in
schools = result
expectation.fulfill()
})
.store(in: &subscribers)
// viewModel?.getSchools()
// Then
waitForExpectations(timeout: 10.0)
XCTAssertTrue(schools.count==0)
}
Here is the debug result . it return 0 ..
Here is the screenshot of the result ..
You mention in the question that "the json file is empty." If that is the case, then this test will fail. The MockService assumes that the Data pulled from the json file will be decodable to the type requested. If it isn't the getModel(_:from:completion:) will never call the completion and the test will not complete in the specified time limit. Solve this by calling the completion closure even when the JSONDecoder response with an error.
Also, even if that mock emits the error properly, your ViewModel doesn't do anything with it that would cause the schools type to update.

Nil on data request, problem with handling requests from REST API

I'm trying to inflate my layout with data, it works but on some attempts user filed or comments field just have still nil and it fails. How can I can optimise the requests? I'm still beginner with swift and I cannot wrap my had around this.
The class that fails is the findUserById which does just what it is in the name. So find the user for specific id from the users list. But sometimes users are not yet initalized and the error with force unwraping is visible
func findUserByUserId(UserId:Int) -> User{
var userPlaceholder : User!
for user in self.users {
if(user.id == UserId){
userPlaceholder = user
}
}
return userPlaceholder
}
This is the function that I'm using for setting up the structure of the table cell
private func fetchPost() {
self.posts.forEach { (post) in
DispatchQueue.global(qos: .userInteractive).async(group: dispatchGroup) {
self.dispatchGroup.enter()
self.postsCellViewModels.append(PostsCellViewModel(post: post, user: self.findUserByUserId(UserId: post.userId), comments: self.findCommensByPostId(PostId: post.id)))
self.dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
os_log("PostsViewModel -> Finished fetching posts")
self.isLoading = false
self.reloadData?()
}
And this is function for fetchingUsers from API, i have the same for posts and comments, data is taken from JSONPlaceholderAPI
func fetchUsers(){
dispatchGroup.enter()
os_log("PostsViewModel -> Starting users fetching")
if let client = client as? JSONPlaceholderClient {
self.isLoading = true
let endpoint = JsonPlaceHolderEndpoint.users
client.fetchUsers(with: endpoint) { (either) in
switch either {
case .success(let users):
self.users = users
os_log("PostsViewModel -> Ended users fetching")
self.dispatchGroup.leave()
case .error(let error):
self.showError?(error)
}
}
}
}

SWIFTUI Firebase Retrieving Subcollection Data

I do understand, that all request from firebase are async.
I have collection tasksCategory -> document -> subcollection tasks
This is my class for getting all created tasks category, there is no problem. Problem is that I need to retrieve all tasks for each category by passing document ID.
class fsTasks: ObservableObject {
#Published var categories = [fsTaskCategory]()
init() {
fsGetTaskCategories()
}
/// Retrieve Tasks Categories For Logged In User
func fsGetTaskCategories() {
db.collection("tasksCategories").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.categories = documents.compactMap { queryDocumentSnapshot -> fsTaskCategory? in
return try? queryDocumentSnapshot.data(as: fsTaskCategory.self)
}
}
}
}
I have create another function to retrieve all tasks for each passed document ID
func fsGetTasks(documentID: String, completation: #escaping([fsTask]) -> Void) {
var tasks = [fsTask]()
db.collection("tasksCategories").document(documentID).collection("tasks").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
tasks = documents.compactMap { queryDocumentSnapshot -> fsTask? in
return try? queryDocumentSnapshot.data(as: fsTask.self)
}
completation(tasks)
}
}
Problem is that I do not have any idea, how can I call this function directly in the view of SWIFTUI.
Basically I have first ForEach through the ObservedObject of all categories, then I need to do another foreach for all tasks for each category, but first I need to retrieve data. I need function which return an array with all tasks retrieved from firebase but only when completation handler return data.
If I have function like this
func retrieveAllTasks(categoryID: String) -> [fsTasks] {
var fetchedTasks = [fsTasks]()
fsGetTasks(documentID: categoryID, completation: { (tasks) in
fetcheTasks = tasks
})
return fetchedTasks
}
I was still retrieving only empty array.
This is the issue
func retrieveAllTasks(categoryID: String) -> [fsTasks] {
var fetchedTasks = [fsTasks]()
fsGetTasks(documentID: categoryID, completation: { (tasks) in
fetcheTasks = tasks
})
return fetchedTasks
}
This is an asynchronous function as well (see the closure) and you have to give Firebase time to retrieve the data from the server and handle it within the Firebase closure.
What's happening here is that while you are doing that within the Firebase closure itself, that's not happening within this closure. return fetchedTasks is returning before fetchedTasks = tasks.
I would call the firebase function directly since it doesn't appear you need the middleman retrieveAllTasks function
self.fsGetTasks(documentID: "some_doc", completion: { taskArray in
for task in taskArray {
print(task
}
})
If you do, you need to add an #escaping clause to that as well and not use return fetchedTasks

Apple Combine framework: How to execute multiple Publishers in parallel and wait for all of them to finish?

I am discovering Combine. I wrote methods that make HTTP requests in a "combine" way, for example:
func testRawDataTaskPublisher(for url: URL) -> AnyPublisher<Data, Error> {
var request = URLRequest(url: url,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 15)
request.httpMethod = "GET"
return urlSession.dataTaskPublisher(for: request)
.tryMap {
return $0.data
}
.eraseToAnyPublisher()
}
I would like to call the method multiple times and do a task after all, for example:
let myURLs: [URL] = ...
for url in myURLs {
let cancellable = testRawDataTaskPublisher(for: url)
.sink(receiveCompletion: { _ in }) { data in
// save the data...
}
}
The code above won't work because I have to store the cancellable in a variable that belongs to the class.
The first question is: is it a good idea to store many (for example 1000) cancellables in something like Set<AnyCancellable>??? Won't it cause memory leaks?
var cancellables = Set<AnyCancellable>()
...
let cancellable = ...
cancellables.insert(cancellable) // ???
And the second question is: how to start a task when all the cancellables are finished? I was thinking about something like that
class Test {
var cancellables = Set<AnyCancellable>()
func run() {
// show a loader
let cancellable = runDownloads()
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in }) { _ in
// hide the loader
}
cancellables.insert(cancellable)
}
func runDownloads() -> AnyPublisher<Bool, Error> {
let myURLs: [URL] = ...
return Future<Bool, Error> { promise in
let numberOfURLs = myURLS.count
var numberOfFinishedTasks = 0
for url in myURLs {
let cancellable = testRawDataTaskPublisher(for: url)
.sink(receiveCompletion: { _ in }) { data in
// save the data...
numberOfFinishedTasks += 1
if numberOfFinishedTasks >= numberOfURLs {
promise(.success(true))
}
}
cancellables.insert(cancellable)
}
}.eraseToAnyPublisher()
}
func testRawDataTaskPublisher(for url: URL) -> AnyPublisher<Data, Error> {
...
}
}
Normally I would use DispatchGroup, start multiple HTTP tasks and consume the notification when the tasks are finished, but I am wondering how to write that in a modern way using Combine.
You can run some operations in parallel by creating a collection of publishers, applying the flatMap operator and then collect to wait for all of the publishers to complete before continuing. Here's an example that you can run in a playground:
import Combine
import Foundation
func delayedPublisher<Value>(_ value: Value, delay after: Double) -> AnyPublisher<Value, Never> {
let p = PassthroughSubject<Value, Never>()
DispatchQueue.main.asyncAfter(deadline: .now() + after) {
p.send(value)
p.send(completion: .finished)
}
return p.eraseToAnyPublisher()
}
let myPublishers = [1,2,3]
.map{ delayedPublisher($0, delay: 1 / Double($0)).print("\($0)").eraseToAnyPublisher() }
let cancel = myPublishers
.publisher
.flatMap { $0 }
.collect()
.sink { result in
print("result:", result)
}
Here is the output:
1: receive subscription: (PassthroughSubject)
1: request unlimited
2: receive subscription: (PassthroughSubject)
2: request unlimited
3: receive subscription: (PassthroughSubject)
3: request unlimited
3: receive value: (3)
3: receive finished
2: receive value: (2)
2: receive finished
1: receive value: (1)
1: receive finished
result: [3, 2, 1]
Notice that the publishers are all immediately started (in their original order).
The 1 / $0 delay causes the first publisher to take the longest to complete. Notice the order of the values at the end. Since the first took the longest to complete, it is the last item.

Loop over Publisher Combine framework

I have the following function to perform an URL request:
final class ServiceManagerImpl: ServiceManager, ObservableObject {
private let session = URLSession.shared
func performRequest<T>(_ request: T) -> AnyPublisher<String?, APIError> where T : Request {
session.dataTaskPublisher(for: self.urlRequest(request))
.tryMap { data, response in
try self.validateResponse(response)
return String(data: data, encoding: .utf8)
}
.mapError { error in
return self.transformError(error)
}
.eraseToAnyPublisher()
}
}
Having these 2 following functions, I can now call the desired requests from corresponded ViewModel:
final class AuditServiceImpl: AuditService {
private let serviceManager: ServiceManager = ServiceManagerImpl()
func emptyAction() -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "", nonce: String.randomNumberGenerator)
return serviceManager.performRequest(request)
}
func burbleAction(offset: Int) -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "burble", nonce: String.randomNumberGenerator, offset: offset)
return serviceManager.performRequest(request)
}
}
final class AuditViewModel: ObservableObject {
#Published var auditLog: String = ""
private let auditService: AuditService = AuditServiceImpl()
init() {
let timer = Timer(timeInterval: 5, repeats: true) { _ in
self.getBurbles()
}
RunLoop.main.add(timer, forMode: .common)
}
func getBurbles() {
auditService.emptyAction()
.flatMap { [unowned self] offset -> AnyPublisher<String?, APIError> in
let currentOffset = Int(offset?.unwrapped ?? "") ?? 0
return self.auditService.burbleAction(offset: currentOffset)
}
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [unowned self] completion in
print(completion)
}, receiveValue: { [weak self] burbles in
self?.auditLog = burbles!
})
.store(in: &cancellableSet)
}
}
Everything is fine when I use self.getBurbles() for the first time. However, for the next calls, print(completion) shows finished, and the code doesn't perform self?.auditLog = burbles!
I don't know how can I loop over the getBurbles() function and get the response at different intervals.
Edit
The whole process in a nutshell:
I call getBurbles() from class initializer
getBurbles() calls 2 nested functions: emptyAction() and burbleAction(offset: Int)
Those 2 functions generate different requests and call performRequest<T>(_ request: T)
Finally, I set the response into auditLog variable and show it on the SwiftUI layer
There are at least 2 issues here.
First when a Publisher errors it will never produce elements again. That's a problem here because you want to recycle the Publisher here and call it many times, even if the inner Publisher fails. You need to handle the error inside the flatMap and make sure it doesn't propagate to the enclosing Publisher. (ie you can return a Result or some other enum or tuple that indicates you should display an error state).
Second, flatMap is almost certainly not what you want here since it will merge all of the api calls and return them in arbitrary order. If you want to cancel any existing requests and only show the latest results then you should use .map followed by switchToLatest.