Generic Function: Type of expression is ambiguous without more context - swift

I have a static function in a class that takes a generic type that must conform to decodable, however when I call this function I get the following error: "Type of expression is ambiguous without more context". The Tour class (which is the type I'm passing to the function) conforms to Decodable and inherits from the CoreDataModel class.
This is occurring on the new Xcode 12.0 Beta in DashboardNetworkAdapter when I call CoreDataModel.create (line 7 on the snippet I've shared for that class).
Edit minimal reproducible example:
DashboardNetworkAdapter:
class DashboardNetworkAdapter {
// MARK: - GET
public func syncTours(page: Int, completion: #escaping(Bool, Error, Bool) -> Void) {
if let path = Bundle.main.path(forResource: "Tours", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
CoreDataModel.create(Array<Tour>.self, from: data) { success, error in
completion(success, error, false)
}
} catch let error {
completion(false, error, false)
}
}
}
}
CoreDataModel:
public class CoreDataModel: NSManagedObject {
// MARK: - Variables
#NSManaged public var id: Int64
#NSManaged public var createdAt: Date?
#NSManaged public var updatedAt: Date?
// MARK: - CRUD
static func create<T>(_ type: T.Type, from data: Data, completion: #escaping (Bool, Error?) -> Void) where T : Decodable {
DataCoordinator.performBackgroundTask { context in
do {
let _ = try DataDecoder(context: context).decode(type, from: data, completion: {
completion(true, nil)
})
} catch let error {
completion(false, error)
}
}
}
}
Tour:
#objc(Tour)
class Tour: CoreDataModel, Decodable {
// MARK: - Variables
#NSManaged public var name: String?
#NSManaged public var image: URL?
#NSManaged public var owned: Bool
#NSManaged public var price: Double
// MARK: - Coding Keys
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case image = "image"
case owned = "owned"
case price = "price"
case updatedAt = "updated_at"
case createdAt = "created_at"
}
// MARK: - Initializer
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("Cannot find decoding context")}
self.init(context: context)
let topLevel = try decoder.container(keyedBy: PaginatedResponseCodingKeys.self)
let values = try topLevel.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
let id = try values.decode(Int64.self, forKey: .id)
let name = try values.decode(String.self, forKey: .name)
let image = try values.decode(String.self, forKey: .image)
let owned = try values.decode(Int.self, forKey: .owned)
let price = try values.decode(Double.self, forKey: .price)
let updatedAt = try values.decode(Date.self, forKey: .updatedAt)
let createdAt = try values.decode(Date.self, forKey: .createdAt)
self.id = id
self.name = name
self.image = URL(string: image)
self.owned = owned == 1
self.price = price
self.updatedAt = updatedAt
self.createdAt = createdAt
}
init(context: NSManagedObjectContext) {
guard let entity = NSEntityDescription.entity(forEntityName: "Tour", in: context) else { fatalError() }
super.init(entity: entity, insertInto: context)
}
}
DataDecoder:
class DataDecoder: JSONDecoder {
// MARK: - Variables
var context: NSManagedObjectContext!
private var persistent: Bool = true
// MARK: - Initializers
public init(persistent: Bool = true, context: NSManagedObjectContext) {
super.init()
self.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
let dateFormatter = DateFormatter()
locale = .autoupdatingCurrent
dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.date(from: string)!
})
self.context = context
userInfo[.context] = context
}
// MARK: - Utilities
public func decode<T>(_ type: T.Type, from data: Data, completion: (() -> Void)? = nil) throws -> T where T : Decodable {
let result = try super.decode(type, from: data)
if(persistent) {
saveContext(completion: completion)
}
return result
}
private func saveContext(completion: (() -> Void)? = nil) {
guard let context = context else { fatalError("Cannot Find Decoding Context") }
context.performAndWait {
try? context.save()
completion?()
context.reset()
}
}
}
CodingUserInfoKey Extension:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")!
}

