What's the appropriate way to access an enum from Swift Decodables? - ios

I have a really weird case where I took JSON and thought I would be able to substring all the way to the data I want to access, however, it's not working as expected.
Using QuickType, I was able to convert this JSON: https://kidsuper.fodalabs.com/wp-json/wp/v2/art
To the below.
When trying to access, it seems like I should be able to do .acf.gallery.id however once I get to acf.gallery, it says .id does not exist. This is strange but here's what it returns when I try
let temp = imageArray[indexPath.row].acf.gallery.id
Value of type 'GalleryUnion?' has no member 'id'
Just for fun I tried this one and had no luck as well:
let temp = imageArray[indexPath.row].acf.GalleryUnion.galleryElementArray
Error
: Value of type 'Acf' has no member 'GalleryUnion'
The return when I print .acf.gallery starts like this:
Acf(company: "Season #3",
gallery: Optional(weddingszn.GalleryUnion.galleryElementArray([weddingszn.GalleryElement(
id: 135, galleryID: 135, title: "1-791x1024",
filename: "1-791x1024.jpg", url: "https://kidsuper.fodalabs.com/wp-content/up
Full code is below for what I'm trying to parse. Any ideas?
struct Acf: Codable {
let company: String
let gallery: GalleryUnion?
let tagline: String
let featuredImg: Bool?
enum CodingKeys: String, CodingKey {
case company, gallery, tagline
case featuredImg = "featured_img"
}
}
enum GalleryUnion: Codable {
case bool(Bool)
case galleryElementArray([GalleryElement])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode([GalleryElement].self) {
self = .galleryElementArray(x)
return
}
throw DecodingError.typeMismatch(GalleryUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for GalleryUnion"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .galleryElementArray(let x):
try container.encode(x)
}
}
}
struct GalleryElement: Codable {
let id, galleryID: Int
let title, filename: String
let url: String
let alt, author, description, caption: String
let name, date, modified: String
let mimeType: MIMEType
let type: TypeEnum
let icon: String
let width, height: Int
let sizes: Sizes
enum CodingKeys: String, CodingKey {
case id = "ID"
case galleryID = "id"
case title, filename, url, alt, author, description, caption, name, date, modified
case mimeType = "mime_type"
case type, icon, width, height, sizes
}
}

You have to use if case, guard case or switch case to unpack the enum before you drill down into the array.
if case .galleryElementArray(let x) = imageArray[indexPath.row].acf.gallery {
print(x.first!.id)
}

however once I get to acf.gallery, it says .id does not exist. This is strange
No it isn't. According to your code, .gallery should be a GalleryUnion. Well, a GalleryUnion has no .id. It has no properties at all. A GalleryElement has an .id, but a GalleryUnion is not a GalleryElement. So I don't see where your surprise comes from; there is no surprise here.
A GalleryUnion has cases. You need to check which case this is. If it's . galleryElementArray you need to extract the associated value. Even then you won't have any .id, because what you will now have is still not a GalleryElement; it's an array of GalleryElements.
You could make this a lot easier on yourself by defining GalleryUnion with an extra calculated property that fetches the associated value for you:
enum GalleryUnion : Codable {
case bool(Bool)
case galleryElementArray([GalleryElement])
var galleryElements : [GalleryElement]? {
switch self {
case .bool : return nil
case let .galleryElementArray(arr): return arr
}
}
// ... and so on
}
That would allow you, at least, to say:
act.gallery.galleryElements?.map {$0.id}
...or whatever it is you have in mind.

So, GalleryUnion can one of two things. It can either both .bool(_) or galleryElementArray(_). When you want access the actual underlying value, you need to determine which state it's in.
To do this in Swift, you can use a switch statement. You can then use it to gain access to the internally contained values. Maybe something similar to:
if let gallery = acf.gallery {
switch gallery {
case .galleryElementArray(let values):
values.forEach {
print($0.id)
}
case .bool(_):
break
}
}
You might like to have a read of Enumerations, look for the "Associated Values" sections

Related

SwiftUI Can't Pass String to KeyPath

I have an app that references a large dataset that I load from an external public
site as a comma separated value file. I parse the data to a an array of a model called
WaterPointModel. An abbreviated version is:
struct WaterPointModel: Identifiable {
let id = UUID()
let STATE: String
let COUNTY: String
let AQWFrTo: Double
let AQWGWSa: Double
let AQWGWTo: Double
//many more
}
I then want to summarize(reduce) the data. My function for this is:
func sumOnAttributeUSList(sumField: KeyPath<WaterPointModel,Double>) -> Double {
return dataStore.waterPointModels.map({ $0[keyPath:sumField] }).reduce(0, +)
}
Next I want to call this to build a report:
let aqWFrTo = sumOnAttributeUSList(sumField: \.AQWFrTo)
This all works. However there are other reports where I need to pass a string to
create that keypath. Say I have a lookup table where I lookup "abc" and get "AQWFrTo". I
would like to do something like this in a loop:
let abc = "AQWFrTo"
let aqWFrTo = sumOnAttributeUSList(sumField: \WaterPointModel.abc)
I have not been able to make any version of this work. Any guidance would be appreciated.
Xcode 13.3.1, iOS 15.4
A simple approach is this:
func toKeypath(_ str: String) -> KeyPath<WaterPointModel,Double>? { // <-- with or without optional
switch str {
case "AQWFrTo": return \.AQWFrTo
case "AQWGWSa": return \.AQWGWSa
case "AQWGWTo": return \.AQWGWTo
// ...
default: return nil // <-- or a default Keypath
}
}
let aqWFrTo = sumOnAttributeUSList(sumField: toKeypath("AQWFrTo"))

Map array of objects to Dict with key as Enum

I got a dict of enum as key and values as object
#Published var subscriptionProducts = [SubscriptionType: SKProduct]()
enum SubscriptionType {
case monthly = "uniqueID"
case annualy = "uniqueID"
}
I got another array of type SKProduct
I want to assign it to my dict with a property of each object as key, and the object itself as value
Trying
subscriptionProducts = Dictionary(uniqueKeysWithValues:products.map({$0.productIdentifier , $0 }))
But I got
Type of expression is ambiguous without more context
the productIdentifier is of type String, how to assign it to the rawValue of my enum?
The result I want to achieve later is to get the specific object using that key
let monthlySubscriptionProduct = subscriptionProducts[.monthly]
You need to first declare your SubscriptionType enumeration RawValue as String. Then you will need to convert your productIdentifier String value to SubscriptionType using its fallible init(rawValue:) initializer. Something like:
enum SubscriptionType: String {
case monthly, annualy
}
let subscriptionProducts: [SubscriptionType: SKProduct] = Dictionary(uniqueKeysWithValues: products.compactMap {
guard let subscriptionType = SubscriptionType(rawValue: $0.productIdentifier) else {
return nil
}
return (subscriptionType, $0)
})
let monthlySubscriptionProduct = subscriptionProducts[.monthly]
But IMO that's not what you really want. Looks like what you are trying to accomplish is to group all monthly subscriptions which can be done using reduce as follow:
let subscriptionProducts: [SubscriptionType: [SKProduct]] = products.reduce(into: [:]) {
guard let subscriptionType = SubscriptionType(rawValue: $1.productIdentifier) else {
return
}
$0[subscriptionType, default: []].append($1)
}
let monthlySubscriptions = subscriptionProducts[.monthly] ?? []

Unable to extract data properly from JSON in swift

I have a this kind of json object in my response after parsing json string to object
[
"requestId": 1,
"response": {
code = SUCCESS;
},
"messageId": ACTION_COMPLETE
]
I am trying to extract requestId using
responseMsg["requestId"] as! Int
I am getting this error
Could not cast value of type 'NSTaggedPointerString' (0x21877a910) to
'NSNumber' (0x218788588).
I tried it changing to Int(responseMsg["requestId"] as! String)!
This thing is working for positive numbers but not for negative numbers probably bcz when requestId = -2 it throws me an error
Could not cast value of type '__NSCFNumber' (0x21877a000) to
'NSString' (0x218788290).
I tried with different other solution too but did not work.
For parsing the JSON data, its better use Codable instead of manually parsing everything.
For JSON format,
{
"requestId": 1,
"response": {
"code":"SUCCESS"
},
"messageId": "ACTION_COMPLETE"
}
Create Models like,
struct Root: Decodable {
let requestId: String?
let messageId: String
let response: Response
enum CodingKeys: String, CodingKey {
case requestId, messageId, response
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let id = try? values.decode(Int.self, forKey: .requestId) {
requestId = String(id)
} else if let id = try? values.decode(String.self, forKey: .requestId) {
requestId = id
} else {
requestId = nil
}
messageId = try values.decode(String.self, forKey: .messageId)
response = try values.decode(Response.self, forKey: .response)
}
}
Now, parse the JSON data using,
do {
let root = try JSONDecoder().decode(Root.self, from: data)
print(root.requestId) //access requestId like this....
} catch {
print(error)
}
Try
Int(String(describing: responseMsg["requestId"]))!
This ensures any data is converted to string first and then to int
This error message
Could not cast value of type 'NSTaggedPointerString' (0x21877a910) to 'NSNumber' (0x218788588).
tells us that the JSON request id is being parsed as a string. NSTaggedPointerString is a special internal type used by the ObjC runtime to represent strings.
Try this:
let requestId = responseMsg["requestId"] as! String
print("request id: \(requestId)") // Prints a string
Note, it may print something that looks like a number, but it isn't one.
The JSON you are parsing probably looks like
{
"requestId": "1",
"response": {
"code" = "SUCCESS"
},
"messageId": "ACTION_COMPLETE"
}
Note the 1 in quotes.
Swift 5
String interpolation is what worked for me! (casting it first to String didn't work as I had other values for which the json decoder actually did its job and cast them directly to a number)
if let responseMsg_any = responseMsg["requestId"],
let responseMsg_int = Int("\(responseMsg_any)") {
//..
}
Warning:
This solution allows any Type to become a String and be checked for a Int value. Only use this if you're not concerned about the value's Type prior to interpolation.

Returning a processed String of an Enum on Swift iOS

I was just creating a localization module for my project, and as I'm new to Swift I had an idea of if the following was possible.
I have an enum like this:
enum Localizations : String
{
case StringId1 = "string_to_translate_1"
case StringId2 = "string_to_translate_2"
case StringId3 = "string_to_translate_3"
var localized : String {
return NSLocalizedString(self.rawValue, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: "")
}
}
With this enum I can have the localized string with this command:
let myString = Localizations.StringId1.localized
But when you have to put lots of strings the .localized it's like redundant as you already have Localizations before.
So what I'm looking for is if I can do something like this:
let myString = Localizations.StringId1
And myString would be something like "Press Button to Continue"
I have managed to do something working, but not in all cases.
Found in this link: https://appventure.me/2015/10/17/advanced-practical-enum-examples/
On the 'Advanced Enum Usage Protocols' Step it suggests a modification like the following would get what I want:
protocol CustomStringConvertible {
var description: String { get }
}
enum Trade: CustomStringConvertible {
case Buy, Sell
var description: String {
switch self {
case Buy: return "We're buying something"
case Sell: return "We're selling something"
}
}
}
let action = Trade.Buy
print("this action is \(action)")
// prints: this action is We're buying something
My modifications are theses ones:
protocol CustomEnumString {
var localized: String { get }
}
enum Localizations : String, CustomEnumString
{
case StringId1 = "string_to_translate_1"
case StringId2 = "string_to_translate_2"
case StringId3 = "string_to_translate_3"
var localized : String {
return NSLocalizedString(self.rawValue, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: "")
}
}
But when printing it if shows me the enum literal, and when passing to a function the compilers tells me it's invalid:
let localizedString = Localizations.StringId1
print("localization: \(localizedString)")
// prints: "localization: StringId1"
// note: AlertStrings is an struct with two strings
// this fails to compile saying that cannot convert value of type 'Localizations' to expected argument type 'String'
let alertStrings = AlertStrings(title: Localizations.StringId1, message: Localizations.StringId2)
// this one works, but it's not the purpose I had in mind
let alertStrings = AlertStrings(title: Localizations.StringId1.localized, message: Localizations.StringId2.localized)
So... in short, I would like to be able to do this:
let localizedString = Localizations.StringId1
print("localization: \(localizedString)")
// prints: "localization: Press Button To Continue"
let alertStrings = AlertStrings(title: Localizations.StringId1, message: Localizations.StringId2)
But in the enum I only would like to specify literals once, not first on case's and later inside a switch.
Thanks in advance!
phelgo, A member of NSBarcelona just told me about this, and works perfectly!
enum Localizations {
static let StringId1 = NSLocalizedString("string_to_translate_1", comment: "")
}
let myString = Localizations.StringId1
It may look unfamiliar to have an enum with no cases, but we get to
keep all of its safety (and code completion), while still preventing
Localizations from being instantiated by mistake (if it was a
struct)
Conform your enum to CustomStringConvertible and rename localized to description:
enum Localizations: String, CustomStringConvertible
{
case StringId1 = "string_to_translate_1"
case StringId2 = "string_to_translate_2"
case StringId3 = "string_to_translate_3"
var description : String {
return NSLocalizedString(self.rawValue, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: "")
}
}
let alertStrings = AlertStrings(title: Localizations.StringId1.description, message: Localizations.StringId2.description)

swift text with 2 variables and different language

i would like to translate a string, which have two variables inside.
at the moment, i use for translating this code:
NSLocalizedString("Name_In_Langauge_String_File",comment:"")
but how can i translate the following string?
This is a test with 100 Pictures and 50 Users
where 100 and 50 are variables.
Put this in you Localizable.strings:
"Name_In_Langauge_String_File" = "This is a test with %d Pictures and %d Users";
and in your code:
String.localizedStringWithFormat(
NSLocalizedString("Name_In_Langauge_String_File",
comment: ""),
pictures,
users)
In a project I was working on I noticed that we kept repeating the code to do the string formatting for the localization file. This meant you could not just use the value, you first needed to check what parameters were required. One way to avoid this problem is to use Swift enums. This method is also useful for unit testing your localizations.
Assume you have the following 3 localizations in your strings file:
"TestNoParams" = "This is a test message";
"TestOneParam" = "Hello %#";
"TestTwoParams" = "This is a test with %d Pictures and %d Users";
Now you can use the following enum, protocol and extension to reference your strings:
protocol LocalizationProtocol {
var key: String { get }
var value: String { get }
}
extension LocalizationProtocol {
private func localizationValue() -> String {
return NSLocalizedString(key, comment:key)
}
private func localizationValueWithFormat(parameters: CVarArgType...) -> String {
return String(format: localizationValue(), arguments: parameters)
}
}
enum Localizations: LocalizationProtocol {
case TestNoParams
case TestOneParam(name: String)
case TestPicturesAndUsers(pictures: Int, users: Int)
var key: String {
switch self {
case .TestNoParams: return "TestNoParams"
case .TestOneParam: return "TestOneParam"
case .TestPicturesAndUsers: return "TestTwoParams"
}
}
var value: String {
switch self {
case .TestOneParam(let name):
return localizationValueWithFormat(name)
case .TestPicturesAndUsers(let pictures, let users):
return localizationValueWithFormat(pictures, users)
default:
return localizationValue()
}
}
}
Now to use it you just need to call the enums value method:
let testNoParam = Localizations.TestNoParams.value
let testOneParam = Localizations.TestOneParam(name: "users name").value
let testTwoParams = Localizations.TestPicturesAndUsers(pictures: 4, users: 500).value
The example I have shown is simplified, but you can also nest enums to provide a nice grouping for your localizations. For instance you could have your enums nested by ViewController. This is an example for a welcome message: Localizations.Main.WelcomeMessage.value

Resources