Unable to access codable struct values in Swift - ios

I am fetching user's data as json from a service and decoding into Codable User struct. I can access that property where I've fetched the response but I want to access that User struct property somewhere else, let's say in another class, function, etc.
I'm new to this and I'm thinking the ideal approach is to "at the time of fetching, store that data into Core Data or User Defaults and then update my views accordingly.
Please suggest what's the best and appropriate approach or wether there is any way to access codable struct values directly.
Here is my codable struct -
struct User: Codable {
let name : String?
enum CodingKeys: String, CodingKey {
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
The wrong way I'm accessing struct in some function is -
UserInfo.CodingKeys.name.rawValue
//Output is the key string - 'Name'

I think static can help you
struct User: Codable {
let name : String?
private enum CodingKeys: String, CodingKey {
case name = "Name"
}
}
assume fetching data here
class FetchingClass{
static var user: User?
func fetchData(){
//After Url request success
do {
//assign directly to static varibale user
FetchingClass.user = try JSONDecoder().decode(User.self, from: data)
} catch _ {
}
}
}
use like this wherever you want without using coreData or UserDefaults
class AnotherClass{
func anotherClassFunc(){
//use if let or guard let
if let user = FetchingClass.user{
print(user.name)
}
//or
if let FetchingClass.user != nil {
print(FetchingClass.name)
}
}
}

You can try using a singleton reference
struct User: Codable {
let name : String?
enum CodingKeys: String, CodingKey {
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
User.shared = self
}
}
Create another class which hold the ref. of User
final class AppGlobalManager {
static let sharedInstance = AppGlobalManager()
var currentUser: User? // Initialise it when user logged in
private init()
}
Then you can access any of the user data as
AppGlobalManager.sharedInstance.currentUser.name

Related

Swift Codable struct with a generic property

Say we've got a cursor based paginated API where multiple endpoints can be paginated. The response of such an endpoint is always as follows:
{
"nextCursor": "someString",
"PAYLOAD_KEY": <generic response>
}
So the payload always returns a cursor and the payload key depends on the actual endpoint we use. For example if we have GET /users it might be users and the value of the key be an array of objects or we could cal a GET /some-large-object and the key being item and the payload be an object.
Bottom line the response is always an object with a cursor and one other key and it's associated value.
Trying to make this generic in Swift I was thinking of this:
public struct Paginable<Body>: Codable where Body: Codable {
public let body: Body
public let cursor: String?
private enum CodingKeys: String, CodingKey {
case body, cursor
}
}
Now the only issue with this code is that it expects the Body to be accessible under the "body" key which isn't the case.
We could have a struct User: Codable and the paginable specialized as Paginable<[Users]> where the API response object would have the key users for the array.
My question is how can I make this generic Paginable struct work so that I can specify the JSON payload key from the Body type?
The simplest solution I can think of is to let the decoded Body to give you the decoding key:
protocol PaginableBody: Codable {
static var decodingKey: String { get }
}
struct RawCodingKey: CodingKey, Equatable {
let stringValue: String
let intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
intValue = nil
}
init(intValue: Int) {
stringValue = "\(intValue)"
self.intValue = intValue
}
}
struct Paginable<Body: PaginableBody>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
body = try container.decode(Body.self, forKey: RawCodingKey(stringValue: Body.decodingKey))
cursor = try container.decodeIfPresent(String.self, forKey: RawCodingKey(stringValue: "nextCursor"))
}
}
For example:
let jsonString = """
{
"nextCursor": "someString",
"PAYLOAD_KEY": {}
}
"""
let jsonData = Data(jsonString.utf8)
struct SomeBody: PaginableBody {
static let decodingKey = "PAYLOAD_KEY"
}
let decoder = JSONDecoder()
let decoded = try? decoder.decode(Paginable<SomeBody>.self, from: jsonData)
print(decoded)
Another option is to always take the "other" non-cursor key as the body:
struct Paginable<Body: Codable>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
let cursorKey = RawCodingKey(stringValue: "nextCursor")
cursor = try container.decodeIfPresent(String.self, forKey: cursorKey)
// ! should be replaced with proper decoding error thrown
let bodyKey = container.allKeys.first { $0 != cursorKey }!
body = try container.decode(Body.self, forKey: bodyKey)
}
}
Another possible option is to pass the decoding key directly to JSONDecoder inside userInfo and then access it inside init(from:). That would give you the biggest flexibility but you would have to specify it always during decoding.
You can use generic model with type erasing, for example
struct GenericInfo: Encodable {
init<T: Encodable>(name: String, params: T) {
valueEncoder = {
var container = $0.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: . name)
try container.encode(params, forKey: .params)
}
}
// MARK: Public
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
// MARK: Internal
enum CodingKeys: String, CodingKey {
case name
case params
}
let valueEncoder: (Encoder) throws -> Void
}

Custom Decodable JSON - Get first array element

I have JSON data that looks like this:
{
// Other attributes here....
"id": "a4s5d6f8ddw",
"images": {
"selection": [
{
"url": "https://www.myimage.com"
},
// ... more images here
]
}
}
I want to extract the id and the first url from the nested JSON and store it in my struct. I've tried something like this, but I can't quite get it:
struct MyImage {
let id: String
let url: URL
enum CodingKeys: CodingKey {
case id
case images
}
enum Selection: CodingKey {
case selection
}
enum ImageURL: CodingKey {
case url
}
}
extension MyImage: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
let selection = try values.nestedContainer(keyedBy: Selection.self, forKey: .selection)
// Then what?? Need to extract just the first one and then decode to a URL
}
How can I finish this off?
You're on the right track, but some minor errors:
struct MyImage {
let id: String
let url: URL
enum CodingKeys: CodingKey {
case id
case images
}
// Keys inside of Images
enum ImagesKeys: CodingKey {
case selection
}
// Struct inside of Selection
private struct Selection: Decodable {
let url: URL
}
}
With that, the decoder is close to what you were thinking:
extension MyImage: Decodable {
init(from decoder: Decoder) throws {
// Decode the outer stuff
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
// Pull off the images object
let images = try values.nestedContainer(keyedBy: ImagesKeys.self, forKey: .images)
// Extract the selections array
var selections = try images.nestedUnkeyedContainer(forKey: .selection)
// Take the first element
let firstSelection = try selections.decode(Selection.self)
// Done.
self.url = firstSelection.url
}
}

