How to define a fallback case if a remote GET request fails? - ios

I recently started with iOS development, and I'm currently working on adding new functionality to an existing app. For this feature I need to obtain a JSON file from a web server. However, if the server is unreachable (no internet/server unavailable/etc), a local JSON needs to be used instead.
In my current implementation I tried using a do catch block, but if there's no internet connection, the app just hangs instead of going to the catch block. JSON parsing and local data reading seem to work fine, the problem is likely in the GET method, as I tried to define a callback to return the JSON data as a separate variable, but I'm not sure if that's the correct way.
What is the best way to handle this scenario?
let url = URL(string: "https://jsontestlocation.com") // test JSON
do {
// make a get request, get the result as a callback
let _: () = getRemoteJson(requestUrl: url!, requestType: "GET") {
remoteJson in
performOnMainThread {
self.delegate.value?.didReceiveJson(.success(self.parseJson(jsonData: remoteJson!)!))
}
}
}
catch {
let localFile = readLocalFile(forName: "local_json_file")
let localJson = parseJson(jsonData: localFile!)
if let localJson = localJson {
self.delegate.value?.didReceiveJson(.success(localJson))
}
}
getRemoteJson() implementation:
private func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
// Method which returns a JSON questionnaire from a remote API
var request = URLRequest(url: requestUrl) // create the request
request.httpMethod = requestType
// make the request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// check if there is any error
if let error = error {
print("GET request error: \(error)")
}
// print the HTTP response
if let response = response as? HTTPURLResponse {
print("GET request status code: \(response.statusCode)")
}
guard let data = data else {return} // return nil if no data
completion(data) // return
}
task.resume() // resumes the task, if suspended
}
parseJson() implementation:
private func parseJson(jsonData: Data) -> JsonType? {
// Method definition
do {
let decodedData = try JSONDecoder().decode(JsonType.self, from: jsonData)
return decodedData
} catch {
print(error)
}
return nil
}

If you don't have to use complex logic with reachability, error handling, request retry etc. just return nil in your completion in case of data task, HTTP and No data errors:
func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
var request = URLRequest(url: requestUrl)
request.httpMethod = requestType
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Task error
guard error == nil else {
print("GET request error: \(error!)")
completion(nil)
return
}
// HTTP error
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("GET request failed: \(response!.description)")
completion(nil)
return
}
// No data
guard let data = data else {
completion(nil)
return
}
completion(data)
}
task.resume()
}
let url = URL(string: "https://jsontestlocation.com")!
getRemoteJson(requestUrl: url, requestType: "GET") { remoteJson in
if let json = remoteJson {
print(json)
...
}
else {
print("Request failed")
...
}
}

func NetworkCheck() -> Bool {
var isReachable = false
let reachability = Reachability()
print(reachability.status)
if reachability.isOnline {
isReachable = true
// True, when on wifi or on cellular network.
}
else
{
// "Sorry! Internet Connection appears to be offline
}
return isReachable
}
Call NetworkCheck() before your API request. If It returns false, read your local json file. if true do remote API call.
Incase after remote API call, any failure check with HTTP header response code.
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
}

I think you need to stop the request from hanging when it’s waiting for a response. The app might be running on a poor connection and be able to get some but not all the data in which case you likely want to failover to the local JSON.
I think you can roughly use what you have but add a timeout configuration on the URLSession as described here: https://stackoverflow.com/a/23428960/312910

Related

How to Decode Apple App Attestation Statement?

