Using a struct in other swift class (view controller) - swift

I've created a struct in the AppDelegate since I need it to populate before anything else happens. Now I want to use the data in the struct in some of my ViewControllers but I cannot figure out how to get to it within the ViewController.
Struct (KnownCurrencies) file:
struct Root : Decodable {
let currencies : [Currency]
}
struct Currency : Decodable {
let code, name, symbol : String
}
AppDelegate (did finish launching with options) from plist
let url = Bundle.main.url(forResource: "KnownCurrencies", withExtension: "plist")!
do {
let data = try Data(contentsOf: url)
let result = try PropertyListDecoder().decode(Root.self, from: data)
currencies = result.currencies
} catch {print(error)}
ViewController in viewDidLoad
var currencies: [Currency] = []
print (currencies[0].code)
currencies has nothing in it. How do I get to the data in the strut?
The array is empty, producing the error message:
Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range
2023-01-25 10:17:14.763018-0800 US Convert[93709:2245778]
Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range

Related

Having trouble adding values to core data outside of a View

I'm trying to load data into a CoreData entity "Articles" from a function I would like to call in an init() {} call when my app starts which means I'm not doing this from within a view.
I get the message "Accessing Environment's value outside of being installed on a View. This will always read the default value and will not update."
and would like to work around that. I'm using Xcode 14.2
I do have a standard PersistenceController setup and so on
Here is where I run into the issue "let section = SectionsDB(context: managedObjectContext)"
#main
struct ArticlesExampleApp: App {
let persistanceController = PersistanceController.shared
init() {
let x = Articles.loadSections()
}
var body: some Scene {
WindowGroup {
MasterView()
.environment(\.managedObjectContext, persistanceController.container.viewContext)
}
}
class Articles {
class func loadSections() -> Int {
#Environment(\.managedObjectContext) var managedObjectContext
// Initialize some variables
let myPath = Bundle.main.path(forResource: "Articles", ofType: "json")
// Initialize some counters
var sectionsCount = 0
do {
let myData = try Data(contentsOf: URL(fileURLWithPath: myPath!), options: .alwaysMapped)
// Decode the json
let decoded = try JSONDecoder().decode(ArticlesJSON.self, from: myData)
// **here is where I run into the error on this statement**
let section = SectionsDB(context: managedObjectContext)
while sectionsCount < decoded.sections.count {
print("\(decoded.sections[sectionsCount].section_name) : \(decoded.sections[sectionsCount].section_desc)")
section.name = decoded.sections[sectionsCount].section_name
section.desc = decoded.sections[sectionsCount].section_desc
sectionsCount+=1
}
PersistanceController.shared.save()
} catch {
print("Error: \(error)")
}
return sectionsCount
}
}
Since you are already using a singleton, you can just use that singleton in your loadSections function:
let section = SectionsDB(context: PersistanceController.shared.container.viewContext)
And, remove the #Environment(\.managedObjectContext) var managedObjectContext line

Passing data to another view using a model - SwiftUI

I'm trying to pass the data retrieved from the API to a View, but I'm getting the following error:
Class 'ApiManagerViewModel' has no initializers
This is how the ViewModel looks:
class ApiManagerViewModel: ObservableObject {
#Published var blockchainData: ApiDataClass
func callAPI() {
guard let url = URL(string: "myapiurl") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url, timeoutInterval: Double.infinity)
let callAPI = URLSession.shared.dataTask(with: request) { data, responce, error in
do {
if let data = data {
let decodedResponse = try JSONDecoder().decode(APIResponce.self, from: data)
DispatchQueue.main.async {
// update our UI
self.blockchainData = (decodedResponse.data)
}
// Everything is good, so we can exit
return
}
} catch {
print("Unexpected error while fetchign API: \(error).")
return
}
}
callAPI.resume()
}
This is the model:
// MARK: - APIResponce
struct APIResponce: Codable {
let data: ApiDataClass
let error: Bool
}
// MARK: - DataClass
struct ApiDataClass: Codable {
let address, quote_currency: String
let chain_id: Int
let items: [ApiItems]
}
// MARK: - Item
struct ApiItems: Codable {
let contract_decimals: Int32
let contract_name, contract_ticker_symbol, contract_address, logo_url, type, balance: String
let supports_erc: [String]?
let quote_rate: Double?
let quote: Double
}
I've tried initializing it but it's no bueno:
init() {
let address = 0, quote_currency = 0
let chain_id = 0
let items: [ApiItems]
}
If I initialize it like that I get the error, and I also don't want to repeat the same thing the model has:
Return from initializer without initializing all stored properties
I also tried with the variable like:
#Published var blockchainData = []
and I get the error on this line: self.blockchainData = (decodedResponse.data):
Cannot assign value of type 'ApiDataClass' to type '[Any]'
How can I make the variable blockchainData have the value coming from decodedResponse.data so I can pass it to another view?
Thanks
You're getting that error because you've declared var blockchainData: ApiDataClass, but haven't given it an initial value (your attempt at providing an initializer for ApiDataClass didn't help because the problem is ApiManagerViewModel).
The easiest solution to this is to turn it into an optional:
#Published var blockchainData: ApiDataClass?
Then, in your View, you'll probably want to check if it's available. Something like:
if let blockchainData = viewModel.blockchainData {
//code that depends on blockchainData
}
(assuming your instance of ApiManagerViewModel is called viewModel)