Swift - Initialise model object with init(from decoder:)

Below is my model struct
struct MovieResponse: Codable {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
extension MovieResponse {
struct Movie: Codable, Identifiable {
var id = UUID()
var title: String
var year: Int8
var imdbID: String
var type: String
var poster: URL
enum EncodingKeys: String, CodingKey {
case title = "Title"
case year = "Year"
case imdmID
case type = "Type"
case poster = "Poster"
}
}
}
Now in a ViewModel, I am creating an instance of this model using the below code
#Published var movieObj = MovieResponse()
But there is a compile error saying, call init(from decoder) method. What is the proper way to create a model instance in this case?
As the Swift Language Guide reads:
Swift provides a default initializer for any structure or class that provides default values for all of its properties and doesn’t provide at least one initializer itself.
The "and doesn’t provide at least one initializer itself" part is crucial here. Since you are declaring an additional initializer you should either declare your own initialized like so:
init(
totalResults: Int,
response: String,
error: String,
movies: [Movie]
) {
self.totalResults = totalResults
self.response = response
self.error = error
self.movies = movies
}
or move Codable conformance to an extension so Swift can provide you with a default initialiser. This would be a preferred way to do it (my personal opinion, I like to move additional protocol conformances to extensions).
struct MovieResponse {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
}
extension MovieResponse: Codable {
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
You need to add another initializer if you do not want to use a decoder. Swift give you one for free if and only if you do not write your own initializer. Now that you have one you loose the free one.
add another:
init() {
//Your initializer code here
}
If you are trying to use the decoder init you need to use a decoder to invoke it. For instance if it's Json
#Published var movieObj = try? JSONDecoder().decode(MovieResponse.self, from: <#T##Data#>)

How do I set up a decodable JSON Struct so that nil values are just skipped?

I am using the following code to save JSON data. However, on occasion, data is presented as nil. Is it possible to ignore this or set a standard value in the event that nil is returned?
struct Information: Decodable {
public let value: Double?
private enum CodingKeys: String, CodingKey {
case value = "value"
}
}
Declaring custom decoding logic may work in a bad way:
struct Information: Decodable {
public let value: Double
private enum CodingKeys: String, CodingKey {
case value = "value"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let preValue = try values.decode(Double?.self, forKey: .value)
value = preValue ?? 0.0
}
}
For a good one, consider vadian's note

Is it possible to have an array of structs which conform to the same protocol also support Codable?

I have setup the following protocol, and have 2 structs which then conform to this protocol:
protocol ExampleProtocol: Decodable {
var name: String { get set }
var length: Int { get set }
}
struct ExampleModel1: ExampleProtocol {
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol {
var name: String
var length: Int
var dateString: String
}
I want to deserialise some JSON data I receive from the server, and I know it will be returning a mix of both ExampleModel1 and ExampleModel2 in an array:
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
Is there anyway to use the Codable approach and support both models easily? Or will I need to manually deserialise the data for each model?
EDIT 1:
Conforming to Decodable on the structs, still gives the same results:
struct ExampleModel1: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, otherData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.otherData = try container.decode(Array<String>.self, forKey: .otherData)
}
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, dateString
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.dateString = try container.decode(String.self, forKey: .dateString)
}
var name: String
var length: Int
var dateString: String
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
If you have a limited amount of ExampleProtocols and you need to have a different type of ExampleProtocols in the same array, then you can create a holder for ExampleProtocol and use it for decoding/encoding.
ExampleHolder could hold all possible Decodable ExampleProtocol types in one array. So decoder init don't need to have so many if-else scopes and easier to add more in the future.
Would recommend keeping ExampleHolder as a private struct. So it's not possible to access it outside of file or maybe even not outside of ExampleNetworkResponse.
enum ExampleNetworkResponseError: Error {
case unsupportedExampleModelOnDecoding
}
private struct ExampleHolder: Decodable {
let exampleModel: ExampleProtocol
private let possibleModelTypes: [ExampleProtocol.Type] = [
ExampleModel1.self,
ExampleModel2.self
]
init(from decoder: Decoder) throws {
for type in possibleModelTypes {
if let model = try? type.init(from: decoder) {
exampleModel = model
return
}
}
throw ExampleNetworkResponseError.unsupportedExampleModelOnDecoding
}
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
enum CodingKeys: String, CodingKey {
case someString, modelArray
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
someString = try container.decode(String.self, forKey: .someString)
let exampleHolderArray = try container.decode([ExampleHolder].self, forKey: .modelArray)
modelArray = exampleHolderArray.map({ $0.exampleModel })
}
}
–––––––––––––––––––––––––––––––––
If in one response can have only one type of ExampleProtocol in the array then:
struct ExampleNetworkResponse2<ModelArrayElement: ExampleProtocol>: Decodable {
var someString: String
var modelArray: Array<ModelArrayElement>
}
usage:
let decoder = JSONDecoder()
let response = try decoder.decode(
ExampleNetworkResponse2<ExampleModel1>.self,
from: dataToDecode
)

Resources