I am trying to implement a cache using CoreData.
Up until this point I've been storing models that are simple, however I have a model below that contains data types such as CodablePartialUser and CodableFeedItemType.
How should these types be modelled in CoreData?
Should I use the Data type and store them in a data format?
As CodableFeedItemType is an enum, should I store the raw value and convert between formats again?
struct CodablePartialUser: Equatable, Codable {
let userID: String
let firstName: String
let lastName: String
init(userID: String, firstName: String, lastName: String) {
self.userID = userID
self.firstName = firstName
self.lastName = lastName
}
}
enum CodableFeedItemType: String, Codable {
case recognition = "RECOGNITION"
case news = "COMPANY_NEWS"
}
struct CodableFeedItem: Codable {
let id: String
let type: CodableFeedItemType
let createdDate: Date
let createdBy: CodablePartialUser
let likesCount: Int
let commentsCount: Int
let externalID: String
let title: String?
let imageURL: URL?
init(id: String, type: CodableFeedItemType, createdDate: Date, createdBy: CodablePartialUser, likesCount: Int, commentsCount: Int, externalID: String, title: String?, imageURL: URL?) {
self.id = id
self.type = type
self.createdDate = createdDate
self.createdBy = createdBy
self.likesCount = likesCount
self.commentsCount = commentsCount
self.externalID = externalID
self.title = title
self.imageURL = imageURL
}
}
For the CodablePartialUser you can use relationship by creating a new Entity named as "CodablePartialUser"
For CodableFeedItemType you can use enum as like
enum CodableFeedItemType: String, Codable {
case recognition = "RECOGNITION"
case news = "COMPANY_NEWS"
}
extension CodableFeedItemEntity {
var type: CodableFeedItemType {
get {
return CodableFeedItemType(rawValue: typeRaw)!
}
set {
typeRaw = newValue.rawValue
}
}
}
Related
class Model {
let userId: Int
let username: String
private let description: String?
init(userId: Int, username: String, description: String?) {
self.userId = userId
self.username = username
self.description = description
}
}
class ModelDetail: Model {
let url: String
let detail: String
init(url: String, detail: String, userId: Int, username: String, description: String?) {
self.url = url
self.detail = detail
super.init(userId: userId, username: username, description: description)
}
}
I prepared this sample code to copy here. I don't want to inherit "description" value from Model class. When I try to something I am getting this error: Missing argument for parameter 'description' in call
Is it possible not to inherit some variable?
You cannot not inherit description, but you can assign a value nil to it, and simply ignore it.
If you can modify Model, you can change its init to
class Model {
// ...
init(userId: Int, username: String, description: String? = nil)
// ...
}
Since description is optional, we set it to nil by default, so ModelDetail doesn't need to set it at all:
class ModelDetail: Model {
let url: String
let detail: String
init(url: String, detail: String, userId: Int, username: String) {
self.url = url
self.detail = detail
super.init(userId: userId, username: username)
}
}
If you cannot change Model, then ModelDetail can invoke Model's init with nil for description:
class ModelDetail: Model {
// ...
init(url: String, detail: String, userId: Int, username: String) {
// ...
super.init(userId: userId, username: username, description: nil)
}
}
If you want to strictly adhere to the inheritance rules (which do not allow for "unused" fields), then you need to separate description from the model ModelDetail will be inheriting from. I.e.:
Have a base model, which only has userId and username
Both Model and ModelDetail will inherit from that base model, rather than each other:
class BasicModel {
let userId: Int
let username: String
init(userId: Int, username: String) {
self.userId = userId
self.username = username
}
}
class Model: BasicModel {
private let description: String?
init(userId: Int, username: String, description: String?) {
self.description = description
super.init(userId: userId, username: username)
}
}
class ModelDetail: BasicModel {
let url: String
let detail: String
init(url: String, detail: String, userId: Int, username: String) {
self.url = url
self.detail = detail
super.init(userId: userId, username: username)
}
}
Say I have a struct User model which has many properties in it.
struct User: Codable {
let firstName: String
let lastName: String
// many more properties...
}
As you can see above it conforms to Codable. Imagine if the lastName property is should be encoded/decoded as secondName and I would like to keep it as lastName at my end, I need to add the CodingKeys to the User model.
struct User: Codable {
//...
private enum CodingKeys: String, CodingKey {
case firstName
case lastName = "secondName"
// all the other cases...
}
}
Is there any possible way to avoid including all the cases in CodingKeys that have the same value as rawValue like the firstName in the above example (Feels redundant)? I know if I avoid the cases in CodingKeys it won't be included while decoding/encoding. But, is there a way I could override this behaviour?
There is a codable way, but the benefit is questionable.
Create a generic CodingKey
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }
init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue }
}
and add a custom keyDecodingStrategy
struct User: Codable {
let firstName: String
let lastName: String
let age : Int
}
let jsonString = """
{"firstName":"John", "secondName":"Doe", "age": 30}
"""
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ keyPath -> CodingKey in
let key = keyPath.last!
return key.stringValue == "secondName" ? AnyKey(stringValue:"lastName")! : key
})
let result = try decoder.decode(User.self, from: data)
print(result)
} catch {
print(error)
}
There is not such a feature at this time. But you can take advantage of using computed properties and make the original one private.
struct User: Codable {
var firstName: String
private var secondName: String
var lastName: String {
get { secondName }
set { secondName = newValue }
}
}
So no need to manual implementing of CodingKeys at all and it acts exactly like the way you like. Take a look at their counterparts:
How to convert string to Int while convert object in city_id ?
Error when received data from api and convert into object
typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "city_id", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))
class ObjUserInfo: Codable {
var id: Int
var firstName, lastName, email: String
var cityID: Int
var city, country: String
var countryID: Int
var countryFlag: String
var phone, birthDate: String
var gender: Int
var genderText: String
var profileImage, coverImage: String
var profileInfo: String
var accessInfo: AccessInfo
var userFavouriteSports: [ObjSportsList]
var languages: String
enum CodingKeys: String, CodingKey {
case id
case firstName = "first_name"
case lastName = "last_name"
case email
case cityID = "city_id"
case city, country
case countryID = "country_id"
case countryFlag = "country_flag"
case phone
case birthDate = "birth_date"
case gender
case genderText = "gender_text"
case profileImage = "profile_image"
case coverImage = "cover_image"
case profileInfo = "profile_info"
case accessInfo = "access_info"
case userFavouriteSports = "user_favourite_sports"
case languages
}
init(id: Int, firstName: String, lastName: String, email: String, cityID: Int, city: String, country: String, countryID: Int, countryFlag: String, phone: String, birthDate: String, gender: Int, genderText: String, profileImage: String, coverImage: String, profileInfo: String, accessInfo: AccessInfo, userFavouriteSports: [ObjSportsList], languages: String) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.email = email
self.cityID = cityID
self.city = city
self.country = country
self.countryID = countryID
self.countryFlag = countryFlag
self.phone = phone
self.birthDate = birthDate
self.gender = gender
self.genderText = genderText
self.profileImage = profileImage
self.coverImage = coverImage
self.profileInfo = profileInfo
self.accessInfo = accessInfo
self.userFavouriteSports = userFavouriteSports
self.languages = languages
}
}
How to convert string to Int while convert object in city_id ?
Code for convert object
let decoderdec = JSONDecoder()
//decoderdec.keyDecodingStrategy = .convertFromSnakeCase
// 2. Create Data from Response
let jsonData = try JSONSerialization.data(withJSONObject: jsonResponse["data"] as Any)
let objUser = try decoderdec.decode(ObjUserInfo.self, from: jsonData)
EDIT: Yes, you shouldn't force unwrap values in production code -- the previous code was simply for a demonstration purpose. Anyways, I editted the code regarding the comment by #Joakim Danielson above.
You should implement a custom init(from decoder: Decoder) throws method.
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
// Left hand side `id` is for the property, and `forKey: .id` is the case in
// your enum CodingKeys
id = try values.decode(Int.self, forKey: .id)
...
// First try decoding from String, and covert it to Int.
// If it wasn't String, try decoding from Int.
if let rawCityID = try? values.decode(String.self, forKey: .cityID),
let cityID = Int(rawCityID) {
self.cityID = cityID
} else {
cityID = try values.decode(Int.self, forKey: .cityID)
}
...
}
Any suggestion on how to simplify this data struct? The data will be saved as a dictionary on the user's drive and when I read the data from the drive I have to convert them back to Member for easy accessing the properties.
I would like to have it typesafe.
struct Member {
var id: Int
var firstname: String
var lastname: String
var address: String?
var zipCode: Int?
var city: String?
enum Value: String {
case id = "id"
case firstname = "firstname"
case lastname = "lastname"
case address = "address"
case zipCode = "zipCode"
case city = "city"
}
var member: [String:Any] {
return [
Value.id.rawValue:Int(),
Value.firstname.rawValue:firstname,
Value.lastname.rawValue:lastname,
Value.address.rawValue:address ?? String(),
Value.zipCode.rawValue:zipCode ?? Int(),
Value.city.rawValue:city ?? String()
]
}
}
func memberToDic(member: Member) -> [String:Any] {
return [
Member.Value.firstname.rawValue:member.firstname,
Member.Value.lastname.rawValue:member.lastname,
Member.Value.address.rawValue:member.address ?? String(),
Member.Value.zipCode.rawValue:member.zipCode ?? Int(),
Member.Value.city.rawValue:member.city ?? String()
]
}
func dicToMember(dic: [String:Any]) -> Member {
return Member(
id: dic[Member.Value.id.rawValue] as! Int,
firstname: dic[Member.Value.firstname.rawValue] as! String,
lastname: dic[Member.Value.lastname.rawValue] as! String,
address: dic[Member.Value.address.rawValue] as? String,
zipCode: dic[Member.Value.zipCode.rawValue] as? Int,
city: dic[Member.Value.city.rawValue] as? String
)
}
Almost certainly, this is the correct implementation:
struct Member: Codable {
var id: Int
var firstName: String // "first name" is two words, so capitalize "name"
var lastName: String
var address: String // "No address" should be empty, not nil
var zipCode: String // ZIP codes are not integers
var city: String // "No city" should be empty, not nil
}
In order to save this as a plist, use PropertyListEncoder:
let data = try PropertyListEncoder().encode(member)
To read it, use PropertyListDecoder.
Codable automatically creates key mappings for your properties, so there's no need for Value.
You should strongly avoid creating or consuming [String: Any] dictionaries. These exist mostly due to Objective-C interfaces that could not generate strong types.
If address, zipCode, and city all should be set together, or not set together, then you should collect them into a single struct:
struct Address: Codable {
var streetAddress: String
var zipCode: String
var city: String
}
struct Member: Codable {
var id: Int
var firstName: String // "first name" is two words, so capitalize "name"
var lastName: String
var address: Address?
}
In this case, and Optional makes sense because "empty" is not the same thing as "missing."
I have a custom APIClient using alamofire5 beta that conforms to Codable protocol for the request.
I'm trying to send a custom object via httpBody (post) and I'm getting this error:
Invalid type in JSON write (_SwiftValue)
This is the object that I'm trying to send:
struct Complex: Codable {
var id: String
var name: String
var address: String
var zipcode: String
var amenities: [String]
var schedules: [ComplexSchedules]
init(id: String, name: String, address: String, zipcode: String, amenities: [String], schedules: [ComplexSchedules]) {
self.id = id
self.name = name
self.address = address
self.zipcode = zipcode
self.amenities = amenities
self.schedules = schedules
}
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case address = "address"
case zipcode = "zipcode"
case amenities = "amenities"
case schedules = "schedules"
}
struct TimeRange: Codable {
var from: String
var to: String
init(from: String, to: String) {
self.from = from
self.to = to
}
enum CodingKeys: String, CodingKey {
case from = "from"
case to = "to"
}
}
struct ComplexSchedules: Codable {
var day: String
var timeRanges: [TimeRange]
init(day: String, timeRanges: [TimeRange]) {
self.day = day
self.timeRanges = timeRanges
}
enum CodingKeys: String, CodingKey {
case day = "day"
case timeRanges = "time_ranges"
}
}
}
It fails when I call this method:
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: complex, options: [])
Any thoughts?
You may need
do {
let data = try JSONEncoder().encode(complex)
urlRequest.httpBody = data
}
catch {
print(error)
}
as Codable is used to be able to utilize JSONDecoder and JSONEncoder not to use with JSONSerialization that expects a non-custom object like raw String/Array/Dictionary