There's a type mismatch between what completion closure expects for its Error parameter, and what it gets from the inferred type of the CoreDataModel.create completion closure, which is Error?:
public func syncTours(page: Int, completion: #escaping(Bool, Error, Bool) -> Void) {
...
CoreDataModel.create(Array<Tour>.self, from: data) { success, error in
// error is of type Error?, but completion expects Error
completion(success, error, false)
}
...
}
In general, whenever you face type inference issues or errors, make each type explicit, and you'll see exactly exactly where the mismatch is. For example, below you could be explicit about the inner closure signature:
CoreDataModel.create([Tour].self, from: data) { (success: Bool, err: Error?) -> Void in
}

Related

Creating a expiry date for UserDefaults values

So I had this idea of creating an expiry for UserDefaults. This is the approach I was starting to take but I'm stuck.
struct TimedObject<T: Codable>: Codable {
let object: T
let expireDate: Date
}
and then:
extension UserDefaults {
func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
let timedObject = TimedObject<T>(object: value, expireDate: expireDate)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(timedObject) {
UserDefaults.standard.set(encoded, forKey: key)
}
override open class func value(forKey key: String) -> Any? {
guard let value = self.value(forKey: key) else {
return nil
}
if TimedObject<???>.self == type(of: value) { // This is where I'm stuck
...
}
}
So if I would name the type and not use generics I would easily solve this. But naturally I prefer to use generics. Can this be done?
I know OP is using a struct to wrap the stored value but I would still like to offer a different protocol based solution where any type that should be stored with an expiration date needs to conform to this protocol.
Here is the protocol for I am using
protocol TimedObject: Codable {
associatedtype Value: Codable
var value: Value { get }
var expirationDate: Date { get }
}
and the functions to store and retrieve from UserDefaults
extension UserDefaults {
func set<Timed: TimedObject>(_ value: Timed, forKey key: String) {
if let encoded = try? JSONEncoder().encode(value) {
self.set(encoded, forKey: key)
}
}
func value<Timed: TimedObject>(_ type: Timed.Type, forKey key: String) -> Timed.Value? {
guard let data = self.value(forKey: key) as? Data, let object = try? JSONDecoder().decode(Timed.self, from: data) else {
return nil
}
return object.expirationDate > .now ? object.value : nil
}
}
Finally an example
struct MyStruct: Codable {
let id: Int
let name: String
}
extension MyStruct: TimedObject {
typealias Value = Self
var value: MyStruct { self }
var expirationDate: Date {
.now.addingTimeInterval(24 * 60 * 60)
}
}
let my = MyStruct(id: 12, name: "abc")
UserDefaults.standard.set(my, forKey: "my")
let my2 = UserDefaults.standard.value(MyStruct.self, forKey: "my")
Since you're returning Any?, it is best to create another struct to point to TimedObject as you don't need the object property when returning Any?:
struct Expiry: Codable {
var expireDate: Date
}
struct TimedObject<T: Codable>: Timable {
let object: T
var expireDate: Date
}
override open class func value(forKey key: String) -> Any? {
guard let value = self.value(forKey: key) as? Data else {
return nil
}
if let timed = try? JSONDecoder().decode(Expiry.self, from: value) {
//do anything with timed
}
//make sure to return value
}
And here's a method to access TimedObject:
func timedObject<T: Codable>(forKey key: String) -> TimedObject<T>? {
guard let value = self.data(forKey: key) as? Data, let timed = try? JSONDecoder().decode(TimedObject<T>.self, from: value) else {
return nil
}
return value
}
If you make the value() method generic then you can reverse the process done in the set() method: retrieve the data and decode it as a TimedObject<T>.
However, I would choose a different name to avoid possible ambiguities with the exisiting value(forKey:) method. Also I see no reason why this should be a class method.
Note also that your generic set() method should call the non-generic version on the same instance.
extension UserDefaults {
func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
let timedObject = TimedObject(object: value, expireDate: expireDate)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(timedObject) {
set(encoded, forKey: key)
}
}
func expiringValue<T: Codable>(forKey key: String) -> T? {
guard let data = self.data(forKey: key) else {
return nil
}
let decoder = JSONDecoder()
guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
return nil
}
// check expire date ...
return decoded.object
}
}
Example usage:
let val1 = UserDefaults.standard.expiringValue(forKey: "foo") as String?
let val2: String? = UserDefaults.standard.expiringValue(forKey: "bar")
In both cases, expiringValue(forKey:) is called with the inferred type.
Or in combination with optional binding:
if let val: String = UserDefaults.standard.expiringValue(forKey: "test") {
print(val)
}
Another option is to pass the desired type as an additional argument:
func value<T: Codable>(forKey key: String, as: T.Type) -> T? {
guard let data = self.data(forKey: key) else {
return nil
}
let decoder = JSONDecoder()
guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
return nil
}
// check expire date ...
return decoded.object
}
which is then used as
let val = UserDefaults.standard.value(forKey: "foo", as: String.self)

