How do I can save data downloaded from internet to internal device? - ios

I have this code:
struct ServerConnect {
enum Result<T> {
case succes(T)
case error(String)
}
typealias completionHandler = (Result<Data >) -> ()
func getJsonFromServer(parameters: String, completion: #escaping completionHandler) {
let fullUrlString = ApiConstans.fullPath + parameters
guard let url = URL(string: fullUrlString) else {
debugPrint("\(ErrorsLabels.ServerConnect01)")
return completion(.error("\(ErrorsLabels.ServerConnect01)"))
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {
debugPrint("\(ErrorsLabels.ServerConnect02)")
return completion(.error("\(ErrorsLabels.ServerConnect02)"))
}
guard let data = data else {
debugPrint("\(ErrorsLabels.ServerConnect03)")
return completion(.error("\(ErrorsLabels.ServerConnect03)"))
}
debugPrint("R> \(fullUrlString)")
return completion(.succes(data))
}.resume()
}
func getJsonProducts(lang: String?, region: Int?, completion: #escaping completionHandler) {
self.getJsonFromServer(parameters: "?action=GET_PRODUCTS&lang=\(lang!)&region=\(region!)", completion: completion)
}
}
I would like to save the downloaded data from the internet in the device's memory.
I'm trying this code:
getJsonProducts(lang: selectedLanguage, region: selectedRegion , completion: { (data) in
print("##### \(data)")
saveJsonFileToTheDisk(path: selectedLanguage + "/json/products.json", downloadData: data)
})
func saveJsonFileToTheDisk(path: String, downloadData: Data){
do {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsURL.appendingPathComponent(path)
try downloadData.write(to: fileURL, options: .atomic)
} catch { }
}
But unfortunately it does not work. How can I make it work?
Below is an error:
Cannot convert value of type 'ServerConnect.Result' to expected
argument type 'Data'

According to the definition of getJsonProducts method:
func getJsonProducts(lang: String?, region: Int?, completion: #escaping (Result<Data>) -> ()) {
The parameter data which is sent to completion block is actually of type Result containing the data, not the data itself. Thus, you should do:
getJsonProducts(lang: selectedLanguage, region: selectedRegion, completion: { result in
switch result {
case .succes(let data):
saveJsonFileToTheDisk(path: selectedLanguage + "/json/products.json", downloadData: data)
case .error(let errorMessage):
// Do something with the error message.
break
}
})

Your completionHandler from getJsonProducts returns a Result<Data>, not a simple Data, so you need to pattern match the Result instance, since it might not actually contain Data, it might contain a String error.
getJsonProducts(lang: selectedLanguage, region: selectedRegion , completion: { (data) in
switch data {
case let .succes(data):
print("##### \(data)")
saveJsonFileToTheDisk(path: selectedLanguage + "/json/products.json", downloadData: data)
case let .error(error):
print(error)
}
})

Related

No value associated with key CodingKeys - JSONDecoder() Error

Here I have 3 files loginView(SwiftUI file) for UI purpose, LoginViewModel for handling the logic, ServiceManager for handling the Network call
Below code is in loginView(SwiftUI file)
Button("Login") {
loginVM.loginWebserviceCall()
}
Below code is in loginVM class
protocol LoginViewModelService: AnyObject {
func getLoginWebServiceCall(url: URL, params: [String: Any], completion: #escaping (Result<LoginRequest, APIError>) -> ())
}
class LoginViewModel: ObservableObject {
private weak var movieService: LoginViewModelService?
#Published var error: NSError?
init(movieService: LoginViewModelService = LoginStore.shared) {
self.movieService = movieService
}
fileprivate func loginWebserviceCall() {
let loginParams = ["username": "userEnteredUserName", "password": "userEnteredPassword", "request_token": "token"]
self.movieService!.getLoginWebServiceCall(url: "API_URL",
params: loginParams) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let response):
print(response)
case .failure(let error):
self.error = error as NSError
}
}
}
}
class LoginStore: LoginViewModelService {
static let shared = LoginStore()
private init() {}
func getLoginWebServiceCall(url: URL, params: [String: Any], completion: #escaping (Result<LoginRequest, APIError>) -> ()) {
ServiceManager.shared().requestWebServiceCall(requestType: .POST, url: url, params: params, completion: completion)
}
}
Below code is in ServiceManager class
class ServiceManager: NSObject {
private static var manager: ServiceManager? = nil
static func shared() -> ServiceManager {
if manager == nil {
manager = ServiceManager()
}
return manager!
}
func requestWebServiceCall<Response: Decodable>(requestType: HTTPMethod,
url: URL, params: [String: Any]? = nil,
completion: #escaping (Result<Response, APIError>) -> ()) {
var urlRequest = URLRequest.init(url: url)
if let params = params {
let postData = try? JSONSerialization.data(withJSONObject: params, options: .init(rawValue: 0))
urlRequest.httpBody = postData
}
urlRequest.httpMethod = requestType.rawValue
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: urlRequest) { [weak self] (data, response, error) in
guard let self = self else { return }
guard let data = data else {
self.executeCompletionHandlerInMainThread(with: .failure(.noData), completion: completion)
return
}
do {
if let str = String(data: data, encoding: .utf8) {
print(str)
// Output: {"success":true,"expires_at":"2022-06-23 08:56:52 UTC","request_token":"6587563498567begjhgf3r5387853"}
}
let decodedResponse = try JSONDecoder().decode(Response.self, from: data)
self.executeCompletionHandlerInMainThread(with: .success(decodedResponse), completion: completion)
} catch let DecodingError.keyNotFound(key, context) {
print("Key '\(key)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch {
print(error)
}
}.resume()
}
private func executeCompletionHandlerInMainThread<Response: Decodable>(with result: Result<Response, APIError>,
completion: #escaping (Result<Response, APIError>) -> ()) {
DispatchQueue.main.async {
completion(result)
}
}
}
Below is the JSON we are expecting as response
{
"success": true,
"expires_at": "2018-07-24 04:10:26 UTC",
"request_token": "1531f1a558c8357ce8990cf887ff196e8f5402ec"
}
But once I get response, the decoding is getting failed and it is going inside catch block(in ServiceManager class) and it print's below error.
Key 'CodingKeys(stringValue: "username", intValue: nil)' not found: No value associated with key CodingKeys(stringValue: "username", intValue: nil) ("username").
codingPath: []
It is showing username as not found. But in my API response, I don't have username at all.
But I am passing username as httpBody to this API.
What could be the reason? Why it is throwing error?

