Access Data Object by ID - swift

I have the following structs that defined according to the SwiftUI example in Apple.
struct Cat: Hashable, Codable, Identifiable {
let id: Int
let name: String
let audio: [Int]? // list of audio of cat sound
}
struct CatAudio: Hashable, Codable, Identifiable {
let id: Int
let filename: String
}
I then would like to access the audio and then deliver in the view.
I have json data like this:
[
{
"id": 55,
"name": "meow",
"audio": [6,5]
},
{
"id": 3,
"name": "meowmeow",
"audio": [2]
}
]
AudioData.json
[
{
"id": 5,
"filename": "5.wav"
},
{
"id": 2,
"filename": "2.wav"
},
{
"id": 6,
"filename": "6.wav"
}
]
The json files loaded successfully.
#Published var cats: [Cat] = load("CatData.json")
#Published var catAudios: [CatAudio] = load("CatAudio.json")
I then tried to get an audio object from my environment model data:
#EnvironmentObject var modelData: ModelData
and then I want to get an the corresponding audio object of the cat. but I failed to do so as I do not know how to use the "Id" to get it.
Example:
Assume that I got the cat object from my model:
let cat = modelData.cats[0]
I then want to get its audio data according to the id stored in the audio list of it
let catAudio = modelData.catAudios[cat.audio[0]!] // exception here
I found that it is because the array order may not be consistent with the "Id". I want to make use of the "Id" instead of the Array order to get the item.
How can I do it?
========
I have tried to write a function to get the list of CatAudio of a cat.
I have also make audio non-optional.
func getAudio(cat: Cat) -> [CatAudio] {
return cat.audio.compactMap{id in catAudio.filter { $0.id==id } }
}
But it complains and said that cannot convert the value of type [CatAudio] to closure result type 'CatAudio'
I got confused with that.

You need to use high-order functions to match the id values in the audio array with the elements in the CatAudio array
Assuming the 2 arrays and a selected Cat object
var cats: [Cat] = ...
var catAudios: [CatAudio] = ...
let cat = cats[0]
To select one audio for a one id
if let firstId = cat.audio?.first {
let audio = catAudios.first { $0.id == firstId}
}
To get an array of all CatAudio for the cat object
if let array = cat.audio {
let values = array.compactMap { id in catAudios.filter { $0.id == id }}
}
The code would be simpler if the audio array wasn't optional, any reason for it to be declared optional?

Related

Initialising 'top' struct in Swift

