Recently, I attempted to write my own Telegram Bot API. However, the project has seem to have hit a brick wall with URLSession (formerly NSURLSession) issues.
The call structure is as follows:
getMe() -> getData() -> NSURLSession
Ideally, I would like to have the data returned from NSURLSession passed back to getMe() for the application to process. However, this has not proven possible with the methods I have tried.
Below is the code I have been using. synthesiseURL() generates the URL that the app should open the session to in order to perform the action on the Telegram Bot API. A template of the URL generated by synthesiseURL() is https://api.telegram.org/bot\(token)/\(tgMethod).
// NSURLSession getData: gets data from Telegram Bot API
func getData(tgMethod: String, arguments: [String] = [String](), caller: String = #function) {
let url = synthesiseURL(tgMethod: "getMe"), request = NSMutableURLRequest(url: url)
var receivedData = String()
let session = URLSession.shared.dataTask(with: request as URLRequest) { data, response, err in
if err != nil {print(err!.localizedDescription); return}
DispatchQueue.main.async {
receivedData = String(data: data!, encoding: String.Encoding.nonLossyASCII)!
print(receivedData)
}
}
session.resume()
}
I have been trying to get getData to pass receivedData, which contains the Bot API's response, back to the function getMe.
func getMe() -> String {
HTTPInterface(botToken: token).get(tgMethod: "getMe")
return [???] // here's where the data from getData() should come
}
I have tried completion handlers, callbacks, asynchronous calls to the main thread etc, but none seem to be working as expected (getMe() returns an empty string).
Why is this so, and can it be fixed?
The fundamental issue is that your getMe() function is declared as having an immediate String return type, but it depends on a delayed / asynchronous call to get that string. The timeline looks something like this:
getMe() is called by some client code
getMe() kicks of the method that launches a URLSession to get the data
getMe() moves to the next line of execution and returns a string (still empty at this point). The getMe() function has now returned and the client code execution has continued forward with the empty String result
The URLSession completes with data, but execution has already moved on so the data doesn't get used anywhere
The easiest fix is to make your getMe function not have a return type, but to also call back to a closure parameter when the URLSession data comes back, something like:
func getMe(callback:String->()) {
//getData and pass a closure that executes the callback closure with the String data that comes back
}
The less easy fix is to use a technique like dispatch semaphores to prevent getMe() from returning a result until the URLSession data comes back. But this sort of approach is likely to stall your main thread and is unlikely to be the right choice.
Related
I'm quite new to Combine and, instead of running all my tasks into the viewModel, I'm trying to better isolate the code that has to do with business logic.
Let's take a SignIn service as example. The service receives username and password and return token and userID.
The exposed call of the service is signIn that internally calls a private func networkCall. I'd like to implement the two functions to return a Publisher.
The role of networkCall should be calling the API and storing the received token, while the role of signIn is only to return a success or a failure.
This is my code, where I'm also highlighting where I'm getting stuck.
In general I don't know where is the right place to work with the information received from the API (and store the token). At the moment I'm doing it inside a .map call but it sounds wrong to me. Could you share some advice to improve this logic and especially explain which is the right place to run the business logic... I'm supposing that .map is not the right place! and .sink will just stop the chain.
struct SignInResponse:Codable{
var token:String
var userID:String
}
class SignInService {
// Perform the API call
private func networkCall(with request:SignInRequest)->AnyPublisher<SignInResponse, ServiceError>{
return URLSession.DataTaskPublisher(request: request, session: .shared)
.decode(type: SignInResponse.self, decoder: JSONDecoder())
.mapError{error in return ServiceError.error}
.eraseToAnyPublisher()
}
func signIn(username:String, password:String)->AnyPublisher<Result<String, ServiceError>, Never>{
let request = SignInRequest(with username:username, password:password)
return networkCall(with: request)
.map{ (response) -> Result<String, ServiceError> in
if response.token != ""{
// THIS SOUNDS EXTREMELLY WRONG. I SHOULD NOT USE MAP TO HANDLE THE TOKEN -------
self.storage.save(key: "token", value: response.token)
return Result.success(response.userID)
}else{
return Result.failure(ServiceError.unknown)
}
}
.replaceError(with: Result.failure(ServiceError.unknown))
.eraseToAnyPublisher()
}
......
}
From the model I call SignIn in this way:
func requestsSignIn(){
if let username = username, let password = password{
cancellable = service.signIn(username: username, password: password)
.sink(receiveValue: { (result) in
switch result{
case .failure(let error):
self.errorMessage = error.localizedDescription
case .success(let userID):
// the sigin succeeded do something here
}
})
}
}
Basically I agree with the existing answer. Your misconception here seems to be what a Combine pipeline is for. The idea is that either a useful value — here, your user ID — or an error (if appropriate; otherwise, nothing) should pop out the end of the pipeline. The subscriber at the end of the pipeline stands ready to receive either of those.
Thus it generally makes no sense to pass a Result object out the end of the pipeline, which must be further analyzed into a success or failure value. The goal of a Result object is merely to allow you to pass asynchronicity around, i.e. by handing someone else a completion handler to be called with a Result at some future time, just so as not to have to call with one of two values, i.e. either a real value or an error, using two Optional parameters.
Once a Combine publisher has published, though, asynchronicity has already happened, and you're getting the signal of this fact; that's what publishing means. The only thing you now need to preserve is whatever part or mutation of the signal is meaningful and useful to you.
Here is a fairly typical pipeline that does the sort of thing you want to do; I have not divided this into two separate pieces as you do, but of course you can divide it up however you like:
URLSession.DataTaskPublisher(request: request, session: .shared)
.map {$0.data}
.decode(type: SignInResponse.self, decoder: JSONDecoder())
.tryMap { response -> String in
if response.token == "" {
throw ServiceError.unknown
}
return response.userID
}
.receive(on:DispatchQueue.main)
.sink(receiveCompletion: {err in /* do something with error */ },
receiveValue: {userID in /* do something with userID */})
.store(in:&storage)
First, the result of a data task is a tuple, but all we need is the data part, so we map to that. Then we decode. Then we check for an empty token, and throw if we get one; otherwise, we map down to the user ID because that is the only useful result. Finally we switch to the main thread and capture the output using a sink, and store the sink in the usual Set<AnyCancellable> so that it persists long enough for something to happen.
Observe that if at any stage along the way we suffer a failure error, that error is immediately propagated all the way out the end of the pipeline. If the data task fails, it will be a URLError. If the decoding fails, it will be an Error reporting the issue, as usual with a decoder. If the token isn't there, it will be a ServiceError. At any point along the way, of course, you can catch and block or transform the error as it comes down the line if you wish.
As an alternative setup have signIn return a publisher with just Output String and Failure type Service.Error directly (the Result type becomes redundant with a Publisher).
Then, for an error like an empty token string in the response, use tryMap instead of map to transform the Result type from network function and have it throw an ServiceEror.emptyToken or something like that. That will cause the publisher to publish that as the Failure right away.
I declare a string in the beginning
var testString: String?
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {(data, response, error) in
let xml = SWXMLHash.parse(data)
testString = xml["root"]["schedule"]["date"].element?.text
}
But outside the NSURLSession, testString is nil. How can I make it so that it does not become nil and I can actually use the value?
For example, I want to use
println (testString)
AFTER the method block. But it is nil
The reason your variable is nil is because closures are executed asynchronously. That means that the rest of the code after the network request will continue to be called as normal, but the code containing parameters data, response and error is only called when the network request is finished.
To work around this, try putting whatever you are trying to do with the variable inside the closure, so your println(testString) would be inside the curly brackets.
The only way I've found to update a record in Vapor is this:
drop.get("update") { request in
guard var first = try Acronym.query().first(),
let long = request.data["long"]?.string else {
throw Abort.badRequest
}
first.long = long
try first.save()
return first
}
However it's not a very RESTful way of doing it since it's performing a GET request with a parameter instead of a PUT request.
How does one perform a PUT request in Vapor?
As it turns out, performing PUT, as well as other HTTP methods are as simple as changing .get() or .post() to .put() or any other HTTP methods.
As for my question, to create a PUT function in Vapor, just simply add a .put method that takes an Int (Or String, or any data type you'd like), and accept a JSON (Or whatever format you'd like), and simply update like it's a POST request.
drop.put("update") { request in
guard var first = try Acronym.query().first(),
let long = request.data["long"]?.string else {
throw Abort.badRequest
}
first.long = long
try first.save()
return first
}
I'm using the NSURLSession delegate way of making http get request to get json data from a server. I end up passing in the NSData received in my NSURLSessionDataDelegate to a model object to update an array like so:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
networkDelegate?.updateGameList(data)
dataTask.resume()
}
Once the data makes its way into the updateGameList function, weird stuff happens if I try to use NSJSONSerialization.JSONObjectWithData. If I don't try to use NSJSONSerialization, and merely convert the NSData to a string, it looks like the proper response I am expecting, something like this:
Optional([{"id":"a19610e4-675e-40b3-8335-085b61bfb0e3","name":"tttt","status":"PLAYING"},
{"id":"3be2e411-0086-46fd-8fc9-38d11831d0fb","name":"LWP TEST","status":"PLAYING"},
{"id":"b5d97d73-1ce2-4e5b-8b38-805835e2a21d","name":"asd","status":"PLAYING"},
{"id":"8a15575c-4c2c-4b46-ad5b-51b4b2680416","name":"MsGame","status":"WAITING"},
{"id":"b54531b8-5323-4630-929c-6eb2cfebde63","name":"423","status":"PLAYING"},
{"id":"5a7cfaa0-e2a4-41f6-bda2-a854a2d00a57","name":"4234","status":"PLAYING"}])
However, once I try to use the NSData with NSJSONSerialization.JSONObjectWithData to make an NSArray, it starts throwing errors because the data seems to be truncated either at the beginning or end, like this:
Optional([{"id":"a19610e4-675e-40b3-8335-085b61bfb0e3","name":"tttt","status":"PLAYING"},{"id":"3be2e411-0086-46fd-8fc9-38d11831d0fb","name":"LWP TEST","status":"PLAYING"},
{"id":"6d4e9731-61be-4191-bb9a-a30b6395a4a2","name":"RANDOMBOT","status":"PLAYING"},{"id":"c31b363c-f565-4f4a-a6f1-1ac219425f40","name":"Testament ","status":"PLAYING"},
{"id":"af14d8bc-37a0-4ec3-88de-ee364344d720","name":"Testament ","status":"PLAYING"},
{"id":"29439dd9-357d-445b-856c-39862e19c2fc","name":"Testament ","status":"PLAYING"},{"id":"cc29046f-4e80-422d-a103-5be175e799c9","name":"matt7","status":"PLAYING"},
{"id":"ff75c546-0e9b-4560-8efb-d0fa5be61cde","name":"u","status":"DONE"},{"id":"3d88df55-6f84-469c-a18e-27e463dc30eb","name":"test","status":"PLAYING"},{"id":"40eb1b13-21c3-4c8d-a379-e6b85329374b","name":"test","status":"PLAYING"},
{"id":"4e7519dd-79d3-4229-8d0e-47ca112dc08f","name":"test","status":"PLAYING"},{"id":"32ce49cc-17aa-47ca-8b9f-1c35dbdb78e6","name":"test","status":"PLAYING"},
{"id":"f5d5c961-17eb-421d-86b1-fbbadfb795da","name":"test","status":"PLAYING")
update game list error Error Domain=NSCocoaErrorDomain Code=3840
"Unexpected end of file while parsing object."
UserInfo={NSDebugDescription=Unexpected end of file while parsing object.}
Here is the updateGameList function, when I comment out the do-catch block with the JSONSerialization code in it, the dataString prints out the correct response, when I uncomment it out, it runs multiple times, almost like it's trying to process the data a chunk at a time instead of all at once. If I take out the .AllowFragments option, it tells me to put it in, when I put it in, it gets to the real error of the json data not starting or ending correctly.
func updateGameList(gameListData:NSData) {
let dataString = NSString(data: gameListData, encoding: NSUTF8StringEncoding)
print(dataString)
do {
let gameList:NSArray = try NSJSONSerialization.JSONObjectWithData(gameListData, options: [.AllowFragments]) as! NSArray
}
catch {
print("update game list error \(error)")
}
}
Quoting from the documentation:
This delegate method may be called more than once, and each call
provides only data received since the previous call. The app is
responsible for accumulating this data if needed.
So what you're observing is a feature: the data is received in chunks (most likely because the originating server uses Transfer-Encoding: chunked), and therefore you cannot expect to be able to parse each chunk individually - you'll have to accumulate all chunks to from one response, which you can then try to parse.
I declare a string in the beginning
var testString: String?
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {(data, response, error) in
let xml = SWXMLHash.parse(data)
testString = xml["root"]["schedule"]["date"].element?.text
}
But outside the NSURLSession, testString is nil. How can I make it so that it does not become nil and I can actually use the value?
For example, I want to use
println (testString)
AFTER the method block. But it is nil
The reason your variable is nil is because closures are executed asynchronously. That means that the rest of the code after the network request will continue to be called as normal, but the code containing parameters data, response and error is only called when the network request is finished.
To work around this, try putting whatever you are trying to do with the variable inside the closure, so your println(testString) would be inside the curly brackets.