Return MyRŠµsults from result: Result<[MyResults], Error>?

After trying to fetch all pending favors, I want to save the data in a variable and somehow print it in the table view. Still not sure how to do it, but currently I cant "save" MyResults in a variable to return? Can someone please help me? and also, Ive tried searching but cant find what type of data structure is (Result<T, Error>).
extension URLSession {
func fetchData<T: Decodable>(for url: URL, completion: #escaping (Result<T, Error>) -> Void) {
self.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
if let data = data {
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(.success(object))
} catch let decoderError {
completion(.failure(decoderError))
}
}
}.resume()
}
}
func fetchAllFavor()->MyResults
{
let url = URL(string: get_all_pending_favors_url)!
URLSession.shared.fetchData(for: url) { (result: Result<[MyResults], Error>) in
switch result {
case .success(let MyResults):
break
// A list of todos!
case .failure(let error):
break
// A failure, please handle
default:
print("unknown")
}
}
}
This sort of question comes up all the time. You can't return a result from an async function. An async function like dataTask(with:) or your fetchData() function takes a completion handler, which is a closure that it calls once the results are available.
Your should rewrite your fetchAllFavor() function following a similar pattern:
func fetchAllFavor(completion: #escaping (Result<MyResults,Error>) -> Void) {
let url = URL(string: get_all_pending_favors_url)!
URLSession.shared.fetchData(for: url) { (result) in
completion(result)
}
}

How to download a .zip file in Swift without NSURLSession?