Load json from #propertyWrapper in SwiftUI ContentView

I've been using a json decoding utility that works great. I'm wondering if that utility can be abstracted into a propertyWrapper that accepts the json file name as a string.
The call site in the ContentView looks like this:
struct ContentView: View {
#DataLoader("tracks.json") var tracks: [Tracks]
...
My rough sketch of the property wrapper looks like this:
#propertyWrapper
struct DataLoader<T: Decodable>: DynamicProperty {
private let fileName: String
var wrappedValue: T {
get {
return Bundle.main.decode(T.self, from: fileName)
}
set {
//not sure i need to set anything since i just want to get the array
}
}
init(_ fileName: String) {
self.fileName = fileName
wrappedValue = Bundle.main.decode(T.self, from: fileName)
}
}
Currently the body of the the ContentView shows this error:
Failed to produce diagnostic for expression; please file a bug report
I like the idea of removing some boilerplate code, but I think I'm missing something fundamental here.
In SwiftUI views are refreshed very often. When a view is refreshed then the #propertyWrapper will be initialised again - this might or might not be desirable. But it's worth noting.
Here is a simple demo showing how to create a property wrapper for loading JSON files. For simplicity I used try? and fatalError but in the real code you'll probably want to add a proper error handling.
#propertyWrapper
struct DataLoader<T> where T: Decodable {
private let fileName: String
var wrappedValue: T {
guard let result = loadJson(fileName: fileName) else {
fatalError("Cannot load json data \(fileName)")
}
return result
}
init(_ fileName: String) {
self.fileName = fileName
}
func loadJson(fileName: String) -> T? {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(T.self, from: data)
else {
return nil
}
return result
}
}
Then, assuming you have a sample JSON file called items.json:
[
{
"name": "Test1",
"count": 32
},
{
"name": "Test2",
"count": 15
}
]
with a corresponding struct:
struct Item: Codable {
let name: String
let count: Int
}
you can load your JSON file in your view:
struct ContentView: View {
#DataLoader("items") private var items: [Item]
var body: some View {
Text(items[0].name)
}
}

SwiftUI adding Codable conformance for #Published properties using 'id' property

I'm just starting out on learning SwiftUI. Where do I go wrong?
I'm trying to add Codable conformance in my class (ManyItems). This so I eventually can save an array to disk using JSON.
Two errors:
1) In both the "required init(...) "id = try..." and the encode func: "try container.encode..." result in "'id' is unavailable in Swift: 'id' is not available in Swift; use 'Any'"
2) In both the required init(...) and func encode: "Use of unresolved identifier 'one'." I assumed the identifier in the struct would be carried forward into the class?
struct Item: Identifiable {
var id = UUID()
var one: String
}
class ManyItems: ObservableObject, Codable {
#Published var manyitems = [Item]()
enum CodingKeys: CodingKey {
case id
case one
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
one = try container.decode(String.self, forKey: .one)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(one, forKey: .one)
}
}
I've found a solution but had to take a slightly different route. Hopefully this could help someone else who needs to use a Collection conforming to Codable and ObservableObject for saving to the Apps documentDirectory.
I conformed the struct 'Item' to Codable (i.e not the class 'ManyItem'). I added code for JSON encoding and saving of the Collection to the Apps documentDirectory. This happens automatic when a property value changes. The class is initiated through either a read/decoding of the JSON file from the Apps document directory or as a new blank instance if the JSON file is not available yet.
struct Item: Identifiable, Codable {
var id = UUID()
var one: String
}
class ManyItems: ObservableObject {
#Published var manyitems: [Item] {
didSet {
// Saves the array 'items' to disc
do {
// find the documentDirectory and create an URL for the JSON
file
let filename = getDocumentsDirectory().appendingPathComponent("manyitems.json")
let data = try JSONEncoder().encode(self.manyitems)
try data.write(to: filename, options: [.atomicWrite])
} catch {
print("Unable to save data.")
}
}
}
init() {
// replaces a call to 'getDocumentsDirectory()' methode as it created an initialisation error.
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// Add the first [0] path to the filename and create 'filename'
let filename = paths[0].appendingPathComponent("manyitems.json")
//print(filename)
do {
// Try to read the file (if it exist) and load the data into the array 'manyitem'
let data = try Data(contentsOf: filename)
manyitems = try JSONDecoder().decode([Item].self, from: data)
// Yes all good then exit
return
} catch {
// Something whent wrong. Initialize by creating an empty array 'manyitems'
self.manyitems = []
print("Unable to load saved data.")
}
}
// retreives the App's first DocumentDirectory
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
Your Codable models should be,
struct Item: Codable, Identifiable {
var id = UUID()
var one: String
}
class ManyItems: Codable, ObservableObject {
#Published var manyitems = [Item]()
}
As evident from your code, you're not handling any specific parsing cases. So, there is no need to explicitly implement init(from:) and encode(to:) methods.