I'm currently attempting to initialise a struct with Swift, that goes several structs deep. Once an external API is called, it sends back data which I want to place into a struct so that I can use it to populate a TableView.
I'm wanting to decode data that looks like this:
{
"data": {
"1": {
"name": "Bitcoin",
"quote": {
"GBP": {
"price": 25794.72142905233,
"percent_change_1h": -1.4133929,
"percent_change_24h": -0.74636982,
"percent_change_7d": -5.8533249
}
}
},
"52": {
"name": "XRP",
"quote": {
"GBP": {
"price": 0.7157097479533718,
"percent_change_1h": -1.35513268,
"percent_change_24h": 1.84172355,
"percent_change_7d": 5.05130272
}
}
}
}
To do this, I have the following Struct structure:
Coin struct which references data
Datum struct which references the '1' and '52'
Quote struct which references the 'quote'
GBP struct which references the 'price', 'percent_change_1h'
These look like this:
struct Coin: Codable {
let data: [String: Datum]
}
struct Datum: Codable {
let name: String
let quote: Quote
}
struct Quote: Codable {
let gbp: Gbp
enum CodingKeys: String, CodingKey {
case gbp = "GBP"
}
}
struct Gbp: Codable {
let price, percentChange1H, percentChange24H, percentChange7D: Double
enum CodingKeys: String, CodingKey {
case price
case percentChange1H = "percent_change_1h"
case percentChange24H = "percent_change_24h"
case percentChange7D = "percent_change_7d"
}
}
I'm then attempting to set up a variable which is an empty 'Coin' struct under the variable name 'coins' like so:
var coins = Coin()
Unfortunately - trying to do this prompts the following error:
Missing argument for parameter 'data' in call
If I then follow the prompts, I can insert the following:
var coins = Coin(data: [String : Datum])
But this then produces the following error:
Cannot convert value of type '[String: Datum].Type' to expected
argument type '[String: Datum]'
Please can somebody point out what is going wrong here? Have I built my structs incorrectly? Is there an alternative I can do?
Rather than [String : Datum], which the compiler is telling you is a Type, use [:], which is the Swift syntax for an empty Dictionary:
var coins = Coin(data: [:])

Update a struct array inside an array of struct

i am becoming crazy with a problem.
I would like to populate an array with structured datas where one of this value is an array of structured datas. The values would be taken by a json result structured as follow
{
"values": [
{
"a": "Avalue",
"b": "Bvalue",
"c": [
{
"d": "DValue",
"e": "EValue",
"f": "FValue",
},
{
"d": "Dvalue",
"e": "EValue",
"f": "FValue",
}
],
},
...
]
}
so i have this code (schematized)
class CreateArray: UIViewController {
struct FirstStruct {
var a: String
var b: String
var c: [SecondStruct]
}
struct SecondStruct {
var d: String
var e: String
var f: String
}
var firstArray : [FirstStruct] = []
var secondArray : [SecondStruct] = []
var firstsArray = [[Any]]()
override func viewDidLoad() {
populateArrays()
}
func populateArrays() {
for value in values { //don' t focus on schematized for loops
for cs in value.c {
self.secondArray.append(SecondStruct(d: "Dvalue", e: "Evalue", f: "Fvalue"))
}
self.firstArray = [FirstStruct(a: "Avalue", b: "Bvalue", c: self.secondArray)]
self.firstsArray.append(self.firstArray)
}
}
}
Now i have firstsArray made of multiple firstArray with secondArray as c' s value.
What i am looking for is a way to append another secondArray to a c of a specific firstArray, i am able to iterate to the right firstArray in firstsArray with something like that
for elem in firstsArray {
if elem is the right one {
create a secondStruct
elem.c.append(created second struct)
}
}
The problem is that i didn' t found a working way to append the new structured datas to the existing c. The most common problem i had is "Cannot use mutating member on immutable value of type [CreateArray.SecondStruct]"
i have also tried to add mutating func in the struct but nothing to do, but maybe i used it in the wrong way.
I am sure i was not totally clear, so please feel free to ask more infos.
Thanks a lot
EDIT:
I was able to find out a solution thenks to #New Dev suggest.
i have added a mutating func in the first Struct so now it is so:
struct FirstStruct {
var a: String
var b: String
var c: [SecondStruct]
mutating func updateC(latestC: SecondStruct) {
c.append(latestC)
}
}
Now i used this next in a function with for loops to find out the wanted firstArray and modify it (i think it is a replacement to be honest), using something like this
for index in self.firstsArray.indices {
for secondindex in self.firstsArray[index].indices {
if (self.firstsArray[index][secondindex] as! FirstStruct).a == WANTED A VALUE {
if var updatingC = self.firstsArray[index][secondindex] as? FirstStruct {
updatingC.updateC(latestC: (SecondStruct(d: "DValue", e: "EValue", f: "FValue")))
self.firstsArray[index][secondindex] = updatingC
}
}
}
}

Type of expression is ambiguous without more context in Xcode 11

I'm trying to refer to an [Item] list within an #EnvironmentObject however when accessing it within a SwiftUI List, I get the error. What I don't understand is, this error doesn't pop up when following Apple's Landmark tutorial.
As far as I can tell, the [Item] list is loading correctly as I can print it out and do other functions with it. It just bugs out when using it for a SwiftUI List Is there something I've missed?
ItemHome.swift:
struct ItemHome : View {
#EnvironmentObject var dataBank: DataBank
var body: some View {
List {
ForEach(dataBank.itemList) { item in
Text("\(item.name)") // Type of expression is ambiguous without more context
}
}
}
}
Supporting code below:
Item Struct:
struct Item {
var id: Int
var uid: String
var company: String
var item_class: String
var name: String
var stock: Int
var average_cost: Decimal
var otc_price: Decimal
var dealer_price: Decimal
var ctc_price: Decimal
}
DataBank.swift:
final class DataBank : BindableObject {
let didChange = PassthroughSubject<DataBank, Never>()
var itemList: [Item] = load("itemsResults.json") {
didSet {
didChange.send(self)
}
}
}
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> 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)")
}
}
itemsResults.json:
[
{
"id": 1,
"uid": "a019bf6c-44a2-11e9-9121-4ccc6afe39a1",
"company": "Bioseed",
"item_class": "Seeds",
"name": "9909",
"stock": 0,
"average_cost": 0.0,
"otc_price": 0.0,
"dealer_price": 0.0,
"ctc_price": 0.0
},
{
"id": 2,
"uid": "a019bf71-44a2-11e9-9121-4ccc6afe39a1",
"company": "Pioneer",
"item_class": "Seeds",
"name": "4124YR",
"stock": 0,
"average_cost": 0.0,
"otc_price": 0.0,
"dealer_price": 0.0,
"ctc_price": 0.0
}
]
Apparently I missed making sure my models (Item in this case) conformed to the Identifiable protocol fixed it. Still, I wish Apple was more clear with their error messages.
As you mentioned in your answer, a ForEach needs a list of Identifiable objects. If you don't want to make your object implement that protocol (or can't for some reason), however, here's a trick:
item.identifiedBy(\.self)
I had the same problem and it wasn't something related to the line itself, it was related to the curly braces/brackets, so that if someone faced the same problem and doesn't know where the problem is, try to trace the curly braces and the brackets
To conform to Identifiable, just give the id or uid variable a unique value.
An easy way to do this is this:
var uid = UUID()
So your full struct would be:
struct Item: Identifiable {
var id: Int
var uid = UUID()
var company: String
var item_class: String
var name: String
var stock: Int
var average_cost: Decimal
var otc_price: Decimal
var dealer_price: Decimal
var ctc_price: Decimal
}
Xcode may show this error in many cases. Usually when using higher order functions (like map) and reason may be "anything".
There are two types:
Error is in higher order function. In this case your only option is to carefully read the block of code. Once again there may be different kinds of problems.
In some cases higher order function does not have any problem by itself, but there may be a compile time error in function that's called from the body of the higher order function.
Unfortunately Xcode does not point to this error sometimes.
To detect such errors and save lot of time, easy workaround is to temporarily comment higher order function and try to build. Now Xcode will show this error function and will show more reasonable error.

