Any pointers would be much appreciated. No build or run errors with the code below, but 239 "Fail" prints in the Console. Also, once I get it to work properly, this code should be in a separate function or extension and not part of the View, and for reusability. I am expecting to see a country's flag next to its name in a Picker listing. The 'country.name' displays and 'selectedCountry' can be selected for subsequent Core Data storage, but sadly no flag displays. The relevant code and a sample "Country" from the JSON file below:
In the Properties section:
let countryImage: [Country] = Bundle.main.decode("Countries.json")
#State private var selectedContact: [ContactEntity] = []
let flag = "flag"
In the View:
VStack {
Picker(selection: $selectedCountry,
label: Text("Host Country:")) {
ForEach(countryImage) { country in
HStack {
Text(country.name).tag(country.name)
// TODO - Find Flag
if case country.flag = Data(base64Encoded: flag), let uiImage = UIImage(data: country.flag) {
Image(uiImage: uiImage).tag(country.flag)
} else {
let _ = print("Fail")
}
//Image(flag)
}
}
}
.padding(.horizontal, 0)
.padding()
.pickerStyle(MenuPickerStyle())
}//: INNER PICKER VSTACK
The Bundle references an extension:
extension Bundle {
func decode<T: Codable>(_ file: String) -> T { // "_" obviates the need to call the name of the parameter
// 1. Locate the JSON file
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
// 2. Create a property for the data
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
// 3. Create a decoder
let decoder = JSONDecoder()
// 4. Create a property for the decoded data
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
// 5. Return the ready-to-use data
return loaded
}
}
Sample JSON item:
{
"id": 202,
"name": "Sudan",
"isoAlpha2": "SD",
"isoAlpha3": "SDN",
"isoNumeric": 736,
"currency": {
"code": "SDD",
"name": "Dinar",
"symbol": false
},
"flag": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAUCAYAAACaq43EAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNjc2Q0MxMjE3ODgxMUUyQTcxNDlDNEFCRkNENzc2NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNjc2Q0MxMzE3ODgxMUUyQTcxNDlDNEFCRkNENzc2NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkY1RTdDNTBCMTc4NzExRTJBNzE0OUM0QUJGQ0Q3NzY2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkY1RTdDNTBDMTc4NzExRTJBNzE0OUM0QUJGQ0Q3NzY2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+dsEThQAAAfVJREFUeNq8lT9oE1Ecxz/v3ZmkMae2xdbuiqRiqaugKDo5WFGQOiiimy5aR6dOOrhYHRx0qQoVaQOCbgpO/oHatTrVhCYNtIlJbWJJ7p6/C7aiVhfv5Qf3jrt33Of9vt/f7z118sTe+TvPE15XQzHbGaABZbAeihu7zc6sw71MB0cWXGY9n28uOJbhDgM910q9QXx8oEFnVXMsu6mVcSVuF64lZ1jUrYcrp2sMD9XwBbinpPFlzihb4LVxRQhlzdP9qxy6sMKbHT6DZYdYk9YCovd4JF2Vu7f+JpS3O4BVuPUiydXpBPMUKLLc+tweOAzhkpIVxJsMv1bm/rZTfrK7y2QrBWUi8t39qwGh9HWHiX1l9bF/znl8/rpJk1LReryxFiK7I0MPMy8nVf+lXfrhh2fqx8x/X/rf6xJdfR/60tAMOHdziMNjZyguL1rM+I+OF1caUKpXpd2MJY9/BxY/QSzJ3ZFxLh84a7G41toq3MJ0haMHj5tHFx+Y3giLa2NwIMDNMpWSje1V0WzJav/2zKjJfVlQgQksgUPo9oT4KYBMHvO+rKaYcKPeuX7+MCwYR2qtrwPmvsJUDgp1a4eE+4u0W2PwVlplMmv9PHbXpZU+5clnmF6iHeGKtB65mkgrWebrtCtc3i3lyeQ8jKGd8V2AAQDj9qv7QrTRKwAAAABJRU5ErkJggg=="
},
It's easier than expected. Decodable can decode a base64-encoded String directly as Data
In Country declare flag
let flag: Data?
and a computed property
var flagImage : UIImage? {
guard let flagData = flag else { return nil }
return UIImage(data: flagData)
}
And display the image
if let flagImage = country.flagImage {
Image(uiImage: flagImage).tag(country.flag)
} else {
let _ = print("Fail")
}
Related
O.
I have a JSON file in my XCode project. Lets use this as an example:
{ "shifts": [
{"name": "Mary",
"role": "Waiter"},
{"name": "Larry",
"role": "Cook"}
]
}
In my JSONManager.swift, I have something along the lines of this:
struct Shift: Codable {
let role, name: String
static let allShifts: [Shift] = Bundle.main.decode(file: "shifts.json")
}
extension Bundle {
func decode<T: Decodable>(file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Could not find \(file) in the project")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Could not load \(file) in the project")
}
let decoder = JSONDecoder()
guard let loadedData = try? decoder.decode(T.self, from: data) else {
fatalError("Could not decode \(file) in the project")
}
return loadedData
}
}
I keep hitting the fatalError("Could not decode \(file) in the project") and I'm wondering if it's because the JSON is not formatted correctly or why it can't decode the JSON from the JSON file.
Take a look at you JSON structure for a momement.
You have a element called shifts, which is any array of other elements, but your code seems to trying to load a structure which only contains the child element.
Instead, you need a outer struct which contains the child struct, for example...
struct Shift: Codable {
let role, name: String
}
struct Shifts: Codable {
let shifts: [Shift]
}
Then you'd use the parent struct as the initial source type when decoding the content...
extension Bundle {
func decode<T: Decodable>(file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Could not find \(file) in the project")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Could not load \(file) in the project")
}
let decoder = JSONDecoder()
guard let loadedData = try? decoder.decode(T.self, from: data) else {
fatalError("Could not decode \(file) in the project")
}
return loadedData
}
}
let shifts: Shifts = Bundle.main.decode(file: "shifts.json")
I tested this in playgrounds and it worked fine.
There might be away to decode the structure without needing the "parent" struct, but this works for me.
I fixed my own mistake. I don't need the
{ "shifts": }
in my JSON
So I am trying to fetch data from the Pokemon API, and I am getting stuck at the point where I am trying to decode the JSON into a struct. Here is my code:
{
"count":1118,
"next":"https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20",
"previous":null,
"results":
[
{"name":"bulbasaur","url":"https://pokeapi.co/api/v2/pokemon/1/"},
{"name":"ivysaur","url":"https://pokeapi.co/api/v2/pokemon/2/"},
{"name":"venusaur","url":"https://pokeapi.co/api/v2/pokemon/3/"},
{"name":"charmander","url":"https://pokeapi.co/api/v2/pokemon/4/"},
{"name":"charmeleon","url":"https://pokeapi.co/api/v2/pokemon/5/"},
{"name":"charizard","url":"https://pokeapi.co/api/v2/pokemon/6/"},
{"name":"squirtle","url":"https://pokeapi.co/api/v2/pokemon/7/"},
{"name":"wartortle","url":"https://pokeapi.co/api/v2/pokemon/8/"},
{"name":"blastoise","url":"https://pokeapi.co/api/v2/pokemon/9/"},
{"name":"caterpie","url":"https://pokeapi.co/api/v2/pokemon/10/"},
{"name":"metapod","url":"https://pokeapi.co/api/v2/pokemon/11/"},
{"name":"butterfree","url":"https://pokeapi.co/api/v2/pokemon/12/"},
{"name":"weedle","url":"https://pokeapi.co/api/v2/pokemon/13/"},
{"name":"kakuna","url":"https://pokeapi.co/api/v2/pokemon/14/"},
{"name":"beedrill","url":"https://pokeapi.co/api/v2/pokemon/15/"},
{"name":"pidgey","url":"https://pokeapi.co/api/v2/pokemon/16/"},
{"name":"pidgeotto","url":"https://pokeapi.co/api/v2/pokemon/17/"},
{"name":"pidgeot","url":"https://pokeapi.co/api/v2/pokemon/18/"},
{"name":"rattata","url":"https://pokeapi.co/api/v2/pokemon/19/"},
{"name":"raticate","url":"https://pokeapi.co/api/v2/pokemon/20/"}
]
}
func fetchPokemon() {
let defaultSession = URLSession(configuration: .default)
if let url = URL(string: "https://pokeapi.co/api/v2/pokemon/") {
let request = URLRequest(url:url)
let dataTask = defaultSession.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
guard error == nil else {
print ("error: ", error!)
return
}
guard data != nil else {
print("No data object")
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("response is: ", response!)
return
}
guard let mime = response?.mimeType, mime == "application/json" else {
print("Wrong MIME type!")
return
}
DispatchQueue.main.async {
guard let result = try? JSONDecoder().decode(PokemonList.self, from: data!) else {
print("Error Parsing JSON")
return
}
let pokemon = result.pokemon
self.Pokemon = pokemon
print(self.Pokemon)
}
})
dataTask.resume()
}
}
and here is the pokemon struct:
struct Pokemon {
// Various properties of a post that we either need or want to display
let name: String
let url: String
}
extension Pokemon: Decodable {
// properties within a Post returned from the Product Hunt API that we want to extract the info from.
enum PokemonKeys: String, CodingKey {
// first three match our variable names for our Post struct
case name = "name"
case url = "url"
}
init(from decoder: Decoder) throws {
let postsContainer = try decoder.container(keyedBy: PokemonKeys.self)
name = try postsContainer.decode(String.self, forKey: .name)
url = try postsContainer.decode(String.self, forKey: .url)
}
}
struct PokemonList: Decodable {
var pokemon: [Pokemon]
}
It keeps reaching the point when decoding which says "Error Parsing JSON". I'm assuming that there may be an error in how I setup the pokemon struct?
Any ideas?
you are getting a parse error because the data model is not the same. your struct should be:
struct PokemonList: Decodable {
var results: [Pokemon]
var count: Int
var next: String
}
you don't need the extension.
i want to load my data with a json file. So far so good. But now I am struggling. What if the user made some changes to the Oil Object and want to save them? My idea was, that i save the changed oils object to CoreData. But what is this possible? Because every time the user launches the app, the untouched json file gets loaded and the user will not see his changed objects. How can i handle that? Or is my thinking wrong?
struct Oil: Codable, Hashable, Identifiable {
var id: Int
let image: String
let color: String
let title: String
let subtitle: String
let description: String
var localizedTitle: LocalizedStringKey {
return LocalizedStringKey(title)
}
var localizedDescription: LocalizedStringKey {
return LocalizedStringKey(description)
}
var isFavorite: Bool
static let exampleOil = Oil(id: 10001, image: "",color: "lavenderColor" ,title: "lavender", subtitle: "Lavandula angustifolia", description: "", isFavorite: false)
}
final class Oils: ObservableObject {
var oils: [Oil] = load("oilDatabase.json")
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
Do you really need Core Data? If you are just storing a single json file you could save yourself a lot of complexity and just write the updated version of the file to the documents directory.
All you would have to do is to check that the file exists in the documents directory, if it does load it from there otherwise load it from your bundle.
This gets the URL for the documents directory
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
Then we just need to update your load to check that the file exists in the documents directory before loading it.
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
// Check that the fileExists in the documents directory
let filemanager = FileManager.default
let localPath = getDocumentsDirectory().appendingPathComponent(filename)
if filemanager.fileExists(atPath: localPath.path) {
do {
data = try Data(contentsOf: localPath)
} catch {
fatalError("Couldn't load \(filename) from documents directory:\n\(error)")
}
} else {
// If the file doesn't exist in the documents directory load it from the bundle
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
You will also need to save any changes that you make to the json. You can do that with the following function.
func save<T: Encodable>(_ filename: String, item: T) {
let encoder = JSONEncoder()
do {
let url = getDocumentsDirectory().appendingPathComponent(filename)
let encoded = try encoder.encode(item)
let jsonString = String(data: encoded, encoding: .utf8)
try jsonString?.write(to: url, atomically: true, encoding: .utf8)
} catch {
// handle your error
}
}
I have a json file that looks like this (in a file called list.json)
[
{
"id": "C8B046E9-70F5-40D4-B19A-40B3E0E0877B",
"name": "Dune",
"author": "Frank Herbert",
"page": "77",
"total": "420",
"image": "image1.jpg"
},
{
"id": "2E27CA7C-ED1A-48C2-9B01-A122038EB67A",
"name": "Ready Player One",
"author": "Ernest Cline",
"page": "234",
"total": "420",
"image": "image1.jpg"
}
]
This a default file that comes with my app (These are examples that can be deleted). My content view has a member variable that uses a decode function I wrote to get the json array and display it in a list. I have a view to add another book to the json file. The view appends another struct to the array and then encodes the new appended array to list.json with this function
func writeJSON(_ bookData: [Book]) {
do {
let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("list.json")
let encoder = JSONEncoder()
try encoder.encode(bookData).write(to: fileURL)
} catch {
print(error.localizedDescription)
}
}
This function is called in the NewBook view when a button is pressed. bookData is the decoded array in my content view which I used a Binding to in my NewBook view.
The code works if you add the book and go back to the contentview (the list now contains the appended struct) but if you close the app and open it again, the list uses the default json file. I think there is a mistake in my writeJSON function.
Also note that I tried changing the create parameter to false in the URL but that didn't help.
edit: I am adding the Book struct as requested
struct Book: Hashable, Codable, Identifiable {
var id: UUID
var name: String
var author: String
var page: String
var total: String
var image: String
}
edit 2: This is for an iOS app
edit 3: my load data function
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
You are probably not overriding the existing file on disk. Try options: .atomic while writing the data to disk.
func writeJSON(_ bookData: [Book]) {
do {
let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("list.json")
try JSONEncoder().encode(bookData).write(to: fileURL, options: .atomic)
} catch {
print(error)
}
}
edit/update:
The issue here is that you are not saving the file where you think it would. The Bundle directory is read-only and has no relation with your App documents directory.
func load<T: Decodable>(_ filename: String) -> T? {
// no problem to force unwrap here you actually do want it to crash if the file it is not inside your bundle
let readURL = Bundle.main.url(forResource: filename, withExtension: "json")!
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let jsonURL = documentDirectory
.appendingPathComponent(filename)
.appendingPathExtension("json")
// check if the file has been already copied from the Bundle to the documents directory
if !FileManager.default.fileExists(atPath: jsonURL.path) {
// if not copy it to the documents (not it is not a read-only anymore)
try? FileManager.default.copyItem(at: readURL, to: jsonURL)
}
// read your json from the documents directory to make sure you get the latest version
return try? JSONDecoder().decode(T.self, from: Data(contentsOf: jsonURL))
}
func writeJSON(_ bookData: [Book]) {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let jsonURL = documentDirectory
.appendingPathComponent("list")
.appendingPathExtension("json")
// write your json at the documents directory and use atomic option to override any existing file
try? JSONEncoder().encode(bookData).write(to: jsonURL, options: .atomic)
}
Im trying to save checkmarks in my application. But cause im paring my data from an api.. I don't know how I can add like the key "checked". The thing is the JSON gets downloaded once a Week, adding new content. Is there a way to still save my checkmarks?
struct Base : Codable {
let expireDate : String
let Week : [Weeks]
}
struct Weeks : Codable {
let name : String
let items : [Items]
}
struct Items : Codable {
let Icon: String
let text : String
}
In my RootTableView I have the array Weeks, and I would like to add checkmarks to the child tableView Items.
Thanks in advance
UPDATE:
//
// Download JSON
//
enum Result<Value> {
case success(Value)
case failure(Error)
}
func getItems(for userId: Int, completion: ((Result<Base>) -> Void)?) {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "api.jsonbin.io"
print(NSLocale.preferredLanguages[0])
let preferredLanguage = NSLocale.preferredLanguages[0]
if preferredLanguage.starts(with: "de"){
urlComponents.path = "/b/xyz"
}
else
{
urlComponents.path = "/xyz"
}
let userIdItem = URLQueryItem(name: "userId", value: "\(userId)")
urlComponents.queryItems = [userIdItem]
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"secret-key": "xyzzy"
]
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonDataItems = responseData {
let decoder = JSONDecoder()
do {
let items = try decoder.decode(Base.self, from: jsonDataItems)
completion?(.success(items))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
func loadJson() {
getItems(for: 1) { (result) in
switch result {
case .success(let item):
self.saveItemsToDisk(items: item)
self.defaults.set(item.expireDate, forKey: "LastUpdateItems")
case .failure(let error):
fatalError("error: \(error.localizedDescription)")
}
self.getItemesFromDisk()
}
}
//
// Save Json Local
//
func getDocumentsURL() -> URL {
if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
return url
} else {
fatalError("Could not retrieve documents directory")
}
}
func saveItemsToDisk(items: Base) {
// 1. Create a URL for documents-directory/items.json
let url = getDocumentsURL().appendingPathComponent("items.json")
// 2. Endcode our [Item] data to JSON Data
let encoder = JSONEncoder()
do {
let data = try encoder.encode(items)
// 3. Write this data to the url specified in step 1
try data.write(to: url, options: [])
} catch {
fatalError(error.localizedDescription)
}
}
func getItmesFromDisk(){
// 1. Create a url for documents-directory/items.json
let url = getDocumentsURL().appendingPathComponent("items.json")
let decoder = JSONDecoder()
do {
// 2. Retrieve the data on the file in this path (if there is any)
let data = try Data(contentsOf: url)
// 3. Decode an array of items from this Data
let items = try decoder.decode(Base.self, from: data)
itemsDisk = items
} catch {
}
}
I would create a wrapper class (or struct) for Items, say MyItem, that contains the original Items object and the checkmark property.
class MyItem {
let item: Items
var checkmark: Bool
//more properties...?
init(withItem item: Items {
this.item = item
this.checkmark = false
}
func isEqual(otherItem item: Items) -> Bool {
return this.item == item
}
}
The isEqual is used to check if there already exists an MyItem object for a downloaded Items object or if a new should be created. isEqual assumes that you change the Items struct to implement the Equatable protocol.
You probably also need to replace Weeks but here you don't need to include the original Weeks object.
class MyWeek {
let name: String
let items: [MyItem]
}