I have a class that conforms to NSCoding:
class Middleware : NSObject, NSCoding {
var name: String
var uri: String
// MARK: NSCoding
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
uri = aDecoder.decodeObject(forKey: "uri") as! String
}
func encode(with: NSCoder) {
with.encode(name, forKey: "name")
with.encode(uri, forKey: "uri")
}
}
Storing that class in UserDefaults fails:
userDefaults.set(Middleware(name: "Volkszaehler Demo", uri: "http://demo.volkszaehler.org/middleware.php"), forKey: "middlewares")
Gives:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key middlewares'
Got it- custom object needs to go through the encoder first:
let encodedData = NSKeyedArchiver.archivedData(withRootObject: middleware)
encodedData can then be stored.
Related
I am new to SwiftUI and I am trying to encode and decode a MKPlacemark struct to json.
I have the struct defined as below. I am able to display the details in the app but I am not able to decode it.
import Foundation
import MapKit
import UIKit
struct Landmark {
let placemark: MKPlacemark
var id: UUID {
return UUID()
}
var name: String {
self.placemark.name ?? ""
}
var title: String {
self.placemark.title ?? ""
}
var coordinate: CLLocationCoordinate2D {
self.placemark.coordinate
}
}
I can search for placemarks like this:
import Foundation
import Combine
import MapKit
class SearchPlaces: NSObject, ObservableObject {
#Published var searchQuery = ""
#Published var landmarks: [Landmark] = [Landmark]()
#Published var items: [MapItem] = [MapItem]()
public func getNearByLandmarks() {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = searchQuery
let search = MKLocalSearch(request: request)
search.start { (response, error) in
if let response = response {
let mapItems = response.mapItems
self.landmarks = mapItems.map {
Landmark(placemark: $0.placemark)
}
Task {
await self.getData()
}
print("Lamdmarks \(self.landmarks)")
}
}
}
private func getData() async {
guard let landmark = try? JSONEncoder().encode(self.landmarks) else { return }
do {
let decodedLandmark = try JSONDecoder().decode(Landmark.self, from: landmark)
print("decodedLandmark \(decodedLandmark.id)")
} catch {
print("Error \(error.localizedDescription)")
}
}
}
But I get this error: Error
The data couldn’t be read because it isn’t in the correct format.
The placemark looks like this in xcode
Lamdmarks \[Landmark(placemark: La Hacienda Market, 249 Hillside Blvd, South San Francisco, CA 94080, United States # \<+37.66312925,-122.40844847\> +/- 0.00m, region CLCircularRegion (identifier:'\<+37.66307481,-122.40861130\> radius 141.17', center:\<+37.66307481,-122.40861130\>, radius:141.17m))
How do I decode a MKPlacemark to json when I don't know all of its keys.
I tried this
extension NSSecureCoding { func archived() throws -> Data { try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) } }
extension Data { func unarchived<T: NSSecureCoding>() throws -> T? { try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(self) as? T } }
extension Landmark: Codable {
func encode(to encoder: Encoder) throws {
var unkeyedContainer = encoder.unkeyedContainer()
try unkeyedContainer.encode(placemark.archived())
try unkeyedContainer.encode(id)
try unkeyedContainer.encode(name)
try unkeyedContainer.encode(title)
try unkeyedContainer.encode(coordinate)
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
placemark = try container.decode(Data.self).unarchived()!
coordinate = try container.decode(CLLocationCoordinate2D.self, "coordinate")
id = try container.decode(UUID.self)
name = placemark.name ?? "no name"
title = placemark.title ?? "no title"
}
}
First of all never print(error.localizedDescription) in a Codable context. The generic error message is meaningless.
Always
print(error)
to get the real meaningful DecodingError.
Second of all don't try to adopt Codable by serializing each single property in classes which conform to NSSecureCoding. Take advantage of the built-in serialization and also of the PropertyWrapper pattern.
This PropertyWrapper converts/serializes MKPlacemark to Data and vice versa
#propertyWrapper
struct CodablePlacemark {
var wrappedValue: MKPlacemark
}
extension CodablePlacemark: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
guard let placemark = try NSKeyedUnarchiver.unarchivedObject(ofClass: MKPlacemark.self, from: data) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid placemark"
)
}
wrappedValue = placemark
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true)
try container.encode(data)
}
}
In the Landmark struct adopt Codable and declare the placemark
struct Landmark: Codable {
#CodablePlacemark var placemark: MKPlacemark
}
But the property wrapper makes only sense if you encode the placemark.
I have a view with a button that calls an API, the API either returns an HTTP code 200 or 400 based on a particular scenario.
The button works just fine and everything works smoothly if code 200 is returned, however if code 400 is returned, the view is not updated that the user have to click on the button once again to get the updated message.
I added the http code property as a published variable in the VM's class and the http is an observable, but it doesn't get updated in the view on the first API call, I'm not sure what I'm missing.
I made a lot of changes to the shared code just to help in demonstrating the actual problem.
Update: Also I think another part of the problem, is that the url function returns the value before the url session returns the data, I don't know why this is happening, that when I execute it a second time it uses the values from the previous execution.
HTTPError Class
class HTTPError : Codable, ObservableObject {
var statusCode: Int?
var message: [String]?
var error: String?
init(statusCode: Int? = nil, message: [String]? = [], error: String? = nil){
self.statusCode = statusCode
self.message = message ?? []
self.error = error
}
convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.statusCode = try container.decodeIfPresent(Int.self, forKey: .statusCode)
do {
self.message = try container.decodeIfPresent([String].self, forKey: .message)
} catch {
guard let value = try container.decodeIfPresent(String.self, forKey:
.message) else {return}
self.message = []
self.message?.append(value)
}
self.error = try container.decodeIfPresent(String.self, forKey: .error)
}
VM Class
class VM: ObservableObject {
#Published var isLoading = true
#Published var httpError = HTTPError()
func checkDriverIn(_ record: DriverQRParam) async -> (Bool) {
...
var request = URLRequest(url: url)
...
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
...
let task = URLSession.shared.dataTask(with: request) { (data, response,
error) in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
self.httpError = try! JSONDecoder().decode(HTTPError.self, from: data)
//gets updated just fine in this class//
}
task.resume()
}catch {
print("Couldn't encode data \(String(describing: error))")
}
if httpError.statusCode != nil && httpError.statusCode == 400 {
return (false)
} else {
return (true)
}
}
View.Swift
struct xyz: View {
#State private var VM = VM()
Button("click") {
Task {
await VM.checkDriverIn(driverParam)
}
}
}
However, getting an error message that says that my class 'Expenses' does not conform to protocol 'Decodable' & Type 'Expenses' does not conform to protocol 'Encodable'
import Foundation
class Expenses : ObservableObject, Codable {
#Published var items : [ExpenseItem] {
// Step 1 creat did set on publsihed var.
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder(
if let decoded = try?
decoder.decode([ExpenseItem].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}
my expense item is flagged as
struct ExpenseItem : Identifiable, Codable {
let id = UUID()
let name : String
let type : String
let amount : Int
}
Conformance to Encodable/Decodable is auto-synthesized when all stored properties conform to Encodable/Decodable, but using a property wrapper on a property means that now the property wrapper type needs to conform to Encodable/Decodable.
#Published property wrapper doesn't conform. It would have been nice to just implement conformance on the Published type itself, but unfortunately it doesn't expose the wrapped value, so without using reflection (I've seen suggestions online), I don't think it's possible.
You'd need to implement the conformance manually:
class Expenses : ObservableObject {
#Published var items : [ExpenseItem]
// ... rest of your code
}
extension Expense: Codable {
enum CodingKeys: CodingKey {
case items
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.items, forKey: .items)
}
required init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CodingKeys.self)
self.items = try container.decode([ExpenseItem].self, forKey: .items)
}
}
I'm making a to-do list app to learn the ins and outs and it has a simple lists-with-items structure. I use 3 classes to manage them:
TodoManager: is a singleton that is meant to centralise managing lists and items in those lists in my view controllers. It holds an array of TodoLists and a bunch of functions to add lists, mark them as completed and return lists.
TodoList: has a string var (name), bool var (completed) and an array of TodoItems
TodoItem: has a string var (name) and a bool var (completed).
I want to store my array of custom objects [TodoList] so I can load it later and I was looking for the simplest way in the world to do it. UserDefaults does not allow custom objects (as it shouldn't because it's for settings) so I need to persist the data using NSCoding and for that I need to have my TodoList class inherit from NSObjects.
class TodoManager: NSObject, NSCoding {
// the singleton
static let shared = TodoManager()
// filePath var
private var filePath : String {
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return url.appendingPathComponent("objectsArray").path
}
// init
override private init() {}
// array to store all lists
private var lists = [TodoList]()
func newTodoList(title: String) {
lists.append(TodoList(instanceTitle: title))
}
// coding
func encode(with coder: NSCoder) {
coder.encode(lists, forKey: "lists")
}
required convenience init(coder decoder: NSCoder) {
self.init()
lists = decoder.decodeObject(forKey: "lists") as! [TodoList]
}
// saving and loading
func saveAll() {
let data = lists
NSKeyedArchiver.archiveRootObject(data, toFile: filePath)
}
func loadAll() {
if let dataArray = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? [TodoList] {
lists = dataArray
}
}
}
class TodoList: NSObject, NSCoding {
// array to store items in this list instance
private var items = [TodoItem]()
// vars to store title and completion status
private var title = String()
private var completed = Bool()
// init
init(instanceTitle: String) {
title = instanceTitle
completed = false
}
// coding
func encode(with coder: NSCoder) {
coder.encode(items, forKey: "lists")
coder.encode(title, forKey: "title")
coder.encode(completed, forKey: "completed")
}
required convenience init(coder decoder: NSCoder) {
self.items = decoder.decodeObject(forKey: "items") as! [TodoItem]
self.title = decoder.decodeObject(forKey: "title") as! String
self.completed = decoder.decodeBool(forKey: "completed")
self.init() // <----- critical line
}
// item-related
func addItem(title: String) {
items.append(TodoItem(instanceTitle: title))
}
}
class TodoItem: NSObject, NSCoding {
private var title = String()
private var completed = Bool()
// inits
init(instanceTitle: String) {
title = instanceTitle
completed = false
}
func encode(with coder: NSCoder) {
coder.encode(title, forKey: "title")
coder.encode(completed, forKey: "completed")
}
required convenience init(coder decoder: NSCoder) {
self.init() // <----- similar critical line
title = decoder.decodeObject(forKey: "title") as! String
completed = decoder.decodeBool(forKey: "completed")
}
}
The problem I run into is that instanceTitle is undeclared when in the convenience init so I can't pass it to self.init(). I cannot add the declaration to the required convenience init because it will error that the class does not conform to the required protocol. I tried a good many variations but after hours of staring at this and using my google-foo I can't figure it out. What am I doing wrong in NSCoding's rabbit hole?
When the convenience initializer() is called, the actual arguments originally passed to the designated init() by the function that is requesting a new instance of the class to be made do not need to pass through the convenience init. You call the designated initializer with a placeholder which is filled in on execution. For my case it looks like this:
init(instanceTitle: String) {
title = instanceTitle
completed = false
}
required convenience init(coder decoder: NSCoder) {
// call designated init
self.init(instanceTitle: "[placeholder]")
items = decoder.decodeObject(forKey: "items") as! [TodoItem]
title = decoder.decodeObject(forKey: "title") as! String
completed = decoder.decodeBool(forKey: "completed")
}
i am new to Swift programming and have been working on a To-Do List app . I am trying to use the Permanent Data Storage to save the information entered by user,but i keep getting the error "Thread 1: signal SIGABRT " . When i checked the output log, i see the error
"Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: '-[__NSCFArray
insertObject:atIndex:]: mutating method sent to immutable object'"
My code is below. I use a simple textbox and a button:
#IBOutlet var text1: UITextField!
#IBAction func button1(_ sender: AnyObject) {
let listObject = UserDefaults.standard.object(forKey: "lists")
var items:NSMutableArray
if let tempitems = listObject as? NSMutableArray {
items = tempitems
items.addObjects(from: [text1.text!])
} else {
items = [text1.text!]
}
UserDefaults.standard.set(items, forKey: "lists")
text1.text = ""
}
The crash is exactly what it means: you can't mutate an immutable object. Try this:
var items: NSMutableArray!
if let listObject = UserDefaults.standard.object(forKey: "lists") as? NSArray {
items = listObject.mutableCopy() as! NSMutableArray
} else {
items = NSMutableArray()
}
items.addObjects(from: [text1.text!])