I'm trying to download a zip file to the user's phone storage in an iOS app. Is it possible to do this without NSURLSession?
Yes, there are multiple tools but you should still try and use URL session.
A very easy way to do this is using Data. But it blocks your thread so you need to work a bit with queues to make it work properly (Otherwise your app MAY crash).
A very simple, non-safe, thread-blocking way would be:
func saveFile(atRemoteURL remoteURL: URL, to localURL: URL) {
let data = try! Data(contentsOf: remoteURL)
try! data.write(to: localURL)
}
But doing it a bit more stable should look something like this:
private func downloadIteam(atURL url: URL, completion: ((_ data: Data?, _ error: Error?) -> Void)?) {
let queue = DispatchQueue(label: "downloading_file")
queue.async {
do {
let data = try Data(contentsOf: url)
completion?(data, nil)
} catch {
completion?(nil, error)
}
}
}
private func saveDataToDocuments(_ data: Data, to: String, completion: ((_ resultPath: URL?, _ error: Error?) -> Void)?) {
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(to)
let queue = DispatchQueue(label: "saving_file")
queue.async {
do {
let folderPath: String = path.deletingLastPathComponent().path
if !FileManager.default.fileExists(atPath: folderPath) {
try FileManager.default.createDirectory(atPath: folderPath, withIntermediateDirectories: true, attributes: nil)
}
try data.write(to: path)
completion?(path, nil)
} catch {
completion?(nil, error)
}
}
}
public func downloadFileAndSaveItToDocuments(urlToRemoteFile: URL, completion: #escaping (_ file: (relativePath: String, fullPath: URL)?, _ error: Error?) -> Void) {
let fileName = urlToRemoteFile.lastPathComponent
let relativePath = "downloads/" + fileName
func finish(fullPath: URL?, error: Error?) {
DispatchQueue.main.async {
if let path = fullPath {
completion((relativePath, path), error)
} else {
completion(nil, error)
}
}
}
downloadIteam(atURL: urlToRemoteFile) { (data, error) in
guard let data = data else {
completion(nil, error)
return
}
saveDataToDocuments(data, to: relativePath) { (url, saveError) in
finish(fullPath: url, error: saveError ?? error)
}
}
}
I hope the code is self-documented enough.

How to check and cast a type to Decodable in swift