I'm trying to perform the Firebase App Check validation using REST APIs since it's the only way when developing App Clips as they dont' support sockets. I'm trying to follow Firebase docs. All I'm having trouble with is the decoding of the App Attestation Statement.
So far I've been able to extract the device keyId, make Firebase send me a challenge to be sent to Apple so they can provide me an App Attest Statement using DCAppAttestService.shared.attestKey method.
Swift:
private let dcAppAttestService = DCAppAttestService.shared
private var deviceKeyId = ""
private func generateAppAttestKey() {
// The generateKey method returns an ID associated with the key. The key itself is stored in the Secure Enclave
dcAppAttestService.generateKey(completionHandler: { [self] keyId, error in
guard let keyId = keyId else {
print("key generate failed: \(String(describing: error))")
return
}
deviceKeyId = keyId
print("Key ID: \(deviceKeyId.toBase64())")
})
}
func requestAppAttestChallenge() {
guard let url = URL(string: "\(PROJECT_PREFIX_BETA):generateAppAttestChallenge?key=\(FIREBASE_API_KEY)") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
return
}
guard let data = data, error == nil else {
return
}
let response = try? JSONDecoder().decode(AppAttestChallenge.self, from: data)
if let response = response {
let challenge = response.challenge
print("Response app check challenge: \(challenge)")
print("Response app check keyID: \(deviceKeyId)")
let hash = Data(SHA256.hash(data: Data(base64Encoded: challenge)!))
dcAppAttestService.attestKey(deviceKeyId, clientDataHash: hash, completionHandler: {attestationObj, errorAttest in
let string = String(decoding: attestationObj!, as: UTF8.self)
print("Attestation Object: \(string)")
})
}
}
task.resume()
}
I tried to send the attestation object to Firebase after converting it in a String, although I wasn't able to properly format the String. I see from Apple docs here the format of the attestation, but it isn't really a JSON so I don't know how to handle it. I was trying to send it to Firebase like this:
func exchangeAppAttestAttestation(appAttestation : String, challenge : String) {
guard let url = URL(string: "\(PROJECT_PREFIX_BETA):exchangeAppAttestAttestation?key=\(FIREBASE_API_KEY)") else {
return
}
let requestBody = ExchangeAttestChallenge(attestationStatement: appAttestation.toBase64(), challenge: challenge, keyID: deviceKeyId)
let jsonData = try! JSONEncoder().encode(requestBody)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) {data, response, error in
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("Exchange App Attestation \(String(describing: response))")
return
}
guard let data = data, error == nil else {
return
}
print("Exchange App Attestation: \(data)")
}
task.resume()
}

Unit Testing with URLProtocol subclassing returns a 0 bytes Data instead of nil

