Default initialiser in protocol requires unwanted mutable properties - ios

In Swift 3: Imagine you want your models to be value types (struct) through out your app. But you would also like persistence of said models using Core Data/Realm, which requires you to create classes. Then you can convert your structs to classes and vice verse using JSON (which would require structs and classes to both support JSON deserialization and serialization).
Wouldn't it be neat if you don't have to write JSON deserialization (and analogously for serialization, but I'm focusing on deserialization here) in two places, but use put deserialization in a protocol, that both your struct and class uses.
Using structs we want our JSON model to have immutable fields, thus all properites being let constants. But using a protocol implementation of the deserialization does not allow for this AFAIK.
The code example below works, but it is ugly, because of all unwanted requirements (UR) marked in comments in the code.
struct JSON {
let json: [String: Any]
func value<Value>(_ key: String) throws -> Value {
guard let value = json[key] as? Value else { throw NSError() }
return value
}
}
protocol JSONDeserializable {
init(json: JSON) throws
}
protocol UserModel: JSONDeserializable {
var username: String { get set } // Unwanted requirement (UR) #1: property needs "set" so that it can be initialized within protocol
init() // UR2: needs empty init, because of default implementation of `init(json: JSON)` in `extension UserModel`
}
extension UserModel {
init(json: JSON) throws {
self.init() // UR3: Needs to call this otherwise compilation error: `'self' used before chaining to another self.init requirement`
username = try json.value("username")
}
}
struct UserStruct: UserModel {
// UR4: property cannot be `let`, beause of `set` in protocol.
var username: String = "" // UR5: Property have to have default value because of it being a empty init
init() {}
}
final class UserClass: NSObject, UserModel {
// UR6: analogue with UR4
var username: String = "" // UR7: analogue with UR5
}
let json: JSON = JSON(json: ["username": "Sajjon"])
let u1 = try UserStruct(json: json)
let u2 = try UserClass(json: json)
print(u1.username) // prints "Sajjon"
print(u2.username) // prints "Sajjon"
Is there another way of achieving this, with a lower amount of unwanted requirements? Or an optimal solution with zero UR? 🙄

Thanks to what #hamish pointed out, the best solution would be (where struct JSON and protocol JSONDeserializable remains the same as in the question). This is not a perfect solution since you have to implement the initializer of the class. The neat part is that you don't have to implement any initializer for the struct, since it has one implicitly.
protocol UserModel: JSONDeserializable {
var username: String { get }
var firstname: String { get }
var country: String { get }
init(
username: String,
firstname: String,
country: String
)
}
extension UserModel {
init(json: JSON) throws {
self.init(
username: try json.value("username"),
firstname: try json.value("firstname"),
country: try json.value("country")
)
}
}
struct UserStruct: UserModel {
let username: String
let firstname: String
let country: String
// struct has default initializer
}
final class UserClass: UserModel {
let username: String
let firstname: String
let country: String
init(
username: String,
firstname: String,
country: String
) {
self.username = username
self.firstname = firstname
self.country = country
}
}
let json: JSON = JSON(json: [
"username": "Sajjon",
"firstname": "Alexander",
"country": "Sweden"
])
let u1 = try UserStruct(json: json)
let u2 = try UserClass(json: json)
print(u1.username) // prints "Sajjon"
print(u2.username) // prints "Sajjon"

Related

Optional list of NSData in Realm - Swift

I need to save a list of images as NSData in Realm. I tried using Realm optional but realmOptional<NSdata> can't be used because realmOptional does not conform to type NSDate.
Is there a way to do it?
Edit: Basically all I want is to be able to store a list of NSData but optional
something like:
#objc dynamic var photos: List<NSData>?
Solution for optional List types when using Decodable
In my case I needed an optional List because I'm decoding json into Realm objects, and it's possible that the property might not exist in the json data. The typical workaround is to manually decode and use decodeIfPresent(). This isn't ideal because it requires boilerplate code in the model:
class Driver: Object, Decodable {
var vehicles = List<Vehicle>()
var name: String = ""
// etc
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.vehicles = try container.decodeIfPresent(List<Vehicle>.self, forKey: .vehicles) ?? List<Vehicle>()
self.name = try container.decode(String.self, forKey: .name)
// etc
}
}
However, we can avoid the boilerplate by extending KeyedDecodingContainer with an overload for List types. This will force all lists to be decoded using decodeIfPresent():
Realm v5:
class Driver: Object, Decodable {
var vehicles = List<Vehicle>()
var name: String = ""
//etc
}
class Vehicle: Object, Decodable {
var drivers = List<Driver>()
var make: String = ""
// etc
}
extension KeyedDecodingContainer {
// This will be called when any List<> is decoded
func decode<T: Decodable>(_ type: List<T>.Type, forKey key: Key) throws -> List<T> {
// Use decode if present, falling back to an empty list
try decodeIfPresent(type, forKey: key) ?? List<T>()
}
}
Realm v10+:
class Driver: Object, Decodable {
#Persisted var vehicles: List<Vehicle>
#Persisted var name: String = ""
// etc
}
class Vehicle: Object, Decodable {
#Persisted var drivers: List<Driver>
#Persisted var make: String = ""
// etc
}
extension KeyedDecodingContainer {
// This will be called when any #Persisted List<> is decoded
func decode<T: Decodable>(_ type: Persisted<List<T>>.Type, forKey key: Key) throws -> Persisted<List<T>> {
// Use decode if present, falling back to an empty list
try decodeIfPresent(type, forKey: key) ?? Persisted<List<T>>(wrappedValue: List<T>())
}
}
Edit: Clarified code examples, fixed typo
The List cannot be optional, but the Objects in the list can be optional, you have to declare it like:
#Persisted var value: List<Type?>
Here is the link with the Supported data types
https://www.mongodb.com/docs/realm/sdk/swift/data-types/supported-property-types/
according to https://realm.io/docs/swift/latest/#property-cheatsheet you can not define optional lists in realm

