RxSwift and three API requests - ios

I want to fetch a data from three different APIs, and then save it in a database. Therefore, data from each query should be separated after the operation.
How to do it with RxSwift? Zip? In my example I'm using only two URLs, but it's just example.
QueryService:
import Foundation
import RxSwift
class QueryService {
let albumsURL = URL(string: "https://jsonplaceholder.typicode.com/albums")!
func fetchAlbums() -> Observable<[Album]> {
return Observable.create { observer -> Disposable in
let task = URLSession.shared.dataTask(with: self.albumsURL) { data, _, _ in
guard let data = data else {
observer.onError(NSError(domain: "", code: -1, userInfo: nil))
return
}
do {
let albums = try JSONDecoder().decode([Album].self, from: data)
observer.onNext(albums)
} catch {
observer.onError(error)
}
}
task.resume()
return Disposables.create{
task.cancel()
}
}
}
func fetchUsers() -> Observable<[User]> {
return Observable.create { observer -> Disposable in
let task = URLSession.shared.dataTask(with: URL(string: "https://jsonplaceholder.typicode.com/users")!) { data, _, _ in
guard let data = data else {
observer.onError(NSError(domain: "", code: -1, userInfo: nil))
return
}
do {
let users = try JSONDecoder().decode([User].self, from: data)
observer.onNext(users)
} catch {
observer.onError(error)
}
}
task.resume()
return Disposables.create{
task.cancel()
}
}
}
}

I'm assuming you're trying to execute two tasks in parallel and want to reach their results from a single observable.
If this is the case, your code should be like this:
Observable.zip(service.fetchAlbums(), service.fetchUsers())
.subscribe(onNext: { (albums, users) in
print(albums)
print(users)
})
.disposed(by: self.disposeBag)

Related

How do I get multiple JSON objects from api call?

I am trying to make an API call to the GitLab API to get the projects that are available to a particular user.
I can get one project of an index of my choosing, put it into a ProjectModel with the projectId and the projectName but I can not figure out how to get all of them into an array of ProjectModels.
By printing then I can see them all being printed in the console but it will not let me append them to an array.
It is in the parseJSON function that I am trying to get a hold of all of the projects.
Does anyone have any suggestions?
This is my manager to fetch the projects:
protocol FetchProjectsManagerDelegate {
func didUpdateProjects(_ fetchProjectsManager: FetchProjectsManager, project: ProjectModel?)
func didFailWithError(error: Error)
}
struct FetchProjectsManager {
let projectsURL = "secret"
var delegate: FetchProjectsManagerDelegate?
func fetchProjects(privateToken: String) {
let privateTokenString = "\(projectsURL)projects?private_token=\(privateToken)"
performRequest(with: privateTokenString)
}
func performRequest(with privateTokenString: String) {
// Create url
if let url = URL(string: privateTokenString) {
// Create URLSession
let session = URLSession(configuration: .default)
// Give the session a task
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}
if let safeData = data {
if let project = self.parseJSON(safeData) {
self.delegate?.didUpdateProjects(self, project: project)
}
}
}
// Start the task
task.resume()
}
}
func parseJSON(_ projectData: Data) -> ProjectModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode([Project].self, from: projectData)
for project in decodedData {
print(project)
}
let projectId = decodedData[0].id
let projectName = decodedData[0].name
let project = ProjectModel(projectId: projectId, projectName: projectName)
return project
} catch {
delegate?.didFailWithError(error: error)
return nil
}
}
}
This is my project model
struct ProjectModel {
let projectId: Int
let projectName: String
}
Your parseJson method only returns a single project instead of all of them, change it to
func parseJSON(_ projectData: Data) -> [ProjectModel]? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode([Project].self, from: projectData)
let projects = decodedData.map { ProjectModel(projectId: $0.id,
projectName: $0.name) }
return projects
} catch {
delegate?.didFailWithError(error: error)
return nil
}
}
and you of course need to update didUpdateProjects so that it takes an array of ProjectModel or call it in a loop

RXSwift: Subscriber never gets call back

I have this function:
func makeRepoRequest() -> Single<[String: Any]> {
return Single<[String: Any]>.create {[weak self] observer in
guard let something = self?.temp else {
let disposeBag = DisposeBag()
self?.getRepo("364").subscribe(onSuccess: { content in
observer(.success(content))
}, onError: { error in
observer(.error(error))
}).disposed(by: disposeBag)
return Disposables.create()
}
observer(.success(something))
return Disposables.create()
}
}
is subscribe to this function:
func getRepo(_ repo: String) -> Single<[String: Any]> {
return Single<[String: Any]>.create { single in
print(repo)
let url = "https://api.github.com/repositories?since=\(repo)"
print(url)
let task = URLSession.shared.dataTask(with: URL(string:url)!) { data, _, error in
if let error = error {
single(.error(error))
return
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []),
let result = json as? [String: Any] else {
let error = NSError(domain: "Decoding", code: 0, userInfo: nil)
single(.error(error))
return
}
single(.success(result))
}
task.resume()
return Disposables.create()
}
}
but for some reason the subscription it never gets a call back. Any of you knows why the subscription never gets a call back?
I'll really appreciate your help.
Your makeRepoRequest() is defined incorrectly. The disposable you create inside the closure should be the one that you return. There shouldn't be any disposeBag in there, also you need to unwrap self and make sure something is emitted if self doesn't exist, also if you are going to keep a cache in temp you really should assign to it...
func makeRepoRequest() -> Single<[String: Any]> {
return Single<[String: Any]>.create { [weak self] observer in
guard let this = self else {
observer(.error(MyError.missingSelf))
return Disposables.create()
}
guard !this.temp.isEmpty else {
return this.getRepo("364").subscribe(onSuccess: { content in
this.temp = content
observer(.success(content))
}, onError: { error in
observer(.error(error))
})
}
observer(.success(this.temp))
return Disposables.create()
}
}
However, since you are just emitting content with no changes, you don't even need to use .create(_:). So something like this:
func makeRepoRequest() -> Single<[String: Any]> {
if !temp.isEmpty {
return getRepo("364")
.do(onSuccess: { [weak self] in self?.temp = $0 })
}
else {
return Single.just(temp)
}
}
Lastly, you aren't properly canceling your network request in your getRepo(_:) method. It should end with return Disposables.create { task.cancel() }
I suggest you read up more on Disposables.

