How to pause programmatic execution inside a DispatchQueue without using sleep() - ios

so I make a post request to a server. In the response, I run some code in the main thread and conditionally call another post request. My problem is this--I need to wait ~5 seconds before the conditional is processed. How can I do this without using sleep()? (Using sleep freezes my app).
func getStuff() {
let server = appState.server + "extension"
let package = self.myID
guard let url = URL(string: server) else { return }
var urlRequest = URLRequest(url: url)
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("application/json", forHTTPHeaderField: "Accept")
urlRequest.httpMethod = "POST"
guard let data = try? JSONEncoder().encode(package) else { return }
urlRequest.httpBody = data
let task = URLSession.shared.dataTask(with: urlRequest) {
(data, response, error) in
if let error = error {
return
}
guard let data = data else { return }
guard let dataString = String(data: data, encoding: String.Encoding.utf8) else { return }
let jsonData = Data(dataString.utf8)
var decodedJob: Job? = nil
do {
decodedJob = try JSONDecoder().decode(Formatter.self, from: jsonData)
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async {
self.firstTimeLoading = false
sleep(5) //how do I replicate this behavior without freezing the app?
if array == [] {
otherPostFunction()
}
}
}
task.resume()
}

Do the following:
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(5)) {[weak self] in
if self?.array ?? [] == [] {
self?.otherPostFunction()
}
}

Related

Swift 5, RxSwift: Network request with RxSwift

I am starting to use RxSwift to make the service call.
This was my old code:
class Service: GraphQLService {
func graphQL(body: [String: Any?], onSuccess: #escaping (Foundation.Data) throws -> (), onFailure: #escaping (Error) -> ()) {
guard let urlValue = Bundle.main.urlValue else { return }
guard let url = URL(string: urlValue) else { return
print("Error with info.plist")
}
var request = URLRequest(url: url)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
onFailure(error)
}
if let data = data {
do{
try onSuccess(data)
}
catch{
onFailure(error)
}
}
}.resume()
}
And here I do the function to get time deposits:
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits(onSuccess: #escaping ([TimeDeposits]) -> (), onFailure: #escaping (Error) -> ()) {
let body = ["query": timeDepositQuery]
Service().graphQL(body: body, onSuccess: { data in
let json = try? JSONDecoder().decode(GraphQLResponse.self, from: data)
onSuccess(json?.data?.account?.timeDeposits ?? [])
}, onFailure: onFailure)
}
And so far this is my code with RxSwift:
class Service: GraphQLService {
func graphQL(body: [String : Any?]) -> Observable<Foundation.Data> {
return Observable.create { observer in
let urlValue = Bundle.main.urlValue
let url = URL(string: urlValue ?? "")
let session = URLSession.shared
var request = URLRequest(url: url!)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
session.dataTask(with: request) { (data, response, error) in
if let error = error {
observer.onError(error)
}
if let data = data {
do{
try onSuccess(data)
observer.onNext(data)
}
catch{
//onFailure(error)
observer.onError(error)
print("Error: \(error.localizedDescription)")
}
}
}.resume()
return Disposables.create {
session.finishTasksAndInvalidate()
}
}
}
This is where I don't understand how in my getTimeDeposits () I can do the deserialization with try? JSONDecoder () ... with RxSwift without using onSuccess?
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits() -> Observable<[TimeDeposits]> {
let body = ["query": timeDepositQuery]
Service().graphQL(body: body)
}
You can have getTimeDeposits() return an Observable as well and handle the deserialization in a map closure. A couple of other things.
RxCocoa already has a method on URLSession so you don't need to write your own.
I suggest reducing the amount of code you have in a function that makes the network request. You want to be able to test your logic for making the request without actually making it.
Something like this:
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits() -> Observable<[TimeDeposits]> {
let body = ["query": timeDepositQuery]
return Service().graphQL(body: body)
.map { try JSONDecoder().decode(GraphQLResponse.self, from: $0).data?.account?.timeDeposits ?? [] }
}
}
class Service: GraphQLService {
func graphQL(body: [String: Any?]) -> Observable<Data> {
guard let urlValue = Bundle.main.urlValue else { fatalError("Error with info.plist") }
let request = urlRequest(urlValue: urlValue, body: body)
return URLSession.shared.rx.data(request: request) // this is in RxCocoa
}
func urlRequest(urlValue: String, body: [String: Any?]) -> URLRequest {
guard let url = URL(string: urlValue) else { fatalError("Error with urlValue") }
var request = URLRequest(url: url)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
return request
}
}
If you don't want to use RxCocoa for some reason, here is the correct way to wrap the URLSession.dataTask method:
extension URLSession {
func data(request: URLRequest) -> Observable<Data> {
Observable.create { observer in
let task = self.dataTask(with: request, completionHandler: { data, response, error in
guard let response = response as? HTTPURLResponse else {
observer.onError(URLError.notHTTPResponse(data: data, response: response))
return
}
guard 200 <= response.statusCode && response.statusCode < 300 else {
observer.onError(URLError.failedResponse(data: data, response: response))
return
}
guard let data = data else {
observer.onError(error ?? RxError.unknown)
return
}
observer.onNext(data)
observer.onCompleted() // be sure to call `onCompleted()` when you are done emitting values.
// make sure every possible path through the code calls some method on `observer`.
})
return Disposables.create { task.cancel() } // don't forget to handle cancelation properly. You don't want to kill *all* tasks, just this one.
}
}
}
enum URLError: Error {
case notHTTPResponse(data: Data?, response: URLResponse?)
case failedResponse(data: Data?, response: HTTPURLResponse)
}