First of all, sorry for my english, I hope it won't be a pain to read me :D
I'm currently building an iOS application for a project in my school and I'm facing a problem.
I make an API call and I test it by using the URLProtocol method.
The API call :
task?.cancel()
task = exchangeSession.dataTask(with: request) { data, response, error in
// The dataTask method will execute in a separate queue, so we
// get back into the main one because
// we will modify the user interface with our exchange result
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil, error)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil, nil)
return
}
guard let responseJSON = try? JSONDecoder().decode(ConvertResponse.self, from: data), let result = responseJSON.result else {
callback(false, nil, nil)
return
}
callback(true, result, nil)
}
}
task?.resume()
The MockURLProtocol :
final class MockURLProtocol: URLProtocol {
// We return true in order to allow URLSession to use this protocol for any URL Request
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
static var loadingHandler: ((URLRequest) -> (Data?, HTTPURLResponse?, Error?))?
override func startLoading() {
guard let handler = MockURLProtocol.loadingHandler else {
print("Loading handler is not set.")
return
}
let (data, response, error) = handler(request)
guard error == nil else {
client?.urlProtocol(self, didFailWithError: error!)
return
}
if let data = data {
client?.urlProtocol(self, didLoad: data)
}
if let response = response {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
}
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
As you can see, it contains a handler which will have the data / response and error.
In one of my tests I want to check the case where I don't have any data, so I put nil for it.
The test :
func testConvertShouldPostFailedCallbackIfNoData() {
MockURLProtocol.loadingHandler = { request in
return (nil, nil, nil)
}
let expectation = XCTestExpectation(description: "Wait for queue change.")
client.convert(from: from, to: to, amount: amount) { success, result, error in
XCTAssertFalse(success)
XCTAssertNil(result)
XCTAssertNil(error)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.01)
}
Here is my problem: nil doesn't work for my data parameter, it shows me "0 Bytes" instead.
It works for error and response, which is strange to me.
The result : The result
I wanted to ask you why the data parameter isn't nil?
With 0 bytes it's not considered as nil and I'm not going in the right loop in my code.
I tried a lot of breakpoints, but I still can't figure it out.
But one thing is really strange. If I put and error in the handler with nil data, the data will be nil.
Maybe it has something to do with the didFailWithError function?
This function forces data to be nil somehow?
startLoading is not the right place to check for the response or data or error. Instead, use the URLSessionDataDelegate's delegate methods to check the response/data/error received for that request.

swift JSON login REST with post and get response example

It's my first experience with REST in iOS development with swift. I couldn't find any working or straight (simple) example for doing what i need here.
I have a login backend (https://myaddress.com/rest/login), where I need to pass 2 params: login and password. When I pass good values (user exists in database) I get 2 variables as a result: token (string) and firstLogin (bool). So when I get those values I know that login is successful and I can log in into my app.
So I am begging you for an example (just a simple function) of how to achieve that. If I get working code example I will know how to use it for other rest services in my app. I tried many solutions from tutorials I found, but any of them was working for me.. So to not waste my time searching I would like someone experienced to show me the way to achieve that.
I am not sure if Alamofire is so good to use, I know that swift 4 has it's own build neetwork services and to work with json. Any solution that works would be great.
Also, side question - if I would prefer to use Alamofire, do I need to use swiftyJSON also? Or it's just for parsing?
You can use URLSession if you don't like to import Alamofire in your Project to perform a simple task.
here are some method : GET, POST, DELETE METHODS and tutorial
GET METHOD
func makeGetCall() {
// Set up the URL request
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
do {
guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])
as? [String: Any] else {
print("error trying to convert data to JSON")
return
}
// now we have the todo
// let's just print it to prove we can access it
print("The todo is: " + todo.description)
// the todo object is a dictionary
// so we just access the title using the "title" key
// so check for a title and print it if we have one
guard let todoTitle = todo["title"] as? String else {
print("Could not get todo title from JSON")
return
}
print("The title is: " + todoTitle)
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
POST METHOD
func makePostCall() {
let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
guard let todosURL = URL(string: todosEndpoint) else {
print("Error: cannot create URL")
return
}
var todosUrlRequest = URLRequest(url: todosURL)
todosUrlRequest.httpMethod = "POST"
let newTodo: [String: Any] = ["title": "My First todo", "completed": false, "userId": 1]
let jsonTodo: Data
do {
jsonTodo = try JSONSerialization.data(withJSONObject: newTodo, options: [])
todosUrlRequest.httpBody = jsonTodo
} catch {
print("Error: cannot create JSON from todo")
return
}
let session = URLSession.shared
let task = session.dataTask(with: todosUrlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling POST on /todos/1")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
do {
guard let receivedTodo = try JSONSerialization.jsonObject(with: responseData,
options: []) as? [String: Any] else {
print("Could not get JSON from responseData as dictionary")
return
}
print("The todo is: " + receivedTodo.description)
guard let todoID = receivedTodo["id"] as? Int else {
print("Could not get todoID as int from JSON")
return
}
print("The ID is: \(todoID)")
} catch {
print("error parsing response from POST on /todos")
return
}
}
task.resume()
}
DELETE METHOD
func makeDeleteCall() {
let firstTodoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
var firstTodoUrlRequest = URLRequest(url: URL(string: firstTodoEndpoint)!)
firstTodoUrlRequest.httpMethod = "DELETE"
let session = URLSession.shared
let task = session.dataTask(with: firstTodoUrlRequest) {
(data, response, error) in
guard let _ = data else {
print("error calling DELETE on /todos/1")
return
}
print("DELETE ok")
}
task.resume()
}
Thanks #MAhipal Singh for you answer. I'll post here example with Alamafire that I used so it's all in one stack question. It's easier than I though, solutions I tried to use before were not working cause I had problems with pinning certificate about I forgot..
func loginRest(login:String, password:String, deviceId:String){
let urlStr = restServices.REST_MAIN_URL + restServices.REST_LOGIN
let params = ["login":login, "password":password, "deviceId":deviceId]
let paramsJson = try! JSONSerialization.data(withJSONObject: params)
var headers: HTTPHeaders = ["Content-Type": "application/json"]
Alamofire.request(urlStr, method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseJSON { (response) in
switch response.result {
case .success:
print("SUKCES with \(response)")
case .failure(let error):
print("ERROR with '\(error)")
}
}
If the post is proper the response is (console print):
SUKCES with SUCCESS: {
firstLogin = 1;
token = "dfkafjkfdsakfadsjfksjkfaadjfkjdfkjfskjfdkafjakfjakfjsafksjdafjy878328hjh";
}

Swift, Check if particular website reachable

How to check reachability of particular website?
I am connected to wifi network for internet access, which have blocked some sites. How to check if I have access to those sites or not?
I have checked with Reachability class, but I can not check for particular website.
Currently I am using Reachability.swift
I don't know what is the best practice, but I use HTTP request to do so.
func checkWebsite(completion: #escaping (Bool) -> Void ) {
guard let url = URL(string: "yourURL.com") else { return }
var request = URLRequest(url: url)
request.timeoutInterval = 1.0
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("\(error.localizedDescription)")
completion(false)
}
if let httpResponse = response as? HTTPURLResponse {
print("statusCode: \(httpResponse.statusCode)")
// do your logic here
// if statusCode == 200 ...
completion(true)
}
}
task.resume()
}
func pageExists(at url: URL) async -> Bool {
var headRequest = URLRequest(url: url)
headRequest.httpMethod = "HEAD"
headRequest.timeoutInterval = 3
let headRequestResult = try? await URLSession.shared.data(for: headRequest)
guard let httpURLResponse = headRequestResult?.1 as? HTTPURLResponse
else { return false }
return (200...299).contains(httpURLResponse.statusCode)
}
The initializer you want to use is listed on that page.
You pass the hostname as a parameter:
init?(hostname: String)
// example
Reachability(hostname: "www.mydomain.com")

Xcode: URL Shared Session not running when unit testing

I have made a simple app which adds data to a database, and then retrieves it. Whilst creating unit testing, it appears that the URLSession.Shared.dataTask is not running. I can see this through the output of the print statements I have setup. Below is my code:
func addChild(childName:String,dob:String,number1:String,number2:String,parentNum:String,parentPass:String,notes:String){
//url to php file
let url = NSURL(string:"http://localhost/addChild.php")
//request to this file
let request = NSMutableURLRequest(url: url as! URL)
//method to pass data to this file
request.httpMethod = "POST"
//body to be appended to url
let body = "childName=\(childName)&dateOfBirth=\(dob)&contact1=\(number1)&contact2=\(number2)&parentAccNum=\(parentNum)&parentAccPass=\(parentPass)&notes=\(notes)"
request.httpBody = body.data(using: String.Encoding.utf8)
print("a")
//launching the request
URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { (data:Data?, response:URLResponse?, error:Error?) in
print("b")
if (error == nil){
print("c")
//send request
//get main queue in code process to communicate back to user interface
DispatchQueue.main.async(execute: {
do{
//get json result
let json = try JSONSerialization.jsonObject(with: data!,options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
print("d")
//assigning json to parseJSON in guard/secure way
//checking whether the parsing has worked
guard let parseJSON = json else{
print("Error while parsing")
return
}
//get id from parseJSON dictionary
let id = parseJSON["id"]
//if there is some id value
if id != nil{
print(parseJSON)
self.success = true
print("success")
}
}
catch{
print("Caught an error:\(error)")
}
} )
}
//if unable to proceed request
else{
print("Error:\(error)")
}
//launch prepared session
}).resume()
}
And then below is my unit testing script:
import XCTest
#testable import computerScienceCoursework
class addChildTest: XCTestCase {
//Setting up the values of the text fields
var testChildName:String = "Test name"
var testDOB:String = "99/99/99"
var testContact1:String = "00000000000"
var testContact2:String = "11111111111"
var testParAccNum:String = "-1"
var testParAccPass:String = "Password"
var testNotes:String = "Insert notes here"
var newChild = AddChildController()
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testAddChildIsWorking(){
//Assigning the values to the text fields
newChild.addChild(childName: testChildName,dob: testDOB,number1: testContact1,number2: testContact2,parentNum: testParAccNum,parentPass: testParAccPass,notes: testNotes)
XCTAssert(newChild.success == true)
}
}
Problem here is that you don´t know when the async task is finished and the success property is getting updated.
There are some possible solutions for your problem one of them is to add a completion handler to your method.
func addChild(childName:String,dob:String,number1:String,number2:String,parentNum:String,parentPass:String,notes:String, completion: (Bool) -> Void){
//url to php file
let url = NSURL(string:"http://localhost/addChild.php")
//request to this file
let request = NSMutableURLRequest(url: url as! URL)
//method to pass data to this file
request.httpMethod = "POST"
//body to be appended to url
let body = "childName=\(childName)&dateOfBirth=\(dob)&contact1=\(number1)&contact2=\(number2)&parentAccNum=\(parentNum)&parentAccPass=\(parentPass)&notes=\(notes)"
request.httpBody = body.data(using: String.Encoding.utf8)
print("a")
//launching the request
URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { (data:Data?, response:URLResponse?, error:Error?) in
print("b")
if (error == nil){
print("c")
//send request
//get main queue in code process to communicate back to user interface
DispatchQueue.main.async(execute: {
do{
//get json result
let json = try JSONSerialization.jsonObject(with: data!,options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
print("d")
//assigning json to parseJSON in guard/secure way
//checking whether the parsing has worked
guard let parseJSON = json else{
print("Error while parsing")
completion(false)
return
}
//get id from parseJSON dictionary
let id = parseJSON["id"]
//if there is some id value
if id != nil{
print(parseJSON)
self.success = true
print("success")
completion(true)
}
}
catch{
print("Caught an error:\(error)")
completion(false)
}
} )
}
//if unable to proceed request
else{
print("Error:\(error)")
completion(false)
}
//launch prepared session
}).resume()
}
Then in your test method you can the method.
func testAddChildIsWorking()
{
let asyncExpectation = expectationWithDescription("addChildIsWorkingFunction")
newChild.addChild(childName: testChildName, dob: testDOB, number1: testContact1,
number2: testContact2, parentNum: testParAccNum, parentPass: testParAccPass, notes: testNotes) { (success) in
asyncExpectation.fulfill()
}
self.waitForExpectationsWithTimeout(10) { error in
XCTAssert(newChild.success == true)
}
}
waitForExpectationWithTimeout is waiting until a fulfill is trigger or a timeout occurs. In this way you could test your async code.
For more informations check this link
Hope that helps.

Resources