Displaying networking error message to user in Swift - swift

The question is how can I make this code reusable especially the error checking in the network method and the condition in the completionhandler, so I don't have duplicate code?
I created a method which makes a network request with URLSession and calls a completion handler with the statuscode as argument. In the completion handling, I created a condition which shows an error message or perfom a segue based on the statuscode. All of this code works but I want to make it reusable so I don't have duplicate code.
Networking method:
func saveMessage(data: String, day: String, completion: #escaping (Int)->()) {
let url = URL(string: "\(Constants.baseURL)/daily_mindset/today_message")
guard let requestUrl = url else { fatalError() }
var request = URLRequest(url: requestUrl)
request.httpMethod = "POST"
// Set HTTP Request Header
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let jsonData = encodeJSON(with: data, day: day)
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
completion(700)
return
}
guard let response = response as? HTTPURLResponse else {
completion(701)
return
}
guard (200...299).contains(response.statusCode) else {
completion(response.statusCode)
return
}
guard let mime = response.mimeType, mime == "application/json" else {
completion(702)
return
}
guard let data = data else {
completion(703)
return
}
do {
let todoItemModel = try JSONDecoder().decode(MessageData.self, from: data)
Constants.currentMindsetId = todoItemModel._id!
print("Response data:\n \(todoItemModel)")
} catch let jsonErr{
print(jsonErr)
}
completion(response.statusCode)
}
task.resume()
}
Calling networking method with completionhandler:
messageManager.saveMessage(data: textView.text, day: day, completion: {(statusCode: Int) -> Void in
if (200...299).contains(statusCode) {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ToDailyMindsetScreen", sender: sender)
}
} else if (400...499).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Please make sure you filled in the all the required fields."
}
} else if (500...599).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, couldn't reach our server."
}
} else if (700...).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, something went wrong. Try again later."
}
}
})
Code in the networking method I want to reuse:
if error != nil {
completion(700)
return
}
guard let response = response as? HTTPURLResponse else {
completion(701)
return
}
guard (200...299).contains(response.statusCode) else {
completion(response.statusCode)
return
}
guard let mime = response.mimeType, mime == "application/json" else {
completion(702)
return
}
guard let data = data else {
completion(703)
return
}
Code in the completionhandler I want to reuse:
if (200...299).contains(statusCode) {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ToDailyMindsetScreen", sender: sender)
}
} else if (400...499).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Please make sure you filled in the all the required fields."
}
} else if (500...599).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, couldn't reach our server."
}
} else if (700...).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, something went wrong. Try again later."
}
}

If the error messages are ViewController specific you can start with creating a function that returns the message based on the status code like this:
private func getErrorMessageFor(statusCode: Int) -> String? {
if (200...299).contains(statusCode) {
//If no error message is returned assume that the request was a success
return nil
} else if (400...499).contains(statusCode) {
return "Please make sure you filled in the all the required fields."
} else if (500...599).contains(statusCode) {
return "Sorry, couldn't reach our server."
} else if (700...).contains(statusCode) {
return "Sorry, something went wrong. Try again later."
} else {
return "Message for other errors?"
}
}
You can always move this code to a ViewController subclass to provide more generic error messages and override it later to provide more detailed errors for a specific View Controller.
class BaseViewController: UIViewController {
func getErrorMessageFor(statusCode: Int) -> String? {
//base implementation here
}
}
class OtherViewController: BaseViewController {
override func getErrorMessageFor(statusCode: Int) -> String? {
//create a new error message only for statusCode 404
if statusCode == 404 {
return "The requested resource was not found on the server. Please contact the support team"
} else {
return super.getErrorMessageFor(statusCode: statusCode)
}
}
}
Keep in mind that as your app grows you might want to create an APIClient that would handle networking and error handling for you. Take a look at https://bustoutsolutions.github.io/siesta/, it is very user friendly

Related

Firebase getIDToken and how to use it in an API call