SKCloudServiceController().requestUserToken Freezes on iOS 14.2

I am trying to run the following function from SKCloudServiceController but for some reason every time it runs, the app just freezes. I have tested my developer token and it does work. I am running Xcode 12.2. Maybe there was an update which would make this not work anymore?
I've tested the token and it works.
class AppleMusicAPI {
let developerToken = "b'eyJ0{...}RDlRSlFw'"
func getUserToken() -> String {
var userToken = String()
let lock = DispatchSemaphore(value: 0)
func requestAccess(_ completion: #escaping(String?) -> Void) {
SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { (receivedToken, error) in
completion(receivedToken)
}
}
requestAccess( { (completeToken) in
if let token = completeToken {
userToken = token
lock.signal()
}
})
lock.wait()
return userToken
}
func fetchStorefrontID() -> String {
let lock = DispatchSemaphore(value: 0)
var storefrontID: String!
let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")!
var musicRequest = URLRequest(url: musicURL)
musicRequest.httpMethod = "GET"
musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
musicRequest.addValue(getUserToken(), forHTTPHeaderField: "Music-User-Token")
URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
guard error == nil else { return }
if let json = try? JSON(data: data!) {
let result = (json["data"]).array!
let id = (result[0].dictionaryValue)["id"]!
storefrontID = id.stringValue
lock.signal()
}
}.resume()
lock.wait()
return storefrontID
}
func searchAppleMusic(_ searchTerm: String!) -> [Song] {
let lock = DispatchSemaphore(value: 0)
var songs = [Song]()
let musicURL = URL(string: "https://api.music.apple.com/v1/catalog/\(fetchStorefrontID())/search?term=\(searchTerm.replacingOccurrences(of: " ", with: "+"))&types=songs&limit=25")!
var musicRequest = URLRequest(url: musicURL)
musicRequest.httpMethod = "GET"
musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
musicRequest.addValue(getUserToken(), forHTTPHeaderField: "Music-User-Token")
URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
guard error == nil else { return }
if let json = try? JSON(data: data!) {
let result = (json["results"]["songs"]["data"]).array!
for song in result {
let attributes = song["attributes"]
let currentSong = Song(id: attributes["playParams"]["id"].string!, name: attributes["name"].string!, artistName: attributes["artistName"].string!, artworkURL: attributes["artwork"]["url"].string!)
songs.append(currentSong)
}
lock.signal()
} else {
lock.signal()
}
}.resume()
lock.wait()
return songs
}
}
I have a theory on what happened: since the requestUserToken function is called on the main thread, using a semaphore creates an infinite wait(lock.wait() and lock.signal() are called on the same thread). What eventually worked for me was using completion handlers instead of semaphores. So my getUserToken function looked like this:
func getUserToken(completion: #escaping(_ userToken: String) -> Void) -> String {
SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { (userToken, error) in
guard error == nil else {
return
}
completion(userToken)
}
}
And in any subsequent functions that need the userToken, I passed it in as a parameter:
func fetchStorefrontID(userToken: String, completion: #escaping(String) -> Void){
var storefrontID: String!
let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")!
var musicRequest = URLRequest(url: musicURL)
musicRequest.httpMethod = "GET"
musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
musicRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token")
URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
guard error == nil else { return }
if let json = try? JSON(data: data!) {
let result = (json["data"]).array!
let id = (result[0].dictionaryValue)["id"]!
storefrontID = id.stringValue
completion(storefrontID)
}
}.resume()
}
Calling fetchStorefrontID by first calling getUserToken then calling fetchStorefrontID in its completion handler
getUserToken{ userToken in
fetchStorefrontID(userToken){ storefrontID in
print(storefrontID)
//anything you want to do with storefrontID here
}
}
This is just what eventually worked for me.
Cleaning up a little of what has already been posted.
func getUserToken(completion: #escaping(_ userToken: String?) -> Void) {
SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { (receivedToken, error) in
guard error == nil else { return }
completion(receivedToken)
}
}
func fetchStorefrontID(userToken: String, completion: #escaping(String) -> Void) {
var storefrontID: String! = ""
let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")!
var musicRequest = URLRequest(url: musicURL)
musicRequest.httpMethod = "GET"
musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
musicRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token")
URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
guard error == nil else { return }
if let json = try? JSON(data: data!) {
let result = (json["data"]).array!
let id = (result[0].dictionaryValue)["id"]!
storefrontID = id.stringValue
completion(storefrontID)
}
}.resume()
}
And then to call that code:
SKCloudServiceController.requestAuthorization { status in
if status == .authorized {
let api = AppleMusicAPI()
api.getUserToken { userToken in
guard let userToken = userToken else {
return
}
api.fetchStorefrontID(userToken: userToken) { data in
print(data)
}
}
}
}

