How to save data in userdefaults when is from a model - ios

I have a Model like this
struct House: Codable {
let address: String?
let rooms: String?
let garage: Garage
}
struct Garage: Codable {
let space: String?
let numbers_of_ cars: String?
}
I get from a api the data and set to can show it,
now I need to save in userdefaults to can show info if there is not any network o wifi
I am trying like this
var itemItems = [House]()
fileprivate func fetchData() {
Service.shared.fetchCourses { (houses, err) in
if let err = err {
print("Failed to fetch houses:", err)
return
}
self.itemItems = houses!
var itemsToSave = [House]()
for i in 0..<self.itemItems.count {
itemsToSave.append(self.itemItems[i])
}
UserDefaults.standard.set(arrayToSave, forKey : "arrayToSave")
}
}
but!! I get an error
check the image
https://i.ibb.co/bHhtj28/Captura-de-Pantalla-2019-11-24-a-la-s-4-25-18-p-m.png

You cannot save custom structs to UserDefaults directly.
But as both structs conform to Codable encode them
var itemItems = [House]()
fileprivate func fetchData() {
Service.shared.fetchCourses { (houses, err) in
if let err = err {
print("Failed to fetch houses:", err)
return
}
self.itemItems = houses!
do {
let data = try JSONEncoder().encode(self.itemItems)
UserDefaults.standard.set(data, forKey : "houses")
} catch { print(error) }
}
}

Related

Cannot find 'ModelA' in scope using generic types

I have a call api function and parameter using generic types.
And I alse craete codable data model.
Why the function parameter don't get my custom struct model and get the error Cannot find 'ModelA' in scope.
Is my T type error?
I don't know how to fix it.
Thanks.
struct ResponseHeader :Codable {
let returnCode : String?
let returnMsg : String?
}
struct ModelA :Codable {
let responseHeader : ResponseHeader?
let responseBody : ResponseBody?
struct ResponseBody: Codable {
let name : String?
let age : String?
let email: String?
}
}
enum APIRouter: String {
case apiA = "http://localhost:3000/ApiA"
case apiB = "http://localhost:3000/ApiB"
case apiC = "http://localhost:3000/ApiC"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.callApi(apiRouter: .apiA, model: ModelA) //Error. Cannot find 'ModelA' in scope
}
func callApi<T: Codable>(apiRouter: APIRouter, model: T.Type) {
let urlString = URL(string: apiRouter.rawValue)
if let url = urlString {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else { return }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
if let data = data {
do {
let response = try decoder.decode(model.self, from: data)
print(response)
} catch {
print(error)
}
} else {
print("Error")
}
}
task.resume()
}
}
}
Add self at the end.
This generic function takes as an argument an Instance Type of model so you have to pass ModelA.self.
self.callApi(apiRouter: .apiA, model: ModelA.self) //Here

Got this error: nw_protocol_get_quic_image_block_invoke dlopen libquic failed

I was trying to connect my API data to view it in the cell but it seems that I can't get my response and it's always == nil
The code below describes the Country.SWIFT // Model.SWIFT // Response.SWIFT which is showing how can I get my JSON response using Codable
and CountryCell.SWIFT is showing how I used it to call the API
The link to the API image:
Country.SWIFT
struct Country: Decodable {
var CountryName = ""
var CountryImage = ""
var objectId = ""
// MARK: - Coding Keys
enum CodingKeys: String, CodingKey {
case CountryName = "CountryName"
case CountryImage = "CountryImage"
case objectId = "objectId"
}
//MARK: - Json Decoder
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Parsing our attributes
self.CountryName = try container.decode(String.self, forKey: .CountryName)
self.CountryImage = try container.decode(String.self, forKey: .CountryImage)
self.objectId = try container.decode(String.self, forKey: .objectId)
}
}]
Model.SWIFT
protocol ModelDelegate {
func countriesFetched (_ countries: [Country])
}
class Model {
//MARK: - Vars
var delegate: ModelDelegate?
// MARK: - Get Countries
func getCountries () {
// URL Object
let url = URL(string: Constants.API_URL)
guard url != nil else {return}
// URL Session object
let session = URLSession.shared
//Data Task from URLSession object
let dataTask = session.dataTask(with: url!) { (data, response, error) in
if error != nil || data == nil {
print(error!.localizedDescription)
return
}
print(data!)
do {
let decoder = JSONDecoder()
let response = try decoder.decode(Response.self, from: data!)
if response.items != nil {
DispatchQueue.main.async {
self.delegate?.countriesFetched(response.items!)
}
}
}
catch {
}
}
// start data task
dataTask.resume()
}
}
Response.SWIFT
struct Response: Decodable {
var items: [Country]? = []
init(from decoder: Decoder) throws {
var itemsContrainer = try decoder.unkeyedContainer()
self.items = try itemsContrainer.decode([Country].self)
}
}
CountryCell.SWIFT
class CountryCell: UICollectionViewCell {
//MARK: - Vars
var country: Country?
//MARK: - Outlets
#IBOutlet weak var imageViewCountryOutlet: UIImageView!
#IBOutlet weak var lblCountryNameOutlet: UILabel!
//MARK: - Creating Cell
func generateCell (_ myCountry: Country) {
self.country = myCountry
guard self.country != nil else { return }
lblCountryNameOutlet.text = country!.CountryName
guard self.country!.CountryImage != "" else {return}
let url = URL(string: self.country!.CountryImage)
guard url != nil else {return}
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) { (data, response, error) in
if error == nil || data != nil {
if url!.absoluteString != self.country!.CountryImage {
return
}
let image = UIImage(data: data!)
DispatchQueue.main.async {
self.imageViewCountryOutlet.image = image
}
}
}
dataTask.resume()
}
}
There is no need for the wrapping Response type, decode the list of countries directly:
let decoder = JSONDecoder()
let items = try decoder.decode([Country].self, from: data!)
In your code, when you ask in Response for unkeyedContainer, the expected JSON structure would need additional nested array.
Check decoding errors in the catch block with
do {
...
}
catch {
print(error)
}
I know it sounds bit absurd but in my case, by using class instead of struct and declare all attributes with public var worked for me.