Cast from '(key: String, value: [Dates])' to unrelated type 'Dates' always fails

As my objective is to use the MVVM architecture,I have configured my Model class fine, but now having troubles with initialising my ViewModel class. It gives me an error saying "Cast from '(key: String, value: [Any])' to unrelated type 'Dates' always fails". When I run it, it crashes. Any help would much appreciate.
View Model Class as follow
struct JobsViewModel {
private let title : String?
private let imageArray : [Formats]?
private let category : String?
private let shifts : [ShitDetails]?
private let distance : String?
//Dependency injection
init(mainData:Dates) {
self.title = mainData.title
self.imageArray = mainData.client?.photos
self.shifts = mainData.shifts
self.category = mainData.job_category?.description
self.distance = mainData.distance
}
}
Model Class as follow
public struct Schedule: Codable {
public let data : [String:[Dates]]
}
public struct Dates: Codable {
public let title: String?
public let distance: String?
public let client: Images?
public let shifts: [ShitDetails]?
public let job_category:JobCategory?
}
On success of my API call I'm trying to initailize it as bellow and its where it crash.
var jobsViewModel = [JobsViewModel]() //jobsViewModel is a instance variable
Network.shared.retrieveHotelDetails(successBlock: { (results) in
let mainData = results as? Schedule
self.jobsViewModel = mainData?.data.map({return JobsViewModel(mainData: $0 as! Dates)}) ?? []
}
{"2018-06-07": [
{
"someKey": "Test1",
"someKey": "Test1"
}
],
"2018-06-06": [
{
"someKey": "Test1",
"someKey": "Test1"
}
]}
So you're trying to map from that (mainData?.data is a [String:[Dates]] Dictionary) to a "JobsViewModel" Array.
Your main issue is that you essentially try to map from something that contains multiple arrays to one array. If you want to do that, flatMap will be your best option.
Just like this:
self.jobsViewModel = mainData.data.flatMap({ $0.value }).map({ JobsViewModel(mainData: $0 )})
This first "flat maps" all of the Dates Arrays in your dictionary into one large dictionary that contains them all and then creates (via "map") a JobsViewModel for each of them by using each Dates object in the JobsViewModel initializer.

Swift: Decodable

Lets say i have this json from an API request:
friends: {
"john":31,
"mark":27,
"lisa":17,
"tom":41
}
I usually expect it in an array format:
friends: [
{ "john":31 },
{ "mark":27 },
{ "lisa":17 },
{ "tom":41 }
]
But the API doesn't provide me this way the results. So i want finally to map it to an array of [Friend], where Friend is:
class Friend: Decodable {
let name: String
let age: Int
}
How should i serialize this json to get [Friend] ?
First of all, example isn't valid json at all. To be valid it either shouldn't include "friends" label, or it should be embedded in another object like this
{
"friends": {
"john":31,
"mark":27,
"lisa":17,
"tom":41
}
}
If I understand question correctly, you want to decode json object to swift array. I don't think there is a way to do so without writing custom decoding. Instead, you can decode json into Dictionary and when manually map it like so
struct Friend {
let name: String
let age: Int
}
struct Friends: Decodable {
let friends: [String: Int]
}
let friends = try! JSONDecoder().decode(Friends.self, from: json.data(using: .utf8)!)
.friends
.map { (name, age) in Friend(name: name, age: age) }
Disclaimer: I recommend to change you API format to one like in #scriptable's answer (which was deleted while I was answering, hm), where name and age fields are properly defined. And where you're not limited to basically a pair of key-value to parse.
But if you can't or won't change it you may use something like this to decode your Friend type:
struct Friend: Decodable {
let name: String
let age: UInt
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String: UInt].self)
guard dictionary.count == 1, let (name, age) = dictionary.first else {
throw DecodingError.invalidFriendDictionary
}
self.name = name
self.age = age
}
enum DecodingError: Error {
case invalidFriendDictionary
}
}
struct Friends: Decodable {
let friends: [Friend]
}
let friends = try JSONDecoder().decode(Friends.self, from: data)
It assumes key is name and value is age. And checks that there's only a one pair to parse.