Why not getting response from one URL?

I am using URLSession. I am not receiving any error or response.
It works with one url. However it does not work with one another.
I have also tried percentencoding. But it doesn't work too.
The code is below
let urlString = "https://stark-spire-93433.herokuapp.com/json"//This is not working
//let urlString = "https://jsonplaceholder.typicode.com/todos"//This is working
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)//URLSession.shared
var request = URLRequest(url: URL(string:urlString)!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: [], options: [])
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
print("response---",response)
print("error--",error)
if data != nil {
let json = try? JSONSerialization.jsonObject(with: data!)
print("JSOn",json)
} else {
print("error data is nil")
}
})
task.resume()
Too cumbersome code.
This is sufficient
let url = URL(string:"https://stark-spire-93433.herokuapp.com/json")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { print(error!); return }
do {
let json = try JSONSerialization.jsonObject(with: data)
print("JSOn",json)
} catch {
print(error)
}
}
task.resume()

Make Swift Class to make Http Requests

I made this class below to help make http requests from anywhere in my application, however when i call the class it does not make the request as expected. I see i can print from the function so i know it is making it that far at least. What else do i need to make this request?? the api parameter is being passed correctly as well.
in viewController:
let url = "post/test"
MakeHttpRequest.sharedInstance.postRequest(api: url)
in class:
import Foundation
final class MakeHttpRequest {
static let sharedInstance = MakeHttpRequest()
var api_token = "token_here"
var url_base = "https://test.frb.io/"
func postRequest (api : String) {
let parameters = ["email": "testing123#gmail.com"]
guard let destination = URL(string: url_base + api) else { return }
var request = URLRequest(url: destination)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
// request.setValue("clientIDhere", forHTTPHeaderField: "Authorization")
guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: [])
else { return }
request.httpBody = httpBody
let session = URLSession.shared
session.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
if (json["response"]) != nil {
}
} catch {
print(error)
}
}
}.resume()
print(url_base + api)
}
private init() {
}
}
So two things to check. Sometimes swift function chaining doesn't work properly. So try splitting the session.dataTask and resume into two statements. And second try adding error handling to the response. You may be getting an error without knowing it. See code sample below.
import Foundation
final class MakeHttpRequest {
static let sharedInstance = MakeHttpRequest()
var api_token = "token_here"
var url_base = "https://test.frb.io/"
func postRequest (api: String, parameters: [String: Any]? = nil) {
guard let destination = URL(string: url_base + api) else { return }
var request = URLRequest(url: destination)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
// request.setValue("clientIDhere", forHTTPHeaderField: "Authorization")
if let parameters = parameters {
guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { return
}
request.httpBody = httpBody
}
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
if (json["response"]) != nil {
print("1234")
} else {
print("ABCD")
}
} catch {
print(error)
}
} else {
print(error ?? "")
}
}
task.resume()
print(url_base + api)
}
private init() {
}
}
let url = "post/test"
MakeHttpRequest.sharedInstance.postRequest(api: url)