How to get an array from URLSession

Trying to make a program for a news site. I take information from the site through the api, everything works fine.
The only question is, how do I get this array out of the loop?
Here is my code:
import UIKit
class ViewController: UIViewController {
var news:[News] = []
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
func getUsers() {
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
news = try JSONDecoder().decode([News].self, from: data)
// print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
}
struct News:Codable, CustomStringConvertible{
let href:String?
let site:String?
let title:String?
let time:String?
var description: String {
return "(href:- \(href), site:- \(site), title:- \(title), time:- \(time))"
}
}
Declare news array in your class and assign the response to this array in getUsers method
var news:[News] = []
func getUsers(){
guard let url = URL(string: "https") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
self.news = try JSONDecoder().decode([News].self, from: data)
print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
The fundamental problem is you are retrieving data asynchronously (e.g. getUsers will initiate a relatively slow request from the network using URLSession, but returns immediately). Thus this won’t work:
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
You are returning from getUsers before the news has been retrieved. So news will still be [].
The solution is to give getUsers a “completion handler”, a parameter where you can specify what code should be performed when the asynchronous request is done:
enum NewsError: Error {
case invalidURL
case invalidResponse(URLResponse?)
}
func getUsers(completion: #escaping (Result<[News], Error>) -> Void) {
let queue = DispatchQueue.main
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {
queue.async { completion(.failure(NewsError.invalidURL)) }
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
queue.async { completion(.failure(error)) }
return
}
guard
let data = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
queue.async { completion(.failure(NewsError.invalidResponse(response))) }
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let news = try decoder.decode([News].self, from: data)
queue.async { completion(.success(news)) }
} catch let parseError {
queue.async { completion(.failure(parseError)) }
}
}.resume()
}
Then your view controller can fetch the news, passing a “closure”, i.e. code that says what to do when the asynchronous call is complete. In this case, it will set self.news and trigger the necessary UI update (e.g. maybe refresh tableview):
class ViewController: UIViewController {
var news: [News] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchNews()
}
func fetchNews() {
getUsers() { result in
switch result {
case .failure(let error):
print(error)
case .success(let news):
self.news = news
print(news)
}
// trigger whatever UI update you want here, e.g., if using a table view:
//
// self.tableView.reloadData()
}
// but don't try to print the news here, as it hasn't been retrieved yet
// print(news)
}

How to return single.deferred in closure

What am I doing wrong? How do I get the Single from the closure?
How can I get value from Alamofire.request?
func loadImageName(url: URL) -> Single<String> {
let data = try? Data(contentsOf: url)
guard let query = data else {
return .error(Error.notFound)
}
...
return Single.deferred {
Alamofire.request(request).responseJSON {
if let error = $0.error {
return Single<String>.error(error)
}
return Single<String>.just(result)
}
}
}
In this case you'll want Single.create.
This takes a block, which has a single argument. This argument is a function that you can call to pass a SingleEvent, i.e. either a .success(Element) (where Element is String in your case) or an .error.
The block must return a Disposable. You can construct a Disposable that will do additional work when it gets disposed. In the context of network requests, it makes sense to cancel the request.
func loadImageName(url: URL) -> Single<String> {
let data = try? Data(contentsOf: url)
guard let query = data else {
return .error(Error.notFound)
}
// ...
Single.create { single in
let request = Alamofire.request(request).responseJSON {
if let error = $0.error {
return single(.error(error))
}
return single(.success(result))
}
return Disposables.create { request.cancel() }
}
}

Completion block for nested requests

I am trying to build methods with completion blocks for nested requests. The issue is that a completion block catches to early for parent requests (meaning that the child requests haven't actually completed yet). So far I haven't found a way for a child request to communicate back to the parent request other than what I've done in my example below (which is to count the amount of child requests have completed and compare it against the expected amount of requests).
The example below is working against a Firestore database. Imagine a user has multiple card games (decks) with each multiple cards. I'm grateful for any help how to build better completion blocks for cases like these:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var decksCount = Int()
var cardsCount = Int()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
decksCount += 1
cardsCount += cardSnapshot.count
if decksCount == deckSnapshot.count {
completion(cardsCount)
}
}
})
})
}
}
}
Here is the solution, using DispatchGroup, found with #meggar's help in the comments:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var cardsCount = Int()
let group = DispatchGroup()
group.enter()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
group.enter()
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
cardsCount += cardSnapshot.count
}
group.leave()
})
})
}
group.leave()
}
group.notify(queue: .main) {
completion(cardsCount)
}
}

Resources