Decoding JSON in Swift to Dictionary with different types

I have a JSON result from the server that looks like the following:
let json = """
{
"type": "rating",
"data": {
"maxRating": 5,
"isDarkMode": true
}
}
"""
The value for data can be any key-values. I want to map this JSON to my Swift model. So, I implemented the following:
struct Model: Decodable {
let type: String
let data: [String: Any]
private enum CodingKeys: String, CodingKey {
case type
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
self.data = try container.decode([String: Any].self, forKey: .data) // THIS
}
}
But on self.data = try container.decode ... it gives me the following error:
error: no exact matches in call to instance method 'decode'
self.data = try container.decode([String: Any].self, forKey: .data)
How can I fix it?
[String : Any] is not decodable. You can not decode with Any. But there have other solutions. Here is one https://dev.to/absoftware/how-to-make-swift-s-string-any-decodable-5c6n
Make change in the Metadata as -
enum DataType: String, Codable {
case payload
case metadata
}
struct Payload: Codable {
let id: String
let eventName: String
let metadata: [Metadata]
}
struct Metadata: Codable {
let maxRating: Int?
let isDarkMode: Bool?
// Add other variables that may appear from your JSON
}
enum MyValue: Decodable {
case payload(_ payload: Payload)
case metadata(_ metadata: Metadata)
private enum CodingKeys: String, CodingKey {
case `type`
case `data`
}
init(from decoder: Decoder) throws {
let map = try decoder.container(keyedBy: CodingKeys.self)
let dataType = try map.decode(DataType.self, forKey: .type)
switch dataType {
case .payload:
self = .payload(try map.decode(Payload.self, forKey: .data))
case .metadata:
self = .metadata(try map.decode(Metadata.self, forKey: .data))
}
}
}
Use the following Extension
JSONCodingKeys.swift
import Foundation
// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a
struct JSONCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
How to use.
struct Model: Decodable {
let type: String
let data: [String: Any]
private enum CodingKeys: String, CodingKey {
case type
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
self.data = try container.decode([String: Any].self, forKey: .data)
}
}
let decoder = JSONDecoder()
let userJson = """
{
"type": "rating",
"data": {
"maxRating": 5,
"isDarkMode": true
}
}
""".data(using: .utf8)!
let user = try! decoder.decode(Model.self, from: userJson)
print(user)
Original answer from
You have to create different struct for data parameter.
You can refer below struct for reference
import Foundation
struct JsonResponse : Codable {
let type : String?
let data : Data?
enum CodingKeys: String, CodingKey {
case type = "type"
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decodeIfPresent(String.self, forKey: .type)
data = try values.decodeIfPresent(Data.self, forKey: .data)
}
}
struct Data : Codable {
let maxRating : Int?
let isDarkMode : Bool?
enum CodingKeys: String, CodingKey {
case maxRating = "maxRating"
case isDarkMode = "isDarkMode"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
maxRating = try values.decodeIfPresent(Int.self, forKey: .maxRating)
isDarkMode = try values.decodeIfPresent(Bool.self, forKey: .isDarkMode)
}
}