Swift - Multiple Parameters to webservice

I have the following code that should send a username and password off to a webservice, in return I get a single integer back:
func attemptLogin() {
let url:URL = URL(string: endpoint+"/LoginNew")!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
let postString = "username="+txtUsername.text! + "; password="+txtPassword.text!
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = session.dataTask(with: request as URLRequest) {
(
data, response, error) in
guard let data = data, let _:URLResponse = response, error == nil else {
print("error")
return
}
let dataString = String(data: data, encoding: String.Encoding.utf8)
print(dataString)
}
task.resume()
}
In my function I need to add two parameters are I'm trying to do in this line:
let postString = "username="+txtUsername.text! + "; password="+txtPassword.text!
request.httpBody = postString.data(using: String.Encoding.utf8)
I am getting the following response from my web service when I run this however
Optional("Missing parameter: password.\r\n")
I am obviously not appending the parameters to the request properly but I'm not sure what I've done wrong.
It is good practice to avoid using explicit unwraps of optionals (using !), use guard let for text i UITextFields instead.
And why not separate into two methods, attemptLogin and login, which maybe can take a closure for code to execute when sign in completed? Maybe the closure can take an Result enum.
Like this:
typealias Done = (Result) -> Void
enum MyError: Error {
case unknown
}
enum Result {
case success(String)
case failure(MyError)
init(_ error: MyError) {
self = .failure(error)
}
init(_ dataString: String) {
self = .success(dataString)
}
}
func login(username: String, password: String, done: Done? = nil) {
let session = URLSession.shared
guard
let url = URL(string: endpoint+"/LoginNew"),
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
let postString = "username=\(username)&password=\(password)"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = session.dataTask(with: request) {
(data, response, error) in
guard let data = data else { done?(Result(.unknown)); return }
let dataString = String(data: data, encoding: String.Encoding.utf8)
done?(Result(dataString))
}
task.resume()
}
func attemptLogin() {
guard
let username = txtUsername.text,
let password = txtPassword.text
else { return }
login(username: username, password: password) {
result in
swicth result {
case .success(let dataString):
print(dataString)
case .failure(let error):
print("Failed with error: \(error)")
}
}
}
Disclaimer: Have not tested the code above, but hopefully it compiles (at least with very small changes).

Resources