Realm query problem with sorted localized data

Consider the following Realm models:
class Fruit: Object {
#objc dynamic var name = ""
let localizations = List<Localization>()
/**
Returns the localized name of the fruit matching the preferred language of the app
or self.name if the fruit does not have a localization matching the user preferred language codes.
*/
var localizedName: String? {
guard !Locale.isPreferredLanguageDefaultAppLanguage else { return self.name }
let preferredLanguagesCodes = Locale.preferredLanguagesCodes
let localizations = preferredLanguagesCodes.compactMap({ languageCode in Array(self.localizations).filter({ $0.languageCode == languageCode }).first })
return localizations.first?.localizedName ?? self.name
}
}
class Localization: Object {
#objc dynamic var localizedName: String = ""
#objc dynamic var languageCode: String = ""
}
Let's say I have 2 fruits in my database (represented in JSON format for the sake of simplicity):
[
{
"name": "Apple",
"localizations": [
{
"localizedName": "Pomme",
"languageCode": "fr"
}
]
},
{
"name": "Banana",
"localizations": [
{
"localizedName": "Banane",
"languageCode": "fr"
}
]
}
]
Now I want to get all the fruits in my database, and sort them alphabetically by their localizedName.
var localizedNameSortingBlock: ((Fruit, Fruit) -> Bool) = {
guard let localizedNameA = $0.localizedName, let localizedNameB = $1.localizedName else { return false }
return localizedNameA.diacriticInsensitive < localizedNameB.diacriticInsensitive
}
let sortedFruits = Array(Realm().objects(Fruit.self)).sorted(by: localizedNameSortingBlock)
If the first preferred language of my device is "English", I get this:
Apple
Banana
If it's set to "French":
Banane
Pomme
It's quite simple, but this solution has a major inconvenient:
By casting the Results<Fruit> collection into an array, I'm loosing the ability to get live updates via Realm's notification token system.
The problem is, I can't sort using NSPredicate directly on the Results collection, because the localizedName property is a computed property, thus is ignored by Realm.
I thought about writing the localizedName value directly into the name property of the Fruit object, but doing so requires to loop through all fruits and change their name whenever the user change of preferred language. There must be a better way.
So my question is:
Is there a way to retrieve all the fruits in my database, get them sorted by their localizedName, without loosing the ability to receive batch updates from Realm?