I have an API call that grabs json, but requires token authentication. Token auth works great, but when I try and pass the token along to the API function, it's coming back nil. I believe it's because Auth.auth().currentUser!.getIDToken(...) hasn't actually completed yet. Relevant code below... How do I modify this to
class SessionData : ObservableObject {
...
func token() -> String? {
var result: String? = nil
Auth.auth().currentUser!.getIDToken(completion: { (res, err) in
if err != nil {
print("*** TOKEN() ERROR: \(err!)")
} else {
print("*** TOKEN() SUCCESS: \(err!)")
result = res!
}
})
return result
}
...
}
class FetchPosts: ObservableObject {
#Published var posts = [Post]()
func load(api: Bool, session: SessionData) {
if api {
let url = URL(string: MyAPI.getAddress(token: session.token()!))!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let postsData = data {
// 3.
let decodedData = try JSONDecoder().decode(Response.self, from: postsData)
DispatchQueue.main.async {
self.posts = decodedData.result
if decodedData.error != nil {
print("ERROR: \(decodedData.error!)")
session.json_error(error: decodedData.error!)
}
}
} else {
print("No data. Connection error.")
DispatchQueue.main.async {
session.json_error(error: "Could not connect to server, please try again!")
}
}
} catch {
print("* Error: \(error)")
}
}.resume()
} else {
let url = Bundle.main.url(forResource: "test", withExtension: "json")!
let data = try! Data(contentsOf: url)
let decoder = JSONDecoder()
if let products = try? decoder.decode([Post].self, from: data) {
self.posts = products
}
}
}
}
And this is how the .load function is called:
UserViewer(fetch: posts)
.transition(AnyTransition.slide)
.animation(.default)
.onAppear {
withAnimation{
posts.load(api: true, session: session)
}
}
.environmentObject(session)
Because getIDToken executes and returns asynchronously, you can't return directly from it. Instead, you'll need to use a callback function.
Here's a modification of your function:
func token(_ completion: #escaping (String?) -> ()) {
guard let user = Auth.auth().currentUser else {
//handle error
return
}
user.getIDToken(completion: { (res, err) in
if err != nil {
print("*** TOKEN() ERROR: \(err!)")
//handle error
} else {
print("*** TOKEN() SUCCESS: \(err!)")
completion(res)
}
})
}
Then, you can use it later on:
.onAppear {
session.token { token in
guard let token = token else {
//handle nil
return
}
withAnimation{
posts.load(api: true, session: session, token: token)
}
}
}
Modify your load to take a token parameter:
func load(api: Bool, session: SessionData, token: String) {
if api {
guard let url = URL(string: MyAPI.getAddress(token: token)) else {
//handle bad URL
return
}
Also, as you can see I'm doing in my code samples, I would try to get out of the habit of using ! to force unwrap optionals. If the optional is nil and you use !, your program will crash. Instead, familiarize yourself with guard let and if let and learn to handle optionals in a way that won't lead to a crash -- it's one of the great benefits of Swift.

Unable to play mp3 in Swift using URLSession's download data task

I've created a sample blank project with single View Controller in main.storyboard and here is implementation below:
import UIKit
import AVFoundation
private enum DownloadErrors: Error {
case invalidRequest
case noResponse
case noTemporaryURL
case noCacheDirectory
case inplayable
case fileNotExists
case system(error: Error)
var localizedDescription: String {
switch self {
case .invalidRequest:
return "invalid request"
case .noResponse:
return "no response"
case .noTemporaryURL:
return "no temporary URL"
case .noCacheDirectory:
return "no cache directory"
case .inplayable:
return "invalid to play"
case .fileNotExists:
return "file not exists"
case let .system(error):
return error.localizedDescription
}
}
}
class ViewController: UIViewController {
// MARK: - View Life Cycle
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.test()
}
// MARK: - Private API
private func test() {
self.download(with: self.request, completion: { result in
switch result {
case let .failure(error):
print("failure: \(error.localizedDescription)")
return
case let .success(url):
self.play(atURL: url)
}
})
}
private let request: URLRequest? = {
var components = URLComponents()
components.scheme = "https"
components.host = "islex.arnastofnun.is"
components.path = "/islex-files/audio/10/1323741.mp3"
guard let url = components.url else {
return nil
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("audio/mpeg", forHTTPHeaderField: "Content-Type")
request.setValue(
"attachment; filename=\"1323741.mp3\"",
forHTTPHeaderField: "Content-Disposition"
)
print(request.url?.absoluteString ?? "invalid URL")
return request
}()
private typealias Callback = (Result<URL, DownloadErrors>) -> Void
private func download(with nilableRequest: URLRequest?, completion: #escaping Callback) {
guard let request = nilableRequest else {
completion(.failure(.invalidRequest))
return
}
let task = URLSession.shared.downloadTask(with: request) { (rawTemporaryFileURL, rawResponse, rawError) in
if let error = rawError {
completion(.failure(.system(error: error)))
return
}
guard let httpStatusCode = (rawResponse as? HTTPURLResponse)?.statusCode else {
completion(.failure(.noResponse))
return
}
print("http status code: \(httpStatusCode)")
guard let sourceFileURL = rawTemporaryFileURL else {
completion(.failure(.noTemporaryURL))
return
}
guard let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
completion(.failure(.noCacheDirectory))
return
}
let targetFileURL = cache.appendingPathComponent("audio_10_1323741.mp3")
if !FileManager.default.fileExists(atPath: targetFileURL.path) {
do {
try FileManager.default.moveItem(at: sourceFileURL, to: targetFileURL)
} catch let error {
completion(.failure(.system(error: error)))
}
}
DispatchQueue.main.async {
completion(.success(targetFileURL))
}
}
task.resume()
}
private func play(atURL url: URL) {
do {
guard FileManager.default.fileExists(atPath: url.path) else {
print("play: \(DownloadErrors.fileNotExists)")
return
}
let player = try AVAudioPlayer(contentsOf: url)
player.volume = 1.0
player.prepareToPlay()
player.play()
print("play: finished")
} catch let error {
print("play error: \(error.localizedDescription)")
}
}
}
What did I do wrong? I have a success response and get no error while trying to create an audio player with an url, however my player doesn't play anything. I am trying to download and play a file immediately from the link:
Audio
My log in console of Xcode:
https://islex.arnastofnun.is/islex-files/audio/10/1323741.mp3
http status code: 200
play: finished
Since you are inside a completion block, you most likely need to enclose your call inside a DispatchQueue block to explicitly run it on the main thread. In short, you do this:
DispatchQueue.main.async {
self.play(atURL: url)
}

Webservice returning nil

I am having trouble populating a UITextField with my returnHTML data that I get from my web-service.
If I have my web-service such that:
import Foundation;
class WebSessionCredentials {
static let requestURL = URL(string:"xxxx.on.ca/getData.aspx?requestType=Tech")!
var htmlbody: String?
var instancedTask: URLSessionDataTask?
static var sharedInstance = WebSessionCredentials()
init() {
self.instancedTask = URLSession.shared.dataTask(with: WebSessionCredentials.requestURL) { [weak self] (data,response,error) in
if let error = error {
// Error
print("Client Error: \(error.localizedDescription)")
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
print("Server Error!")
return
}
guard let mime = response.mimeType, mime == "text/html" else {
print("Wrong mime type!");
return
}
if let htmlData = data, let htmlBodyString = String(data: htmlData, encoding: .utf8) {
self?.htmlbody = htmlBodyString;
};
};
};
};
Through this I should be able to access the returned HTML response through WebSessionCredentials.sharedInstance.htmlbody;
Verifying this in playground I seem to be getting the correct response within the class but when calling htmlbody from outside the class I get a nil response - I am out of ideas in terms of how to send that HTML string that I get from the class to outside the function. This question is built off another question I have posted a couple days earlier -> Delegating privately declared variables to a public scope
Thanks,
Rather than implementing the dataTask in the init method add a method run with completion handler
class WebSessionCredentials {
enum WebSessionError : Error {
case badResponse(String)
}
static let requestURL = URL(string:"xxxx.on.ca/getData.aspx?requestType=Tech")!
static var sharedInstance = WebSessionCredentials()
func run(completion : #escaping (Result<String,Error>) -> Void) {
let instancedTask = URLSession.shared.dataTask(with: WebSessionCredentials.requestURL) { (data,response,error) in
if let error = error {
// Error
print("Client Error: \(error.localizedDescription)")
completion(.failure(error))
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
completion(.failure(WebSessionError.badResponse("Server Error!")))
return
}
guard let mime = response.mimeType, mime == "text/html" else {
completion(.failure(WebSessionError.badResponse("Wrong mime type!")))
return
}
completion(.success(String(data: data!, encoding: .utf8)!))
}
instancedTask.resume()
}
}
And use it
WebSessionCredentials.sharedInstance.run { result in
switch result {
case .success(let htmlBody): print(htmlBody)
case .failure(let error): print(error)
}
}

How to create proper completion handler for server login in swift?

I have an api manager class in my swift application and it has a server login with username and password.
I want to know how to create a completion handler for it that when the server responses with 200 status code, the function handles that response and for example performs a segue in the viewcontroller.
I did not find any tutorials for this. Thanks for your help!
EDIT 1:
What i need is: The completion handler is immediately run when the function is called. I want the completion handler run after server responds.
And this is my login function:
public class func Login(username: String, password: String, complitionHandler: #escaping (Int) -> Void) {
let urlS = "http://server.com/" + "login.php"
let url = URL(string: urlS)
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let body = "username=\(username.lowercased())&password=\(password)"
request.httpBody = body.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
print(error!)
print("error")
logedIn = 2
return
}
do{
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSDictionary
if let parseJson = json {
let code = parseJson["status"] as! String
if code == "200" {
print("loged inn")
logedIn = 1
}else if code == "400" {
print("uuuser/pass error")
logedIn = 0
}
}
}catch{
print("json error")
logedIn = 2
}
}
task.resume()
DispatchQueue.main.async {
complitionHandler(logedIn)
}
}
And how i call the function in my ViewController:
Manager.Login(username: "1", password: "1") { (i) in
switch i {
case 0:
print("user/pass error")
case 1:
print("loged in")
self.performSegue(withIdentifier: "toMain", sender: self)
case 2:
print("json error")
default:
()
}
}
You have all of the pieces in place. You just need to move your call to the completion handler to the correct place:
}catch{
print("json error")
logedIn = 2
}
DispatchQueue.main.async {
complitionHandler(logedIn)
}
}
task.resume()
Also note that method names should start with lowercase letters so your Login function should be named login.
Now you can use this login method like:
login(username: someUsername, password: somePassword) { (result) in
if result == 1 {
// success - do your segue
} else if result == 0 {
// bad username/password
} else {
// some error
}
}

a function does internet fetching and return a value

I know the following piece of code is wrong, but I want to show my intent.
I want to write a method that will be called by multiple times. and this fetching method will tell me if it is successfully reached.
func fetch(url: String) -> Bool? {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
var bool: Bool? = nil
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
bool = true
} else {
print("something really wrong")
bool = false
}
}
}).resume()
}
return bool
}
if let bool = fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD"), bool == true {
// if it is true, I can go for next step.
}
Making the UI wait on completion of some API call is not recommended. The app will have no control over how long that API call will take. Situations with bad network connectivity can take several seconds to respond.
You can handle a situation like this is to use a completion handler.
func fetch(url: String, completion: #escaping (_ success: Bool) -> Void) {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
completion(true)
} else {
print("something really wrong")
completion(false)
}
}).resume()
}
}
func testFetch () {
fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD") { (success) in
// if it is true, I can go for next step.
DispatchQueue.main.async {
if success {
// it was good
}
else {
// not good
}
}
}
}