There are 2 files:
1st - Network requests
2nd - ViewController, place where the result of getCities() -> Array<String> { ... }
should be called (at leasted could be checked with print
Using this to make a request:
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error as Any)
} else { ...
}
The problem: The result of the request couldn't be accessed by UIViewController until the finish of the request. The list of UIViewController is initiated too early.
P.S: Already tried
semaphore
and
group
but as for me, it works only for the same class/file.
Don't ask, tell
Use a completion handler to notify when the data are available. No semaphore, no group.
func getCities(completion: #escaping ([String]) -> Void) { ... }
and
getCities { [weak self] cities in
self?.list = cities
print(cities)
// do other stuff with received cities
}
Related
Situation I need to fix - Imagine 2 requests are made in same time (when access token is not valid). Both try to refresh token but one of them will invalidate token for another one.
Is there any way how to:
allow only 1 to refresh token
stop all other requests
rerun all stopped requests (when token is refreshed)
Or do you have any idea how to solve this by other way?
This is how my request look like in every view controller:
AF.request(encodedURLRequest, interceptor: AuthInterceptor()).validate().responseData { (response) in
...
}
This is my AuthInterceptor:
final class AuthInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var adaptedUrlRequest = urlRequest
adaptedUrlRequest.setValue("Bearer \(UserDefaults.standard.getAccessToken())", forHTTPHeaderField: "Authorization")
completion(.success(adaptedUrlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
print("request \(request) failed")
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 403 {
guard let url = URL(string: Endpoint.login.url) else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let parameters: [String: String] = [
"refresh_token": UserDefaults.standard.getRefreshToken(),
"grant_type": "refresh_token"
]
guard let encodedURLRequest = try? URLEncodedFormParameterEncoder.default.encode(parameters,
into: urlRequest) else { return }
AF.request(encodedURLRequest).validate().responseData { (response) in
if let data = response.data {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let loginResponse = try? decoder.decode(LoginResponse.self, from: data) {
UserDefaults.standard.setAccessToken(value: loginResponse.accessToken)
UserDefaults.standard.setRefreshToken(value: loginResponse.refreshToken)
completion(.retryWithDelay(1))
}
}
}
}
}
}
You can use Alamofire's RequestRetrier protocol (as part of the RequestInterceptor protocol) to do this without having to manually start and stop requests.
Essentially, you need to know when a refresh is being performed and store any additional completion handlers from requests which failed because of the expired token. You can do this in your AuthInterceptor class. Just make sure your implementation is thread safe!
I am trying to communicate with Swift to a php-website using the command "uploadTask". The site is sending Data back, which is working well. The result from the website is stored in the variable "answer". But how can I actually use "answer" AFTER the uploadTask.resume() was done?
When running the file, it always prints:
"One" then "three" then "two".
I know that I could do things with "answer" right where the section "print("two")" is. And at many examples right there the command "DispatchQueue.main.async { ... }" is used. But I explicitly want to finish the uploadTask and then continue with some more calculations.
func contactPHP() {
print("One")
let url = "http://....php" // website to contact
let dataString = "password=12345" // starting POST
let urlNS = NSURL(string: url)
var request = URLRequest(url: urlNS! as URL)
request.httpMethod = "POST"
let dataD = dataString.data(using: .utf8) // convert to utf8 string
URLSession.shared.uploadTask(with: request, from: dataD)
{
(data, response, error) in
if error != nil {
print(error.debugDescription)
} else {
let answer = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!
print("Two")
}
}.resume() // Starting the dataTask
print("Three")
// Do anything here with "answer"
}
extension NSMutableData {
func appendString(string: String) {
let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true)
append(data!)
}
}
I already tried it with a completion handler. But this does not work either. This also gives me "One", "Four", "Two", "Three"
func test(request: URLRequest, dataD: Data?, completion: #escaping (NSString) -> ()) {
URLSession.shared.uploadTask(with: request, from: dataD)
{
(data, response, error) in
if error != nil {
print(error.debugDescription)
} else {
let answer = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!
print("Two")
completion(answer)
}
}.resume() // Starting the dataTask
}
let blubb = test(request: request, dataD: dataD) { (data) in
print("Three")
}
print("Four")
Use the URLSession function that has the completion handler:
URLSession.shared.uploadTask(with: URLRequest, from: Data?, completionHandler: (Data?, URLResponse?, Error?) -> Void)
Replace your uploadTask function with something like this:
URLSession.shared.uploadTask(with: request, from: dataD) { (data, response, error) in
if let error = error {
// Error
}
// Do something after the upload task is complete
}
Apple Documentation
After you create the task, you must start it by calling its resume()
method. If the request completes successfully, the data parameter of
the completion handler block contains the resource data, and the error
parameter is nil.
If the request fails, the data parameter is nil and
the error parameter contain information about the failure. If a
response from the server is received, regardless of whether the
request completes successfully or fails, the response parameter
contains that information.
When the upload task is complete, the completion handler of the function is called. You could also implement the delegate's optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) function.
I've been trying to understand this process, I've done a lot of reading and it's just not clicking so I would be grateful if anyone can break this down for me.
I have a method to retrieve JSON from a URL, parse it, and return the data via a completion handler. I could post code but it's all working and I (mostly) understand it.
In my completion handler I can print the data in the console so I know it's there and everything good so far.
The next bit is what's tripping me up. While I can use the data in the completion handler I can't access it from the view controller that contains the handler.
I want to be able to pass tableData.count to numberOfRows and get "Use of unresolved identifier 'tableData'"
I'd really appreciate it if anyone can lay out what I need to do next. Thanks!
Edit: adding code as requested
Here is my completion handler, defined in the ViewController class:
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
print("Here are the \(patients)")
}
in viewDidLoad:
let url = URL(string: "http://***.***.***.***/backend/returnA")
let returnA = URLRequest(url: url!)
retrieveJSON(with: returnA, completionHandler: completionHandler)
Defined in Networking.swift file:
func retrieveJSON(with request: URLRequest, completionHandler: #escaping ([Patient]) -> Void) {
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: request as URLRequest) {
// completion handler argument
(data, response, error) in
// completion handler
guard let data = data else {
print("Did not recieve data")
completionHandler([])
return
}
do {
let decoder = JSONDecoder()
let Patient = try decoder.decode(Array<Patient>.self, from: data)
// print(Patient)
completionHandler(Patient)
}
catch let err {
print("Err", err)
completionHandler([])
}
}
task.resume()
}
I also have a struct defined called Patient but I won't post that as it's very long and just a simple struct matching the JSON received.
First of all, when you use closure, you should consider strong reference cycle.
let completionHandler: ([Patient]) -> Void = { [weak self] patients in
guard let strongSelf = self else { return }
strongSelf.tableData = patients // update tableData that must be used with UITableViewDataSource functions.
strongSelf.tableView.reloadData() // notify tableView for updated data.
}
You are not populating the array(tableData) in the closure:
var completionHandler: ([Patient]) -> Void = {[weak self] (patients) in
print("Here are the \(patients)")
self?.tableData = patients
}
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
self.tableData = patients
self.tableView.reloadData()
//make sure your tableview datasource has tableData property used
}
I am refactoring a previous code I made, where I use Alamofire to download some Json files.
The fisrt request is straight forward. I make the request, I got the response and I parse it and store it on Realm. No problem here. Straight forward stuff.
The second request is a little trickier, because I need several ID that was retrieved from the first JSON request.
My solution for that problem was first to create a Completion handler on the function that has the Alamofire request:
func requestData(httpMethod: String, param: Any?, CallType : String, complition: #escaping (Bool, Any?, Error?) -> Void){
My idea was to use the Completion to wait for the Alamofire Response to finish and then start the new request. Turn out that didn't work as well.
I was able to pull this off by adding a delay to the Completion.
DispatchQueue.main.asyncAfter(deadline: .now() + 4)
It does work, but is far from being a good practice for several reasons and I would like to refactor that with something more intelligent.
My questions:
1) How is the best way to make many JSON requests on the same function? A way to correctly wait the first one to start the second on an so on?
2) Right now, I call a function to request the first JSON, and on the middle of the call I make a second request. It seems to me that I am hanging the first request too long, waiting for all requests to finish to then finish the first one. I don't think that is a good practice
Here is the complete code. Appreciate the help
#IBAction func getDataButtonPressed(_ sender: Any) {
requestData(httpMethod: "GET", param: nil, CallType: "budgets") { (sucess, response, error) in
if sucess{
print("ready")
DispatchQueue.main.asyncAfter(deadline: .now() + 4){
accounts = realm.objects(Account.self)
requestAccounts()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4){
users = realm.objects(User.self)
requestUser()
}
}
}
}
func requestData(httpMethod: String, param: Any?, CallType : String, complition: #escaping (Bool, Any?, Error?) -> Void){
let url = "https://XPTO.com/v1/\(CallType)"
guard let urlAddress = URL(string: url) else {return}
var request = URLRequest(url: urlAddress)
request.httpMethod = httpMethod
request.addValue("application/json", forHTTPHeaderField: "accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer appKey", forHTTPHeaderField: "Authorization")
if param != nil{
guard let httpBody = try? JSONSerialization.data(withJSONObject: param!, options:[]) else {return}
request.httpBody = httpBody
}
Alamofire.request(request).responseJSON { (response) in
let statusCode = response.response?.statusCode
print("Status Code \(statusCode!)")
jsonData = try! JSON(data: response.data!)
complition(true, jsonData, nil)
if httpMethod == "GET"{
saveJsonResponse(jsonData: jsonData, CallType: CallType)
}
}
}
func requestAccounts(){
var count = accounts.count
while count != 0{
let account = accounts[0]
RealmServices.shared.delete(account)
count -= 1
}
let numberOfBugdets = budgets.count
for i in 0...numberOfBugdets - 1{
requestData(httpMethod: "GET", param: nil, CallType: "/budgets/\(budgets[i].id)/accounts") { (sucess, response, error) in
print("accounts downloaded")
let numberOfAccounts = jsonData["data"]["accounts"].count
for j in 0...numberOfAccounts - 1{
let realm = try! Realm()
do{
try realm.write {
// Code to save JSON data to Realm
realm.add(newAccount)
}
} catch {
print("something")
}
}
}
}
}
func requestUser(){
var count = users.count
while count != 0{
let user = users[0]
RealmServices.shared.delete(user)
count -= 1
}
requestData(httpMethod: "GET", param: nil, CallType: "user") { (success, response, error) in
print("User data downloaded")
let realm = try! Realm()
do{
try realm.write {
// Code to save JSON data to Realm
realm.add(newUser)
}
} catch {
print("something")
}
}
}
func saveJsonResponse(jsonData: JSON, CallType: String){
case "budgets":
var count = budgets.count
while count != 0{
let budget = budgets[0]
RealmServices.shared.delete(budget)
count -= 1
}
let numberOfBudgets = jsonData["data"]["budgets"].count
for i in 0...numberOfBudgets - 1 {
// Code to save JSON data to Realm
RealmServices.shared.create(newBudget)
}
}
I recommend completionHandlers in such Situation.
This is how your code snippet on how to implement it and use it try to understand it and implement it in your code.
//CompletionHandlers
func firstOperation(completionHandler: #escaping (_ id: String) -> Void){
//preform alamoFire and in .response { } call completionHandler and pass it the id
completionHandler("10")
}
func buttonClicked () {
firstOperation { (id) in
secondFunction(completionHandler: { (data) in
// your data
})
}
}
func secondFunction(completionHandler: #escaping (_ data: String) -> Void){
//preform alamoFire and in .response { } call completionHandler and pass it the id
completionHandler("some Data")
}
This should give you a better understanding on how to implement it, CompletionHandlers are powerful
specially in handling such cases when you have to perform an action that depends on other action results and in networking we can't anyhow predict the time of the operation.
Read more about completionHandlers here
I have a function which gets the data from an website api.
func getData() {
print("getData is called")
//create authentication ... omitted
//create authentication url and request
let urlPath = "https://...";
let url = NSURL(string: urlPath)!
let request: NSMutableURLRequest = NSMutableURLRequest(URL: url)
request.setValue("Basic \(base64EncodedCredential)", forHTTPHeaderField: "Authorization")
request.HTTPMethod = "GET"
//some configuration here...
let task = session.dataTaskWithURL(url) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
let json = JSON(data: data!)
}
task.resume()
}
I am able to get data from API, and I have an observer so that each time when the app goes to foreground the getData() function can be called again to update the data.
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "willEnterForeground", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
func willEnterForeground() {
print("will enter foreground")
getData()
}
I am pretty sure that when my app goes to foreground, the getData() is called again, however, my data doesn't get updated even when data on the server API has changed. I tried to close the app and open it again, but it still doesn't update the data. So I was wondering if someone can give me some suggestions. Any help will be appreciated!
You need a to make it a closure with a completion. This will enable it to make the request and then call reload "Table View" once you have actually gotten the data back from the request. This is what you need to change on your getData() function
func getData(completion: (json) -> Void)) {
print("getData is called")
//create authentication url and request
let urlPath = "https://...";
let url = NSURL(string: urlPath)!
let request: NSMutableURLRequest = NSMutableURLRequest(URL: url)
request.setValue("Basic \(base64EncodedCredential)", forHTTPHeaderField: "Authorization")
request.HTTPMethod = "GET"
//some configuration here...
let task = session.dataTaskWithURL(url) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
let json = JSON(data: data!)
}
completion(json)
}
Then you will need to adjust where you call this function because you have changed the parameters it should look more like this.
func willEnterForeground() {
print("will enter foreground")
getData(completion:{
self.data = json
self.tableView.reloadData()
})
}
that will wait till the data is back before it reloads the view updating all your content to reflect what is on the server.
Let me know if you have more questions.
I finally found out it is because I didn't remove the cache. Change the cache policy works then.