Skipping empty string with JSONEncoders swift

I a codable serialization extension which I use to turn my Codable struct to dictionaries, the problem I am facing is strings. I get string value from my UITextField at at times this value could be empty and as a result an empty string is decoded. How can I return nil if the value is an empty string.
extension Encodable {
var requestDictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
guard let data = try? encoder.encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
if I have a Struct
let example = Exa(age: 10, name: "")
let dict = example.requestDictionary
print(dict)
I want it to just print ["age": 10] and return nil for the empty string
You can implement your own String encoding method extending KeyedEncodingContainer:
extension KeyedEncodingContainer {
mutating func encode(_ value: String, forKey key: K) throws {
guard !value.isEmpty else { return }
try encodeIfPresent(value, forKey: key)
}
}
Btw your request dictionary can be simplified as:
extension Encodable {
var dictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
return try? JSONSerialization.jsonObject(with: encoder.encode(self)) as? [String: Any]
}
}
Playground testing:
struct Exa: Encodable {
let age: Int
let name: String
}
let example = Exa(age: 10, name: "")
let dict = example.dictionary!
print(dict) // "["age": 10]\n"
I'll just another approach using a property wrapper to mark which properties could be skipped.
#propertyWrapper
struct SkipEmpty {
var wrappedValue: String
}
extension SkipEmpty: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.wrappedValue = try container.decode(String.self)
}
func encode(to encoder: Encoder) throws {
// nothing to do here, see below
}
}
But to actually skip, you'd also need to create a overload for the KeyedEncodingContainer.encode method for the SkipEmpty type:
extension KeyedEncodingContainer {
mutating func encode(_ value: SkipEmpty, forKey key: K) throws {
if !value.wrappedValue.isEmpty {
try encode(value.wrappedValue, forKey: key) // encode the value here
}
}
}
You could possibly try to make it more generic, e.g. SkipEmpty<T: Codable> and provide another argument for the value to skip or a predicate, etc...
The usage is:
struct Exa: Encodable {
var age: Int
#SkipEmpty var name: String
}

Get 'NSInvalidArgumentException' 'Attempt to insert non-property list object when saving class type array to UserDefaults [duplicate]

