How to get value out of URLSession - swift

I keep coming back to the same problem. For other parts of my app I used coredata directly after parsing the API and that works fine. However, now I want to parse a JSON received through an API and just get one value that I use to calculate other values which I then put in Coredata.
Everything works fine and I have set up the URLSessions code as follows:
func fetchData(brand: String, completion: #escaping ((Double) -> Void)) {
let urlString = "\(quoteUrl)\(brand)"
if let url = URL(string: urlString) {
var session = URLRequest(url: url)
session.addValue("application/json", forHTTPHeaderField: "Accept")
session.addValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: session) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(DataModel.self, from: safeData)
let bid = decodedData.quotes.quote.bid
let ask = decodedData.quotes.quote.ask
let itemPrice: Double = (bid + ask)/2
completion(itemPrice)
} catch {
print(error)
}
}
}
task.resume()
}
}
I am using the completionHandler to retrieve the part I need which I use in another file as follows:
func getGainLossNumber(brand: String, quantity: Int, price: Double) -> Double {
var finalPrice = 0.0
APImodel.fetchData(brand: brand) { returnedDouble in
let currentPrice = returnedDouble
if quantity < 0 {
let orderQuantity = quantity * -1
finalPrice = price + (currentPrice*(Double(orderQuantity))*100)
} else {
finalPrice = price - (currentPrice*(Double(quantity))*100)
}
}
return finalPrice
}
finalPrice eventually returns 0.0. If I print currentPrice in the closure I do get the correct result. I used the completion handler in order to retrieve a number from the API because of the issues I was facing but it stil is not doing what I would like to have. The second function should return the value that was calculated using the value I got from the API that I retrieved with the completion handler.
I just can't figure out how to do it.

The problem is that you are calculating finalPrice inside a closure, which is asynchronous. Your getGainLossNumber method however, is synchronous, so it actually returns before your closure is finished calculating finalPrice. Restructure your code so that getGainLossNumber takes a closure as a parameter, and invokes it once finalPrice has been calculated. Something like:
func getGainLossNumber(brand: String, quantity: Int, price: Double, _ completion: #escaping (Double) -> Void) {
APImodel.fetchData(brand: brand) { returnedDouble in
let currentPrice = returnedDouble
let finalPrice: Double
if quantity < 0 {
let orderQuantity = quantity * -1
finalPrice = price + (currentPrice*(Double(orderQuantity))*100)
}
else {
finalPrice = price - (currentPrice*(Double(quantity))*100)
}
completion(finalPrice)
}
}
Also note, that finalPrice does not need to be var as it will be assigned a value only once.
EDIT
Usage:
getGainLossNumber(brand: "brand", quantity: 1, price: 120, { finalPrice in
// You can access/use finalPrice in here.
}

Related

Subsequent ordered HTTP calls

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]!)
}
}
}
}

Swift Completion Handler For Loop to be performed once instead of 10 times due to the loop