Swift 4 JSON Decode Root Array

I am having problems in decoding JSON response using Swift 4 Decoding Functionality.
I have main construct and it has one inner construct var hr_employees: [Employee]? = []. The problem is JSON not mapping for 'var hr_employees: [Employee]? = [].
I am getting correct values forthe three root values response_status,access_level,session_token.
////////////Code for Struct////////////////////////
struct EmployeeData: Codable {
var response_status:Int=0
var access_level:Int=0
var session_token:String=""
var hr_employees: [Employee]? = []
}
private enum CodingKeys: String, CodingKey {
case response_status="response_status"
case access_level="access_level"
case session_token="session_token"
case hr_employees="hr_employees"
}
init() {
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
response_status = try values.decode(Int.self, forKey: .response_status)
do{
session_token = try values.decode(String.self, forKey: .session_token)
}catch {
print( "No value associated with key title (\"session_token\").")
}
do{
access_level = try values.decode(Int.self, forKey: .access_level)
}
catch {
print( "No value associated with key access_level ")
}
}
/////////////////Inner Struct///////////////////////
struct Employee: Codable {
var userId:Int=0
var nameFirst:String=""
var nameLast:String=""
var position:String=""
var company:String=""
var supervisor:String=""
var assistant:String=""
var phone:String=""
var email:String=""
var address:String=""
var gender:String=""
var age:Int=0
var nationality:String=""
var firstLanguage:String=""
var inFieldOfView:String = "0"
var photo:String="user-default"
var status:String="3"
}
////////////Following is the JSON//////////////////////
{
"response_status":1
,"access_level":2
,"hr_employees":[
{
"user_id":4226
,"name_last":"Sampe"
,"name_first":"Frederica"
,"position":"Systems Maint"
,"phone":"123456"
,"email":"omega#demo.mobile"
,"address":"00100 Helsinki 1"
,"age":67
,"company":"Omega Enterprise"
}
,{
"user_id":5656
,"name_last":"Aalto"
,"name_first":"Antero"
,"position":"Programming Methodology and Languages Researcher"
,"supervisor":"Mayo Georgia"
,"phone":"123456"
,"email":"omega#demo.mobile"
,"address":"00100 Finland "
,"age":51
,"company":"Omega Fire Related Equipment"
}
]
}
One problem is that what is in the JSON does not match your definition of Employee. For example nameFirst is not present and name_first is.
Another is that you have a custom implementation of init(from:), and it never fetches the hr_employees value!
Quite a few things for you to improve on:
Your Structs can be improved to harness automation capability of the Codable protocol.
You need to understand why you're using a CodingKeys enum
and in your case... also where best to have it (hint: inside the Struct itself)
You need to know which parameters need to be optional and why
this depends on your json structure ofcourse
If the parameters are to have a default value then there's a whole different process you need to follow; like having your own init(from:Decoder)
which you have to a certain extent but doesn't really handle everything in it's current state
Based on your given JSON example, you can simply do the following.
However... do note that this is not designed to provide default values. i.e. If a key is missing in the json, like status for example, then the parameter status in your Employee struct will be nil rather than a default value of "3".
struct EmployeeData: Codable {
var responseStatus: Int
var accessLevel: Int
/*
sessionToken is optional because as per your JSON
it seems it not always available
*/
var sessionToken: String?
var hrEmployees: [Employee]
/*
CodingKeys is inside the struct
It's used if the JSON key names are different than
the ones you plan to use.
i.e. JSON has keys in snake_case but we want camelCase
*/
enum CodingKeys: String, CodingKey {
case responseStatus = "response_status"
case accessLevel = "access_level"
case sessionToken = "session_token"
case hrEmployees = "hr_employees"
}
}
struct Employee: Codable {
var userId: Int
var nameFirst: String
var nameLast: String
var position: String
var company: String
var supervisor: String?
var assistant: String?
var phone: String
var email: String
var address: String
var gender: String?
var age: Int
var nationality: String?
var firstLanguage: String?
var inFieldOfView: String?
var photo: String?
var status: String?
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case nameFirst = "name_first"
case nameLast = "name_last"
case firstLanguage = "first_language"
case inFieldOfView = "in_field_of_view"
/*
Keys names that are same in json as well as in your
model need not have a raw string value
but must be defined if it's to be encoded/decoded
from the json else it can be omitted and a default
value will be required which won't affect the encoding
or decoding process
*/
case position
case company
case supervisor
case assistant
case phone
case email
case address
case gender
case age
case nationality
case photo
case status
}
}
Check:
do {
let employeeData = try JSONDecoder().decode(EmployeeData.self,
from: jsonAsData)
print(employeeData)
}
catch {
/*
If it comes here then debug, it's most probably nil keys
meaning you need more optional parameters in your struct
*/
print(error)
}
If you want default values in your Struct and the above example is a dealbreaker for you then check the following answer:
https://stackoverflow.com/a/44575580/2857130