How to create nested dictionary elements in Swift?

I want to create a variable which stores this:
["messageCode": API_200, "data": {
activities = (
{
action = 1;
state = 1;
}
);
messages = (
{
body = hi;
// ...
}
);
}, "message": ]
What I have done is this:
var fullDict: Dictionary<String, AnyObject> = [:]
fullDict["messageCode"] = "API_200" as AnyObject
var data: Dictionary<String, AnyObject> = [:]
fullDict ["data"] = data as AnyObject
Is this way is correct and how I can add activities?
I would suggest to go with creating a custom Model:
struct Model {
var messageCode: String
var data: MyData
var message: String
}
struct MyData {
let activities: [Activity]
let messages: [Message]
}
struct Activity {
var action: Int
var state: Int
}
struct Message {
var body: String
// ...
}
Thus you could use it as:
let data = MyData(activities: [Activity(action: 1, state: 1)], messages: [Message(body: "hi")])
let myModel = Model(messageCode: "API_200", data: data, message: "")
However, if you -for some reason- have to declare it as a dictionary, it could be something like this:
let myDict: [String: Any] = [
"messageCode": "API_200",
"data": ["activities": [["action": 1, "state": 1]],
"messages": [["body": "hi"]]
],
"message": ""
]
which means that myDict is a dictionary contains:
messageCode string.
data as nested dictionary, which contains:
activities array of dictionaries (array of [String: Int]).
messages array of dictionaries (array of [String: String]).
message string.
One of the simplest reasons why you should go with the modeling approach is because when it comes to read from myModel, all you have to do is to use the dot . notation. Unlike working with it as a dictionary, you would have to case its values which could be a headache for some point. For instance, let's say that we want to access the first message body in data messages array:
Model:
myModel.data.messages.first?.body
Dictionary:
if let data = myDict["data"] as? [String: [[String: Any]]],
let messages = data["messages"] as? [[String: String]],
let body = messages.first?["body"] {
print(body)
}
Since you explicitly want it as [String:AnyObject]:
var dict: [String:AnyObject] = ["messageCode":"API_200" as AnyObject,
"data": ["activities": [["action":1,
"state":1]],
"messages": [["body":"hi"]]] as AnyObject,
"message": "" as AnyObject]
Basically all the root values should be typecasted as AnyObject
Or the long way:
//Activities is as Array of dictionary with Int values
var activities = [[String:Int]]()
activities.append(["action": 1,
"state": 1])
//Messages is an Array of string
var messages = [[String:String]]()
messages.append(["body" : "hi"])
//Data is dictionary containing activities and messages
var data = [String:Any]()
data["activities"] = activities
data["messages"] = messages
//Finally your base dictionary
var dict = [String:AnyObject]()
dict["messageCode"] = "API_200" as AnyObject
dict["data"] = data as AnyObject
dict["message"] = "" as AnyObject
print(dict)
Parsing this to get your data back will be hell; with all the type casts and all.
Example (lets capture action):
let action = ((dict["data"] as? [String:Any])?["activities"] as? [String:Int])?.first?.value
As you can see you need to typecast at every level. This is the problem with using dictionaries in Swift. Too much cruft.
Sure, you could use a third-party library like SwiftyJSON to reduce the above to:
let action = dict["data"]["activities"][0]["action"]
But do you want a dependency just for something as simple as this?
Instead...
If your structure is defined then create models instead; as Ahmad F's answer suggests. It will be more readable, maintainable and flexible.
...but since you asked, this is how one would do it with pure Dictionary elements.