Reputation: 36670
I know that there are several questions similar to this, that tend to all revolve around the class not conforming to the protocol properly, but that should not be the immediate issue here.
The following is a condensed version of the code that is currently giving me this problem:
enum Binary: Int {
case a = 0
case b = 1
case c = 9
}
final class MyClass: NSCoder {
var string: String?
var date: Date?
var binary: Binary = .c
override init() { }
enum CodingKeys: CodingKey {
case string, date, binary
}
}
extension MyClass: Codable {
convenience init(from decoder: Decoder) throws {
self.init()
let values = try decoder.container(keyedBy: CodingKeys.self)
string = try values.decode(String.self, forKey: .string)
date = try values.decode(Date.self, forKey: .date)
binary = try Binary(rawValue: values.decode(Int.self, forKey: .binary)) ?? .c
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(string, forKey: .string)
try container.encode(date, forKey: .date)
try container.encode(binary.rawValue, forKey: .binary)
}
}
I have created the following class which then attempts to call MyClass
with the purpose of writing & reading it to UserDefaults
:
class MyClassController {
private let myClass: MyClass
init() {
self.myClass = MyClass()
self.myClass.string = "string"
self.myClass.date = Date()
self.myClass.binary = .a
}
func writeMyClass() {
let encodedData = NSKeyedArchiver.archivedData(withRootObject: myClass)
UserDefaults.standard.set(encodedData, forKey: String(describing: MyClass.self))
}
func readMyClass() {
if let decoded = UserDefaults.standard.object(forKey: String(describing: MyClass.self)) as? Data,
let myClass = NSKeyedUnarchiver.unarchiveObject(with: decoded as Data) as? MyClass {
print("string: \(myClass.string ?? "nil") date: \(myClass.date ?? Date()) binary: \(myClass.binary)")
}
}
}
As soon as I call the writeMyClass function though, I get this error:
[DemoDecoder.MyClass encodeWithCoder:]: unrecognized selector sent to instance #blahblah#
Two things I have also tried:
func encode(with aCoder: NSCoder)
to MyClass
MyClass
& CodingKeys
and the init/encode functionsUpvotes: 5
Views: 2309
Reputation: 4160
Solution from @CodeBender works just fine, though there is no need to do manual encoding / decoding using init(from decoder: Decoder)
and encode(to encoder: Encoder)
methods, doing so just defeats the very purpose of the GREAT Codable protocol, unless you need to do some complex level of encoding / decoding.
Here is the code that works just well using the pure benefit of Codable protocol:
import UIKit
struct Movie: Codable {
enum MovieGenere: String, Codable {
case horror, drama, comedy, adventure, animation
}
var name : String
var moviesGenere : [MovieGenere]
var rating : Int
}
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
writeMyMovie(movie: Movie(name: "Titanic", moviesGenere: [Movie.MovieGenere.drama], rating: 1))
readMyMovie()
}
var documentDirectoryURL:URL? {
do {
let documentDirectory = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor:nil,
create:false)
return documentDirectory.appendingPathComponent(String(describing: Movie.self))
} catch {
return nil
}
}
func writeMyMovie(movie:Movie) {
do {
let data = try JSONEncoder().encode(movie)
try data.write(to: documentDirectoryURL!) // CAN USE GUARD STATEMENT HERE TO CHECK VALID URL INSTEAD OF FORCE UNWRAPPING, IN MY CASE AM 100% SURE, HENCE NOT GUARDING ;)
} catch {
print(error.localizedDescription)
}
}
func readMyMovie() {
do {
let data = try Data(contentsOf: documentDirectoryURL!)
let movie = try JSONDecoder().decode(Movie.self, from: data)
print("MOVIE DECODED: \(movie.name)")
} catch {
print(error.localizedDescription)
}
}
}
Upvotes: 0
Reputation: 36670
This is the working code derived from the answer provided by rmaddy above.
A few highlights:
NSKeyedArchiver
& NSKeyedUnarchiver
UserDefaults
JSONEncoder
& JSONDecoder
to write out structData
objectThis is the updated struct & enum that I wish to save:
enum Binary: Int {
case a = 0
case b = 1
case c = 9
}
struct MyStruct {
var string: String?
var date: Date?
var binary: Binary = .c
init() { }
enum CodingKeys: CodingKey {
case string, date, binary
}
}
extension MyStruct: Codable {
init(from decoder: Decoder) throws {
self.init()
let values = try decoder.container(keyedBy: CodingKeys.self)
string = try values.decode(String.self, forKey: .string)
date = try values.decode(Date.self, forKey: .date)
binary = try Binary(rawValue: values.decode(Int.self, forKey: .binary)) ?? .c
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(string, forKey: .string)
try container.encode(date, forKey: .date)
try container.encode(binary.rawValue, forKey: .binary)
}
}
The updated controller class that handles reading & writing the output. In my case, writing out to JSON was fine, so I went with that approach.
class MyStructController {
private var myStruct: MyStruct
init() {
self.myStruct = MyStruct()
self.myStruct.string = "string"
self.myStruct.date = Date()
self.myStruct.binary = .a
}
func writeMyStruct() {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(myStruct)
let documentDirectory = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor:nil,
create:false)
let url = documentDirectory.appendingPathComponent(String(describing: MyStruct.self))
try data.write(to: url)
} catch {
print(error.localizedDescription)
}
}
func readMyStruct() {
do {
let documentDirectory = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor:nil,
create:false)
let url = documentDirectory.appendingPathComponent(String(describing: MyStruct.self))
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let myNewStruct = try decoder.decode(MyStruct.self, from: data)
print("string: \(myNewStruct.string ?? "nil") date: \(myNewStruct.date ?? Date()) binary: \(myNewStruct.binary)")
} catch {
print(error.localizedDescription)
}
}
}
Upvotes: 6
Reputation: 318924
You have a lot of mismatched attempts and various encoding/decoding mechanisms.
NSKeyedArchiver
and NSKeyedUnarchiver
require that all involved types conform to the NSCoding
protocol. This is the older mechanism from the Objective-C frameworks.
The protocols Codable
, Encoder
, and Decoder
are new to Swift 4. Such data types should be used with Swift encoder and decoders such as JSONEncoder
and JSONDecoder
or PropertyListEncoder
and PropertyListDecoder
.
I suggest you remove the reference to NSCoder
and remove the uses of NSKeyedArchiver
and NSKeyedUnarchiver
. Since you have implemented the Codable
protocol, use an appropriate Swift encoder and decoder. In your case you want to use PropertyListEncoder
and PropertyListDecoder
.
Once that is done you should probably change MyClass
to be a struct
instead of a class
.
You should also avoid use UserDefaults
to store data. Write the encoded data to a plist file instead.
Upvotes: 7