Saving a Codable Struct to UserDefaults with Swift

I am trying to encode a struct
struct Configuration : Encodable, Decodable {
private enum CodingKeys : String, CodingKey {
case title = "title"
case contents = "contents"
}
var title : String?
var contents: [[Int]]?
}
into JSON to store in a local key of UserDefaults.standard. I have the following code:
let jsonString = Configuration(title: nameField.text, contents: newContents)
let info = ["row" as String: jsonString as Configuration]
print("jsonString = \(jsonString)")
//trying to save object
let defaults = UserDefaults.standard
let recode = try! JSONEncoder().encode(jsonString)
defaults.set(recode, forKey: "simulationConfiguration")
//end of saving local
The print returns:
jsonString = Configuration(title: Optional("config"), contents: Optional([[4, 5], [5, 5], [6, 5]]))
so I believe I am creating the object correctly. However, when I try and retrieve the key the next time I run the simulator I get nothing.
I put the following in AppDelegate and it always returns No Config.
let defaults = UserDefaults.standard
let config = defaults.string(forKey: "simulationConfiguration") ?? "No Config"
print("from app delegate = \(config.description)")
Any ideas? Thanks
Here you are saving a Data value (which is correct)
defaults.set(recode, forKey: "simulationConfiguration")
But here you are reading a String
defaults.string(forKey: "simulationConfiguration")
You cannot save Data, read String and expect it to work.
Let's fix your code
First of all you don't need to manually specify the Coding Keys. So your struct become simply this
struct Configuration : Codable {
var title : String?
var contents: [[Int]]?
}
Saving
Now here's the code for saving it
let configuration = Configuration(title: "test title", contents: [[1, 2, 3]])
if let data = try? JSONEncoder().encode(configuration) {
UserDefaults.standard.set(data, forKey: "simulationConfiguration")
}
Loading
And here's the code for reading it
if
let data = UserDefaults.standard.value(forKey: "simulationConfiguration") as? Data,
let configuration = try? JSONDecoder().decode(Configuration.self, from: data) {
print(configuration)
}
encode(_:) function of JSONEncoder returns Data, not String. This means when you need to get the Configuration back from UserDefaults you need to get data and decode them.
Here is example:
let defaults = UserDefaults.standard
guard let configData = defaults.data(forKey: "simulationConfiguration") else {
return nil // here put something or change the control flow to if statement
}
return try? JSONDecoder().decode(Configuration.self, from: configData)
you also don't need to assign value to all the cases in CodingKeys, the values is automatically the name of the case
if you are conforming to both, Encodable and Decodable, you can simply use Codable instead as it is combination of both and defined as typealias Codable = Encodable & Decodable
If you want an external dependency that saves a boat load of frustration, checkout SwifterSwift
Here's how I did it in two lines using their UserDefaults extension.
For setting:
UserDefaults.standard.set(object: configuration, forKey: "configuration")
For retrieving the object:
guard let configuration = UserDefaults.standard.object(Configuration.self, with: "configuration") else { return }
print(configuration)
That's about it..!!
Basically your UserDefault stored property will be look something like this,
private let userDefaults = UserDefaults.standard
var configuration: Configuration? {
get {
do {
let data = userDefaults.data(forKey: "configuration_key")
if let data {
let config = try JSONDecoder().decode(User.self, from: data)
return config
}
} catch let error {
print("Preference \(#function) json decode error: \(error.localizedDescription)")
}
return nil
} set {
do {
let data = try JSONEncoder().encode(newValue)
userDefaults.set(data, forKey: "configuration_key")
} catch let error {
print("Preference \(#function) json encode error: \(error.localizedDescription)")
}
}
}