JSON throws nil on every request

First time I'm trying to parse JSON data. The output comes out -
Course (success: zero, timestamp: nil, base: nil, date: nil, courses: nil)
Why is there nil everywhere?
I tried to change the value in the "Course" and "Currency" structures but did not lead to success
import UIKit
import JavaScriptCore
struct Course: Decodable {
var success: Bool?
var timestamp: Int?
var base: String?
var date: String?
var rates: Currency?
}
struct Currency: Decodable {
var USD: Float
var AUD: Double
var CAD: Double
var PLN: Double
var MXN: Double
}
class JsonViewContoller: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let urlData: String = "http://data.fixer.io/api/latest?access_key=7ac2982c82da926b787fd2f089b110e5&symbols=USD,AUD,CAD,PLN,MXN&format=1"
guard let url = URL(string: urlData) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard error == nil else { return }
do {
let course = try JSONDecoder().decode(Course.self, from: data)
print(Course())
} catch let error {
print(error)
}
}.resume()
}
}
I have run your code you don't need to use two structures to parse the above JSON data. You can easily get Data in a single Structure. I have modified your code as follows:-
import UIKit
struct Course: Codable {
let success: Bool?
let timestamp: Int?
let base, date: String?
let rates: [String: Double]?
}
class JsonViewContoller: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
courseData()
}
fileprivate func courseData(){
let urlData: String = "http://data.fixer.io/api/latest?access_key=7ac2982c82da926b787fd2f089b110e5&symbols=USD,AUD,CAD,PLN,MXN&format=1"
guard let url = URL(string: urlData) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard error == nil else { return }
do {
let course = try JSONDecoder().decode(Course.self, from: data)
print(course)
} catch let error {
print(error)
}
}.resume()
}
}

Storing API data into UserDefaults and printing to a list