I I have a loop with a firestore query in it that is repeated 10 times. I need to call the (completion: block) after all the 10 queries completed; Here I have my code so that it performs the (completion: block) per query but this would be too heavy on the server and the user's phone. How can I change below to accomplish what I just described?
static func getSearchedProducts(fetchingNumberToStart: Int, sortedProducts: [Int : [String : Int]], handler: #escaping (_ products: [Product], _ lastFetchedNumber: Int?) -> Void) {
var lastFetchedNumber:Int = 0
var searchedProducts:[Product] = []
let db = Firestore.firestore()
let block : FIRQuerySnapshotBlock = ({ (snap, error) in
guard error == nil, let snapshot = snap else {
debugPrint(error?.localizedDescription)
return
}
var products = snapshot.documents.map { Product(data: $0.data()) }
if !UserService.current.isGuest {
db.collection(DatabaseRef.Users).document(Auth.auth().currentUser?.uid ?? "").collection(DatabaseRef.Cart).getDocuments { (cartSnapshot, error) in
guard error == nil, let cartSnapshot = cartSnapshot else {
return
}
cartSnapshot.documents.forEach { document in
var product = Product(data: document.data())
guard let index = products.firstIndex(of: product) else { return }
let cartCount: Int = document.exists ? document.get(DatabaseRef.cartCount) as? Int ?? 0 : 0
product.cartCount = cartCount
products[index] = product
}
handler(products, lastFetchedNumber)
}
}
else {
handler(products, lastFetchedNumber)
}
})
if lastFetchedNumber == fetchingNumberToStart {
for _ in 0 ..< 10 {
//change the fetching number each time in the loop
lastFetchedNumber = lastFetchedNumber + 1
let productId = sortedProducts[lastFetchedNumber]?.keys.first ?? ""
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
}
}
}
as you can see at the very end I am looping 10 times for this query because of for _ in 0 ..< 10 :
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
So I need to make the completion: block handler to be called only once instead of 10 times here.
Use a DispatchGroup. You can enter the dispatch group each time you call the async code and then leave each time it's done. Then when everything is finished it will call the notify block and you can call your handler. Here's a quick example of what that would look like:
let dispatchGroup = DispatchGroup()
let array = []
for i in array {
dispatchGroup.enter()
somethingAsync() {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
handler()
}

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.

Swift Vapor add additional info to custom response

I've two Models, Trip and Location. I would return a custom response with some field of trip and the number of Location that has the tripID equal to id of Trip. There is my code(not working). The field locationCount is always empty.
func getList(_ request: Request)throws -> Future<Response> {
let deviceIdReq = request.parameters.values[0].value
let queryTrips = Trip.query(on: request).filter(\.deviceId == deviceIdReq).all()
var tripsR = [TripCustomContent]()
var trips = [Trip]()
return queryTrips.flatMap { (result) -> (Future<Response>) in
trips = result
var count = 0
for t in trips {
let tripIdString = String(t.id!)
let v = Location.query(on: request).filter(\.tripID == tripIdString).count().map({ (res) -> Int in
return res
})/*.map{ (result) -> (Int) in
count = result
return result
}*/
let tripCustomContent = TripCustomContent.init(startTimestamp: t.startTimestamp, endTimestamp: t.endTimestamp, deviceId: t.deviceId, locationCount: v)
tripsR.append(tripCustomContent)
}
let jsonEncoder = JSONEncoder()
let data = try jsonEncoder.encode(tripsR)
let response = HTTPResponse.init(status: .ok, version: HTTPVersion.init(major: x, minor: y), headers: HTTPHeaders.init(), body: data)
let finalResponse = Response.init(http: response, using: request)
return try g.encode(for: request)
}
}
and this is my custom content struct:
struct TripCustomContent: Encodable {
var startTimestamp: String?
var endTimestamp: String?
var deviceId: String
var locationCount: Future<Int>
}
any suggestions?
You're trying to use a value which isn't available yet. When you're returning a Future, you aren't returning the value inside it.
So you want your TripCustomContent to be like this (use in vapor Content instead of Codable:
struct TripCustomContent: Content {
var startTimestamp: String?
var endTimestamp: String?
var deviceId: String
var locationCount: Int
}
You queried the Trip correctly, but not the Location. You could maybe try something like this:
return queryTrips.flatMap { trips -> Future<[TripCustomContent]> in
let tripIds = trips.map({ String($0.id!) })
return Location.query(on: request).filter(\.tripID ~~ tripIds).all().map { locations in
return trips.map { trip in
let locationCount = locations.filter({ $0.tripId == String(trip.id!) }).count
return TripCustomContent(... locationCount: locationCount)
}
}
}
What did I do here?
Map the trips to their tripIds to get an array of tripIds
Get all locations with a tripId of one of the tripIds in the above array
Map each of the trips to an instance of TripCustomContent, using the locations of the database filtered by tripId
Finally, you don't need to encode the JSON yourself, just return objects conforming Content:
func getList(_ request: Request) throws -> Future<[TripCustomContent]>
The above could be a solution to your strategy. But maybe you take a look at relations if they can be a more efficient, easier and faster way.

Swift Completion & Loop Issue

Trying to download a PKG file from one of three urls. The logic basically finds the latency of a download from each download url and sets the final download url to the host with the lowest latency.
import Cocoa
import Alamofire
// Create my object via Struct
struct Package {
var latency: Double?
var name: String?
var statuscode: Int?
var download: Bool?
var downloadUrl: String?
}
// Download the package from the provided download url and return the object
func getPKG(pkgName: String, dpUrl: String, completion: #escaping (Package) -> (Package)) {
let url = URL(string: "\(dpUrl)\(pkgName)")
let parameters: Parameters = ["foo":"bar"]
Alamofire.download(url, method: .get, parameters: parameters, encoding: JSONEncoding.default, to: destination)
.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
debugPrint("Download Progress...: \(progress.fractionCompleted)")
}
.validate(statusCode: 200..<399)
.response { response in
debugPrint(response.response!)
debugPrint(response.response!.statusCode)
debugPrint(response.timeline.latency)
let dlObject = Package(latency: response.timeline.latency, name: pkgName, statuscode: response.response?.statusCode, download: true, downloadUrl: dpUrl)
completion(dlObject)
}
}
var share_response = [String: Double]()
var package_sources: NSArray! = ["https://www.jss1.com/Share", "https://www.jss2.com/Share", "https://www.jss3.com/Share"]
let package_names: String = ["Dummy1.pkg", "Dummy2.pkg", "Dummy3.pkg"]
// Loop through the package sources and find the one with
// the lowest latency.
for share_url in package_sources {
getPKG(pkgName: "Dummy.pkg", dpUrl: share_url, completion: {
dlObject in
if dlObject.latency != nil {
share_response[share_url] = dlObject.latency
} else {
debugPrint("nothing yet")
}
return dlObject
})
}
let final_download_url = share_response.min { a, b in a.value < b.value }
// Here is where it breaks and responds with nil
for package in package_names {
let download_url = URL(string: final_download_url + package)
Download commands here...
}
This is done by looping through each download url and populating a dictionary with the key as the url and the value as the latency. When the script moves on to download from the "fastest" download url, it fails with nil.
I'm assuming that's because the script is moving on while the completion handler is still running and nothing is in the dictionary yet, but how would I address this?
Based on the answer from #vadian at Synchronous request using Alamofire
...
let group = DispatchGroup()
var share_response = [String: Double]()
var package_sources: NSArray! = ["https://www.jss1.com/Share", "https://www.jss2.com/Share", "https://www.jss3.com/Share"]
let package_names: String = ["Dummy1.pkg", "Dummy2.pkg", "Dummy3.pkg"]
// Loop through the package sources and find the one with
// the lowest latency.
for share_url in package_sources {
group.enter()
getPKG(pkgName: "Dummy.pkg", dpUrl: share_url, completion: {
group.leave()
dlObject in
if dlObject.latency != nil {
share_response[share_url] = dlObject.latency
} else {
debugPrint("nothing yet")
}
return dlObject
})
}
group.notify(queue: DispatchQueue.main) {
let final_download_url = share_response.min { a, b in a.value < b.value }
// Here is where it breaks and responds with nil
for package in package_names {
let download_url = URL(string: final_download_url + package)
Download commands here...
}
}