I have a simple object which conforms to the NSCoding protocol.
import Foundation
class JobCategory: NSObject, NSCoding {
var id: Int
var name: String
var URLString: String
init(id: Int, name: String, URLString: String) {
self.id = id
self.name = name
self.URLString = URLString
}
// MARK: - NSCoding
required init(coder aDecoder: NSCoder) {
id = aDecoder.decodeObject(forKey: "id") as? Int ?? aDecoder.decodeInteger(forKey: "id")
name = aDecoder.decodeObject(forKey: "name") as! String
URLString = aDecoder.decodeObject(forKey: "URLString") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(name, forKey: "name")
aCoder.encode(URLString, forKey: "URLString")
}
}
I'm trying to save an instance of it in UserDefaults but it keeps failing with the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key jobCategory'
This is the code where I'm saving in UserDefaults.
enum UserDefaultsKeys: String {
case jobCategory
}
class ViewController: UIViewController {
#IBAction func didTapSaveButton(_ sender: UIButton) {
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let userDefaults = UserDefaults.standard
userDefaults.set(category, forKey: UserDefaultsKeys.jobCategory.rawValue)
userDefaults.synchronize()
}
}
I replaced the enum value to key with a normal string but the same error still occurs. Any idea what's causing this?
You need to create Data instance from your JobCategory model using JSONEncoder and store that Data instance in UserDefaults and later decode using JSONDecoder.
struct JobCategory: Codable {
let id: Int
let name: String
}
// To store in UserDefaults
if let encoded = try? JSONEncoder().encode(category) {
UserDefaults.standard.set(encoded, forKey: UserDefaultsKeys.jobCategory.rawValue)
}
// Retrieve from UserDefaults
if let data = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as? Data,
let category = try? JSONDecoder().decode(JobCategory.self, from: data) {
print(category.name)
}
Old Answer
You need to create Data instance from your JobCategory instance using archivedData(withRootObject:) and store that Data instance in UserDefaults and later unarchive using unarchiveTopLevelObjectWithData(_:), So try like this.
For Storing data in UserDefaults
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: category, requiringSecureCoding: false)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedData, forKey: UserDefaultsKeys.jobCategory.rawValue)
For retrieving data from UserDefaults
let decoded = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as! Data
let decodedTeams = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! JobCategory
print(decodedTeams.name)
Update Swift 4, Xcode 10
I have written a struct around it for easy access.
//set, get & remove User own profile in cache
struct UserProfileCache {
static let key = "userProfileCache"
static func save(_ value: Profile!) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
}
static func get() -> Profile! {
var userData: Profile!
if let data = UserDefaults.standard.value(forKey: key) as? Data {
userData = try? PropertyListDecoder().decode(Profile.self, from: data)
return userData!
} else {
return userData
}
}
static func remove() {
UserDefaults.standard.removeObject(forKey: key)
}
}
Profile is a Json encoded object.
struct Profile: Codable {
let id: Int!
let firstName: String
let dob: String!
}
Usage:
//save details in user defaults...
UserProfileCache.save(profileDetails)
Hope that helps!!!
Thanks
Swift save Codable object to UserDefault with #propertyWrapper
#propertyWrapper
struct UserDefault<T: Codable> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data,
let user = try? JSONDecoder().decode(T.self, from: data) {
return user
}
return defaultValue
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
enum GlobalSettings {
#UserDefault("user", defaultValue: User(name:"",pass:"")) static var user: User
}
Example User model confirm Codable
struct User:Codable {
let name:String
let pass:String
}
How to use it
//Set value
GlobalSettings.user = User(name: "Ahmed", pass: "Ahmed")
//GetValue
print(GlobalSettings.user)
Save dictionary Into userdefault
let data = NSKeyedArchiver.archivedData(withRootObject: DictionaryData)
UserDefaults.standard.set(data, forKey: kUserData)
Retrieving the dictionary
let outData = UserDefaults.standard.data(forKey: kUserData)
let dict = NSKeyedUnarchiver.unarchiveObject(with: outData!) as! NSDictionary
Based on Harjot Singh answer. I've used like this:
struct AppData {
static var myObject: MyObject? {
get {
if UserDefaults.standard.object(forKey: "UserLocationKey") != nil {
if let data = UserDefaults.standard.value(forKey: "UserLocationKey") as? Data {
let myObject = try? PropertyListDecoder().decode(MyObject.self, from: data)
return myObject!
}
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "UserLocationKey")
}
}
}
Here's a UserDefaults extension to set and get a Codable object, and keep it human-readable in the plist (User Defaults) if you open it as a plain text file:
extension Encodable {
var asDictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return try? JSONSerialization.jsonObject(with: data) as? [String : Any]
}
}
extension Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil }
guard let object = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = object
}
}
extension UserDefaults {
func setEncodableAsDictionary<T: Encodable>(_ encodable: T, for key: String) {
self.set(encodable.asDictionary, forKey: key)
}
func getDecodableFromDictionary<T: Decodable>(for key: String) -> T? {
guard let dictionary = self.dictionary(forKey: key) else {
return nil
}
return T(dictionary: dictionary)
}
}
If you want to also support array (of codables) to and from plist array, add the following to the extension:
extension UserDefaults {
func setEncodablesAsArrayOfDictionaries<T: Encodable>(_ encodables: Array<T>, for key: String) {
let arrayOfDictionaries = encodables.map({ $0.asDictionary })
self.set(arrayOfDictionaries, forKey: key)
}
func getDecodablesFromArrayOfDictionaries<T: Decodable>(for key: String) -> [T]? {
guard let arrayOfDictionaries = self.array(forKey: key) as? [[String: Any]] else {
return nil
}
return arrayOfDictionaries.compactMap({ T(dictionary: $0) })
}
}
If you don't care about plist being human-readable, it can be simply saved as Data (will look like random string if opened as plain text):
extension UserDefaults {
func setEncodable<T: Encodable>(_ encodable: T, for key: String) throws {
let data = try PropertyListEncoder().encode(encodable)
self.set(data, forKey: key)
}
func getDecodable<T: Decodable>(for key: String) -> T? {
guard
self.object(forKey: key) != nil,
let data = self.value(forKey: key) as? Data
else {
return nil
}
let obj = try? PropertyListDecoder().decode(T.self, from: data)
return obj
}
}
(With this second approach, you don't need the Encodable and Decodable extensions from the top)

Swift can't infer generic type when generic type is being passed through a parameter

I'm writing a generic wrapper class for core data.
Here are some of my basic types. Nothing special.
typealias CoreDataSuccessLoad = (_: NSManagedObject) -> Void
typealias CoreDataFailureLoad = (_: CoreDataResponseError?) -> Void
typealias ID = String
enum CoreDataResult<Value> {
case success(Value)
case failure(Error)
}
enum CoreDataResponseError : Error {
typealias Minute = Int
typealias Key = String
case idDoesNotExist
case keyDoesNotExist(key: Key)
case fetch(entityName: String)
}
I've abstracted my coredata writes in a protocol. I'd appreciate if you let me know of your comments about the abstraction I'm trying to pull off.
Yet in the extension I run into the following error:
Cannot convert value of type 'NSFetchRequest' to
expected argument type 'NSFetchRequest<_>'
Not sure exactly how I can fix it. I've tried variations of changing my code but didn't find success...
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject
var persistentContainer : NSPersistentContainer {get}
var idName : String {get}
func loadFromDB(storableClass : ManagedObject.Type, id: ID) throws -> CoreDataResult<ManagedObject>
func update(storableClass : ManagedObject.Type, id: ID, fields: [String : Any]) throws
func fetch(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext)
init(persistentContainer : NSPersistentContainer)
}
extension CoreDataWriteManagerProtocol {
private func loadFromDB(storableClass : ManagedObject.Type, id: ID) -> CoreDataResult<ManagedObject>{
let predicate = NSPredicate(format: "%# == %#", idName, id)
let fetchRequest : NSFetchRequest = storableClass.fetchRequest()
fetchRequest.predicate = predicate
// ERROR at below line!
return fetch(request: fetchRequest, from: persistentContainer.viewContext)
}
func fetch<ManagedObject: NSManagedObject>(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject>{
guard let results = try? context.fetch(request) else {
return .failure(CoreDataResponseError.fetch(entityName: request.entityName ?? "Empty Entity Name")) // #TODO not sure if entityName gets passed or not.
}
if let result = results.first {
return .success(result)
}else{
return .failure(CoreDataResponseError.idDoesNotExist)
}
}
}
Additionally if I change the line:
let fetchRequest : NSFetchRequest = storableClass.fetchRequest()
to:
let fetchRequest : NSFetchRequest<storableClass> = storableClass.fetchRequest()
I get the following error:
Use of undeclared type 'storableClass'`
My intuition tells me that the compiler can't map 'parameters that are types' ie it doesn't understand that storableClass is actually a type. Instead it can only map generics parameters or actual types. Hence this doesn't work.
EDIT:
I used static approach Vadian and wrote this:
private func create(_ entityName: String, json : [String : Any]) throws -> ManagedObject {
guard let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: Self.persistentContainer.viewContext) else {
print("entityName: \(entityName) doesn't exist!")
throw CoreDataError.entityNotDeclared(name: entityName)
}
let _ = entityDescription.relationships(forDestination: NSEntityDescription.entity(forEntityName: "CountryEntity", in: Self.persistentContainer.viewContext)!)
let relationshipsByName = entityDescription.relationshipsByName
let propertiesByName = entityDescription.propertiesByName
guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
throw CoreDataError.entityNotDeclared(name: entityName)
}
for (propertyName,_) in propertiesByName {
if let value = json[propertyName] {
managedObj.setValue(value, forKey: propertyName)
}
}
// set all the relationships
guard !relationshipsByName.isEmpty else {
return managedObj
}
for (relationshipName, _ ) in relationshipsByName {
if let object = json[relationshipName], let objectDict = object as? [String : Any] {
let entity = try create(relationshipName, json: objectDict)
managedObj.setValue(entity, forKey: relationshipName)
}
}
return managedObj
}
But the following piece of it is not generic as in I'm casting it with as? ManagedObject. Basically it's not Swifty as Vadian puts it:
guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
throw CoreDataError.entityNotDeclared(name: entityName)
}
Is there any way around that?
My suggestion is a bit different. It uses static methods
Call loadFromDB and fetch on the NSManagedObject subclass. The benefit is that always the associated type is returned without any further type cast.
Another change is throwing errors. As the Core Data API relies widely on throwing errors my suggestion is to drop CoreDataResult<Value>. All errors are passed through. On success the object is returned, on failure an error is thrown.
I left out the id related code and the update method. You can add a static func predicate(for id : ID)
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject = Self
static var persistentContainer : NSPersistentContainer { get }
static var entityName : String { get }
static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject
static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject
static func insertNewObject() -> ManagedObject
}
extension CoreDataWriteManagerProtocol where Self : NSManagedObject {
static var persistentContainer : NSPersistentContainer {
return (UIApplication.delegate as! AppDelegate).persistentContainer
}
static var entityName : String {
return String(describing:self)
}
static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject {
let request = NSFetchRequest<ManagedObject>(entityName: entityName)
request.predicate = predicate
return try fetch(request: request)
}
static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject {
guard let results = try? persistentContainer.viewContext.fetch(request) else {
throw CoreDataResponseError.fetch(entityName: entityName)
}
if let result = results.first {
return result
} else {
throw CoreDataResponseError.idDoesNotExist
}
}
static func insertNewObject() -> ManagedObject {
return NSEntityDescription.insertNewObject(forEntityName: entityName, into: persistentContainer.viewContext) as! ManagedObject
}
}
The issue is that NSManagedObject.fetchRequest() has a return type of NSFetchRequest<NSFetchRequestResult>, which is non-generic. You need to update the definition of your fetch function to account for this. Btw the function signatures of the default implementations in the protocol extension didn't actually match the function signatures in the protocol definition, so those also need to be updated.
You also need to change the implementation of fetch(request:,from:), since NSManagedObjectContext.fetch() returns a value of type [Any], so you need to cast that to [ManagedObject] to match the type signature of your own fetch method.
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject
var persistentContainer : NSPersistentContainer {get}
var idName : String {get}
func loadFromDB(storableClass : ManagedObject.Type, id: ID) throws -> CoreDataResult<ManagedObject>
func update(storableClass : ManagedObject.Type, id: ID, fields: [String : Any]) throws
func fetch(request: NSFetchRequest<NSFetchRequestResult>, from: NSManagedObjectContext) -> CoreDataResult<ManagedObject>
init(persistentContainer : NSPersistentContainer)
}
extension CoreDataWriteManagerProtocol {
private func loadFromDB(storableClass : ManagedObject.Type, id: ID) -> CoreDataResult<ManagedObject>{
let predicate = NSPredicate(format: "%# == %#", idName, id)
let fetchRequest = storableClass.fetchRequest()
fetchRequest.predicate = predicate
return fetch(request: fetchRequest, from: persistentContainer.viewContext)
}
func fetch(request: NSFetchRequest<NSFetchRequestResult>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject> {
guard let results = (try? context.fetch(request)) as? [ManagedObject] else {
return .failure(CoreDataResponseError.fetch(entityName: request.entityName ?? "Empty Entity Name")) // #TODO not sure if entityName gets passed or not.
}
if let result = results.first {
return .success(result)
}else{
return .failure(CoreDataResponseError.idDoesNotExist)
}
}
}