In my app, users scan a barcode and the information about the product is fetched from an API.
I want to create a history section, where users can view the last 10 products.
The result from the API data is stored in a Result type, which for it to be able to be shown in a list, has to be identifiable.
Result is a custom data type that I'm using to store the details of the products from the API call in.
Result
struct Result: Codable, Identifiable {
var id = UUID()
var description: String?
var brand: String?
var ingredients: String?
var image: String?
var upc_code: String?
var return_message: String?
var return_code: String?
enum CodingKeys: String, CodingKey {
case description, brand, ingredients, image, upc_code, return_message, return_code
}
}
This data types store the array of Result which I'll display as a list
History
struct History: Codable {
var results: [Result]
}
Here's the API call:
func loadData(url: String, completion: #escaping (Error?, Result?) -> Void ) {
if let url = URL(string: url) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {return}
do {
let defaults = UserDefaults.standard
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(data) {
var sizeCheck = defaults.object(forKey:"productHistory") as? [Data] ?? [Data]()
if (sizeCheck.count == 10) { //Check if there's more than 10 products already on the history list
sizeCheck.removeLast()
}
sizeCheck.append(encoded) //Add new product to list
defaults.set(sizeCheck, forKey: "productHistory") //Add new list to userDefaults
}
let decoder = JSONDecoder()
let result: Result = try decoder.decode(Result.self, from: data)
completion(nil, result) //Used elsewhere to display the scanned product after it's been added to the history list
}
catch let e {
print(e)
completion(e, nil)
}
}
task.resume()
}
}
This is my view that shows the last 10 products in a list when a button is pressed.
The last 10 products should be stored in UserDefaults with the key productHistory. This is done in the API call LoadData()
struct historyView: View {
#Binding var showingHistory: Bool
#State private var results = [Result]()
var body: some View {
let defaults = UserDefaults.standard
if let products = defaults.object(forKey: "productHistory") as? Data {
if let decodedResponse = try? JSONDecoder().decode(History.self, from: products) {
self.results = decodedResponse.results
}
}
return List(self.results, id: \.id) { item in
Text(item.description!)
}
}
}
To my understanding, the issue is that UserDefaults can't store JSON data. So when the API data is fetched, I store the data as it is, into userdefualts. Then decode it when I need it, like storing it in history or displaying it.
Currently I'm getting a blank list and the if statement below isn't passing.
if let decodedResponse = try? JSONDecoder().decode(History.self, from: products) {
Here's the JSON data from the API if I paste the URL into the browser:
EDIT
Here's my APICall():
func callAPI() -> String {
if (scannedCode.barcode == "") {
return "noneScanned"
}
else {
let hashedValue = scannedCode.barcode.hashedValue("API ID")
//print(hashedValue!)
loadData(url: "URL") { error, result in
if let err = error {
self.APIresult = err.localizedDescription
print(APIresult)
//output error
}
else if (result?.ingredients == nil) {
DispatchQueue.main.async {
self.APIresult = "noIngredients"
}
}
else if (result?.description == nil) {
DispatchQueue.main.async {
self.APIresult = "noDescription"
}
}
else {
DispatchQueue.main.async {
self.APIresult = "success"
}
}
DispatchQueue.main.async {
product.result = result!
//updates view that show's the scanned product, as it's #Published
}
}
return APIresult
}
}
In this section, I want to find what data I have about the product and process it accordingly. Therefore with the solution above, I return a different value depending on if it's got a image or a description etc...
With vadian solution, I've changed it to this:
loadData(url: "URL") { result in
switch result {
case .success(product):
print("success")
case .failure(error):
print("failure")
}
}
As mentioned in the comments you are mixing up Data and Result
First of all drop History and rename Result as Product. We are going to save an array of Product to UserDefaults
struct Product: Codable, Identifiable {
var id = UUID()
var description: String?
var image: String?
var upc_code: String?
var return_message: String?
var return_code: String?
private enum CodingKeys: String, CodingKey {
case description, image, upc_code, return_message, return_code
}
}
In loadData use the generic Result type as closure parameter. After receiving the data decode it to a Product instance, then load the saved array, remove the first(!) item (if necessary) append the new item, save the array back and call completion with the new Product. All potential errors are passed in the failure case.
func loadData(url: String, completion: #escaping (Result<Product,Error>) -> Void ) {
guard let url = URL(string: url) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { completion(.failure(error)); return }
do {
let decoder = JSONDecoder()
let product = try decoder.decode(Product.self, from: data!)
let defaults = UserDefaults.standard
var history = [Product]()
if let readData = defaults.data(forKey:"productHistory") {
do {
history = try decoder.decode([Product].self, from: readData)
if history.count == 10 { history.removeFirst() }
} catch { print(error) }
}
history.append(product)
let saveData = try JSONEncoder().encode(history)
defaults.set(saveData, forKey: "productHistory")
completion(.success(product))
}
catch {
print(error)
completion(.failure(error))
}
}
task.resume()
}
and call it
loadData(url: "URL") { result in
switch result {
case .success(let product):
if product.ingredients == nil {
self.APIresult = "noIngredients"
} else if product.description == nil {
self.APIresult = "noDescription"
} else {
self.APIresult = "success"
}
product.result = product
case .failure(let error):
self.APIresult = error.localizedDescription
print(APIresult)
}
}
In HistoryView (please name structs with starting uppercase letter) get the data from UserDefaults and decode the Product array.
struct HistoryView: View {
#Binding var showingHistory: Bool
#State private var results = [Product]()
var body: some View {
let defaults = UserDefaults.standard
if let historyData = defaults.data(forKey: "productHistory") {
do {
self.results = try JSONDecoder().decode([Product].self, from: historyData)
} catch { print(error) }
}
return List(self.results, id: \.id) { item in
Text(item.description ?? "n/a")
}
}
}
Note: Be aware that the UUID is not being encoded and saved.
And please use more descriptive variable names.

Swift decodable can't access nested data in array

I get a critical error stating the following. I've tried everything but I can't seem to access the Movie struct as it says the parent 'Type' has no member called 'data', even though it clearly does.
"Value of type '[Type?]' has no member 'data'"
MODEL
struct SearchData: Decodable {
let data: [Type?]
}
struct Type: Decodable {
let data: [Movie?]
}
struct Movie: Decodable {
let title: String?
}
CONTROLLER
fileprivate var searchResults = [Movie?]()
func fetchTitles() {
let urlString = "https://www.what-song.com/api/search?limit=10&field=america"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// if error occurs
if let err = err {
print("Failed to fetch titles", err)
return
}
// if success
guard let data = data else { return }
do {
let searchResult = try JSONDecoder().decode(SearchData.self, from: data)
self.searchResults = searchResult.data.data
print(searchResult)
} catch {
print("Failed to decode JSON:", error)
}
}.resume()
}
Try this :
var movieTitles = [String]()
for type in searchResult.data {
for movie in type.data {
guard let title = movie.title else { return }
print(title)
movieTitles.append(title)
}
}
you are doing a small mistake here I think
searchResult.data
will return you an array of
Type
You need to parse that array as well, something like this
searchResults = (searchResult.data[0]?.data)!

Resources