RealmSwift initializer: self used before super.init call

import RealmSwift
import Realm
public class Card : Object {
var username : String
var firstName : String
var lastName : String
init?(dictionary: [String:Any]?) {
guard let dictionary = dictionary , let username = dictionary["username"] as? String else { return else}
self.username = username
self.firstName = firstName
self.lastName = lastName
}
required public init() {
fatalError("init() has not been implemented")
}
required public init( realm: RLMRealm, schema: RLMObjectSchema) {
fatalError("init(realm:schema:) has not been implemented")
}
required public init( value: Any, schema: RLMSchema) {
fatalError("init(value:schema:) has not been implemented")
}
}
I get:
'self' used before super.init call
I had my class working properly. After adding RealmSwift i'm getting those errors. If I add super.init() it complains:
Property 'self.username' not initialized at super.init call
Because your properties are String.
From the apple docs...
Setting Initial Values for Stored Properties
Classes and structures must set all of their stored properties to an appropriate initial value by the time an instance of that class or structure is created. Stored properties cannot be left in an indeterminate state.
You can set an initial value for a stored property within an initializer, or by assigning a default property value as part of the property’s definition. These actions are described in the following sections.
You have two options:
1)
var username : String = ""
var firstName : String = ""
var lastName : String = ""
2)
var username : String?
var firstName : String?
var lastName : String?
There's a couple of things going on here.
First and foremost, when adding custom initializers to subclasses of Object, they must be declared as convenience initializers. It's not possible to correctly implement Object's required initializer from a subclass, and using a convenience initializer removes the need to try and do this. It also means you'll instead delegate to self.init() rather than super.init() from within your custom initializer.
Secondly, stored properties must have an initial value. Without the initial value the Swift compiler will not synthesize initializers for your class (in this case, a version of init() that calls through to the base class).
Finally, as I mentioned elsewhere, properties of type String must be declared using Swift's dynamic modifier to allow Realm to intercept get / set operations on them.
By following these guidelines you'll end up with something like so:
public class Card : Object {
dynamic var username: String = ""
dynamic var firstName: String = ""
dynamic var lastName: String = ""
convenience init?(dictionary: [String:Any]?) {
guard let dictionary = dictionary,
let username = dictionary["username"] as? String,
let firstName = dictionary["firstName"] as? String,
let lastName = dictionary["lastName"] as? String
else { return nil }
self.init()
self.username = username
self.firstName = firstName
self.lastName = lastName
}
}

Resources