I want to make a networking request in which I could control the response is JSON raw value [String: Any or the Decodable.
Here is the example:
func reqest<T>(endpoint: EndPoint, completion: #escaping (Result<T, Error>) -> Void) {
session.dataTask(with: request) { (data, response, error) in
if T.self is Decodable.Type {
try? JSONDecoder().decode(T.self, from: data)
} else {
try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
}
}.resume()
}
then when I want JSON value I just call with T as [String: Any] or just use any model confirm Decodable protocol.
The question is for this line:
try? JSONDecoder().decode(T.self, from: data)
How to cast T to Decodable?
I know to use:
func reqest<T: Decodable >(endpoint: EndPoint, completion: #escaping (Result<T, Error>) -> Void)
but [String: Any] isn't Decodable.
Or any better solution to achieve what I want? thanks.
I would recommend to use overload for the reqest(endpoint:completion:) function to achieve what you want.
A structure that I would like for example is this:
enum ResponseError: Error {
case noData
case typeMismatch
}
func reqest<T>(endpoint: EndPoint, completion: #escaping (Result<T, Error>) -> Void) {
baseReqest(endpoint: endpoint) { result in
switch result {
case .success(let data):
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? T else {
completion(.failure(ResponseError.typeMismatch))
return
}
completion(.success(json))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func reqest<T: Decodable>(endpoint: EndPoint, completion: #escaping (Result<T, Error>) -> Void) {
baseReqest(endpoint: endpoint) { result in
switch result {
case .success(let data):
do {
let response = try JSONDecoder().decode(T.self, from: data)
completion(.success(response))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
private func baseReqest(endpoint: EndPoint, completion: #escaping (Result<Data, Error>) -> Void) {
session.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(ResponseError.noData))
return
}
completion(.success(data))
}.resume()
}
That way you can have generic response handling code in baseReqest(endpoint:completion:) function and separate only the response parsing in the other two functions.
Then calling reqest(endpoint:completion:) function could be
using [String: Any] as response type:
reqest(endpoint: endpoint) { (result: Result<[String: Any], Error>) in
// Handle result
}
using [[String: Any]] as response type:
reqest(endpoint: endpoint) { (result: Result<[[String: Any]], Error>) in
// Handle result
}
and also using a Decodable object as response type:
struct Response: Decodable {}
reqest(endpoint: endpoint) { (result: Result<Response, Error>) in
// Handle result
}
Do it in Swift way. Use the struct Codable (that gives you the Decodable together).
For example:
struct testStruct: Codable {
public var testString:String!
public var testAny:Any!
init(
testString:String!,
testAny:Any!
)
{
self.testString = testString
self.testAny = testAny
}
Then you initialize it with this:
var testStructToUse:[testStruct] = []
From here you can populate it with append method:
testStructToUse.append(testStruct(testString: "any String", testAny: "any value"))
And encode with JSONencoder
let jsonData = try JSONEncoder().encode(testStruct)

Completion handler swift 3 return a variable from function

I am confused surrounding the syntax for a completion handler in swift 3.
In the function below, after parsing an xml file from a web service call, it should return a variable (an array [String:String]).
My attempt is below, but obviously it is incorrect.
enum HistoryKey {
case success([String:String])
case failure(String)
}
private func getHistoryKeys(searchterm: String, completion: #escaping () -> HistoryKey) {
let url = PubmedAPI.createEsearchURL(searchString: searchterm)
let request = URLRequest.init(url: url as URL)
let task = session.dataTask(with: request) { (data, response, error) in
if let theData = data{
let myParser = XMLParser.init(data: theData)
myParser.delegate = self
myParser.parse()
}
}
task.resume()
if keys.isEmpty {
return .failure("no historyKeyDictionary")
}else{
return .success(keys)
}
}// End of func
I want to use this function as follows
let result = self.getHistoryKeys(searchTerm)
Two issues:
The completion handler passes a HistoryKey instance and has no return value so the signature must be the other way round.
The call of the completion handler must be inside the completion block of the data task.
To be able to parse the received data outside the completion block return the data on success
enum ConnectionResult {
case success(Data)
case failure(Error)
}
private func getHistoryKeys(searchterm: String, completion: #escaping (ConnectionResult) -> ()) {
let url = PubmedAPI.createEsearchURL(searchString: searchterm)
let task = session.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
} else {
completion(.success(data!))
}
}
task.resume()
}
and call it
getHistoryKeys(searchterm: String) { connectionResult in
switch connectionResult {
case .success(let data):
let myParser = XMLParser(data: data)
myParser.delegate = self
myParser.parse()
// get the parsed data from the delegate methods
case .failure(let error): print(error)
}
}
You are not using completion block.
Use it like:
private func getHistoryKeys(searchterm: String, completion: #escaping (_ keys: Array) -> Void) {
//do the magic
completion(keys)
}
Then you can call this function as:
getHistoryKeys(searchterm: "str") { (keys) in
print("\(keys)")
}
Swift 4.2
enum HistoryKey {
case success([String:String])
case failure(String)
}
func myFunction(str: String, completionHandler: #escaping (HistoryKey) -> ()){
completion(.success([String:String]))
//OR
completion(.failure(""))
}
myFunction(str: String) { result in
switch result {
case .success(let data): break;
case .failure(let error): break;
}
}
OR
func myFunction(str: String, completionHandler: #escaping (String) -> ()){
completionHandler("")
}
myFunction(str: "someThing", completionHandler: {(str) in
})
Return the result as an argument in the completion handler:
private func getHistoryKeys(searchterm: String, completion: #escaping (result: HistoryKey) -> Void) {
let url = PubmedAPI.createEsearchURL(searchString: searchterm)
let request = URLRequest.init(url: url as URL)
let task = session.dataTask(with: request) { (data, response, error) in
if let theData = data{
let myParser = XMLParser.init(data: theData)
myParser.delegate = self
myParser.parse()
}
//DispatchQueue.main.async { // if you want to do UI stuff dispatch calls to completion() on the main queue
if keys.isEmpty {
completion(.failure("no historyKeyDictionary"))
} else{
completion(.success(keys))
}
//}
}
task.resume()
}
And call it like this:
getHistoryKeys("searchMe") { (result: HistoryKey) in
print(result)
}
enum HistoryKey {
case success([String: String])
case failure(String)
}
private func getHistoryKeys(searchterm: String, completion: #escaping (_ result: HistoryKey) -> Void) {
let url = PubmedAPI.createEsearchURL(searchString: searchterm)
let request = URLRequest.init(url: url as URL)
let task = session.dataTask(with: request) { (data, response, error) in
if let theData = data{
let myParser = XMLParser.init(data: theData)
myParser.delegate = self
myParser.parse()
}
if keys.isEmpty {
completion(.failure("no historyKeyDictionary"))
} else {
completion(.success(keys))
}
}
task.resume()
} // End of func
Something like that. Change #escaping declaration and do a completion instead of return.
Hope it helps.
From what I can see, it should be
private func getHistoryKeys(searchterm: String, completion: #escaping (HistoryKey) -> ())
Also completion(.failure("no historyKeyDictionary")) or completion(.success(keys)) should be used instead of return

Resources