Saving array with custom objects with NSCoding - swift3

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")
}

Related

#Published Array is not updating

i'm currently struggling to fetch any changes from an published variable in SwiftUI. Most of the code is created after this tutorial on YouTube.
It's basically an app, that fetches cryptos from a firebase database. To avoid high server costs I want to update any changes of the coins to the database but not have an observer to lower the download rate.
What's the bug?
When I'm adding a coin to my favorites, it sends the data correctly to the database and updates the UI. However when I try to filter the coins the Coin-array switches back to it's previous state. I also added a breakpoint on the CoinCellViewModel(coin: coin)-Line but it only gets executed when I change the filterBy. Here's a little visualisation of the bug:
Repository
class CoinsRepository: ObservableObject {
#Published var coins = [Coin]()
var ref: DatabaseReference!
init() {
self.ref = Database.database().reference()
loadDatabase(ref)
}
func loadDatabase(_ ref: DatabaseReference) {
ref.child("coins").observeSingleEvent(of: .value) { snapshot in
guard let dictionaries = snapshot.value as? [String: Any] else { return }
var coinNames: [String] = []
self.coins = dictionaries.compactMap({ (key: String, value: Any) in
guard let dic = value as? [String: Any] else { return nil }
coinNames.append(dic["name"] as? String ?? "")
return Coin(dic)
})
}
}
func updateFavorite(_ coin: Coin, state: Bool) {
let path = ref.child("coins/\(coin.name)")
var flag = false
path.updateChildValues(["favorite": state]) { err, ref in
if let err = err {
print("ERROR: \(err.localizedDescription)")
} else {
var i = 0
var newCoinArray = self.coins
for coinA in newCoinArray {
if coinA.name == coin.name {
newCoinArray[i].favorite = state
}
i += 1
}
// I guess here's the error
DispatchQueue.main.async {
self.objectWillChange.send()
self.coins = newCoinArray
}
}
}
}
}
ViewModel
class CoinListViewModel: ObservableObject {
#Published var coinRepository = CoinsRepository()
#Published var coinCellViewModels = [CoinCellViewModel]()
#Published var filterBy: [Bool] = UserDefaults.standard.array(forKey: "filter") as? [Bool] ?? [false, false, false]
#Published var fbPrice: Double = 0.00
#Published var searchText: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.combineLatest(coinRepository.$coins, $fbPrice, $filterBy)
.map(filter)
.sink { coins in
self.coinCellViewModels = coins.map { coin in
CoinCellViewModel(coin: coin)
}
}
.store(in: &cancellables)
}
...
}
updateFavorite(_ coin: Coin, state: Bool) get's called in the CoinCellViewModel() but I guess the code isn't necessary here...
I'm fairly new to the Combine topic and not quite getting all the new methods, so any help is appreciated!

How to add and delete objects from a List from an object who's inside another List in SwiftUI and Realm

In the following code I have a List of Cars and each Car from that list has its own list of Services, I can add and delete Cars without a problem by calling carViewModel.addNewCar(make:String, model:String) and carViewModel.deleteCar(at indexSet:IndexSet).
Car.swift
import RealmSwift
final class Car: Object, ObjectKeyIdentifiable{
#objc dynamic var make: String = ""
#objc dynamic var model: String = ""
// creation date, ID etc.
dynamic var services = List<CarService>()
}
CarList.swift
import RealmSwift
final class CarList: Object, ObjectKeyIdentifiable{
#objc dynamic var name: String = ""
// creation date, ID etc.
var cars = RealmSwift.List<Car>()
}
CarService.swift
import RealmSwift
final class CarService: Object, ObjectKeyIdentifiable{
#objc dynamic var serviceName: String = ""
// creation date, ID etc.
}
View Model
import RealmSwift
class CarViewModel: ObservableObject{
#Published var cars = List<Car>()
#Published var selectedCarList: CarList? = nil
var token: NotificationToken? = nil
init(){
// Create a the default lists if they don't already exist.
createDefaultCarList()
createDefaultServiceList()
// Initialize the SelectedCarList and the cars variables items from the Default Car List.
if let list = realm?.objects(CarList.self).first{
self.selectedCarList = list
self.cars = list.cars
}
token = selectedCarList?.observe({ [unowned self] (changes) in
switch changes{
case .error(_): break
case.change(_, _):self.objectWillChange.send()
case.deleted: self.selectedCarList = nil
}
})
}
func addNewCar(make:String, model:String){
if let realm = selectedCarList?.realm{
try? realm.write{
let car = Car()
car.make = make
car.model = model
selectedCarList?.cars.append(car)
}
}
}
func deleteCar(at indexSet:IndexSet){
if let index = indexSet.first,
let realm = cars[index].realm{
try? realm.write{
realm.delete(cars[index])
}
}
}
func addService(toCar: Car, serviceName: String){
try? realm?.write{
let service = CarService()
service.serviceName = serviceName
toCar.services.append(service)
}
}
/// Creates the Default Car List if it doesn't already exists otherwise just prints the error.
func createDefaultCarList(){
do {
if (realm?.objects(CarList.self).first) == nil{
try realm?.write({
let defaultList = CarList()
defaultList.name = "Default Car List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
/// Creates the Default Serivice List if it doesn't already exists otherwise just prints the error.
func createDefaultServiceList(){
do {
if (realm?.objects(ServiceList.self).first) == nil{
try realm?.write({
let defaultList = ServiceList()
defaultList.listName = "Default Service List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
}
My issue is adding or deleting Services to existing Cars. When I call carViewModel.addService(toCar: Car, serviceName: String) I get the error below...
Calling the addService() method.
struct NewServiceFormView: View {
#ObservedObject var carViewModel: CarViewModel
#State var selectedCar:Car // pass from other cars view
var body: some View {
NavigationView {
Form {
// fields
}
.navigationBarItems( trailing:Button("Save", action: addNewCar))
}
}
func addNewCar(){
carViewModel.addService(toCar: selectedCar, serviceName: "Oil Change")
}
}
Error
"Cannot modify managed RLMArray outside of a write transaction."
I can add new Services by explicitly selecting a Car from the cars list. I don't get any errors but the UI doesn't update; I don't see the newly added Service until the app is relaunched.
No errors doing it this way but the UI doesn't update.
carViewModel.addService(toCar: carViewModel.cars[1], serviceName: "Rotors")
How can I properly watch, delete and add Services to existing Cars?
EDIT: Added the following code per Mahan's request.
View to present the NewServiceFormView
struct CarServicesView: View {
#State var selectedCar:Car // a car from parent view
#ObservedObject var carViewModel: CarViewModel
var body: some View {
VStack{
List {
Section(header: Text("Services: \(selectedCar.services.count)")) {
ForEach(selectedCar.services) { service in
}
}
}
.listStyle(GroupedListStyle())
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: openNewServiceForm) {
Image(systemName: "plus")
}
}
}
}.sheet(isPresented: $newServiceFormIsPresented){
NewServiceFormView(carViewModel: carViewModel, selectedCar: selectedCar)
}
}
func openNewServiceForm() {
newServiceFormIsPresented.toggle()
}
}
One issue is how the Realm objects are being observed - remember they are ObjC objects under the hood so you need to use Realm observers. So this
#ObservedObject var carViewModel: CarViewModel
should be this
#ObservedRealmObject var carViewModel: CarViewModel
See the documentation for observedRealmObject
Also, keep in mind if you're observing a Results, the same thing applies, use
#ObservedResults
as shown in the documentation

Class does not conform to Encodable

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)
}
}

How do you use the result of a function as a Bindable object in Swiftui?

I'm developing a simple SwiftUI app, using Xcode 11 beta5.
I have a list of Place, and i want to display the list, and add / edit them.
The data come from core data.
I have 3 classes for this :
- CoreDataController, which handle the connection to core data
- PlaceController, which handle operation on the Places.
public class CoreDataController {
static let instance = CoreDataController()
private let container = NSPersistentContainer(name: "RememberV2")
private init() {
print("Start Init DataController")
container.loadPersistentStores { (storeDescription, error) in
if let error = error {
fatalError("Failed to load store: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
print("End Init DataController")
}
func getContext() -> NSManagedObjectContext {
return container.viewContext
}
func save() {
print("Start Save context")
do{
try container.viewContext.save()
} catch {
print("ERROR - saving context")
}
print("End Save context")
}
}
public class PlaceController {
static let instance = PlaceController()
private let dc = CoreDataController.instance
private let entityName:String = "Place"
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
func createPlace(name:String) -> Bool {
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
dc.save()
DataController.instance.places = getAllPlaces()
return true
}
func createPlace(name:String, comment:String) -> Bool {
print("Start - create place with comment")
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
newPlace.setValue(comment, forKey: "comment")
dc.save()
print("End - create place with comment")
DataController.instance.places = getAllPlaces()
return true
}
func getAllPlaces() -> [Place] {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
if let fp = try? dc.getContext().fetch(r) as? [Place] {
return fp
}
return [Place]()
}
func truncatePlaces() -> Bool {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let batch = NSBatchDeleteRequest(fetchRequest: r)
if (try? dc.getContext().execute(batch)) != nil {
return true
}
return false
}
}
In my view i simply use the function :
List (pc.getAllPlaces(), id: \.id) { place in
NavigationLink(destination: PlaceDetail(place: place)) {
PlacesRow(place:place)
}
}
It works to display the information, but if i add a new place, the list is not updated.
I have to go back to the home screen, then display again the Places screen for the list to be updated.
So i use another controller :
class DataController: ObservableObject {
#Published var places:[Place] = []
static let instance = DataController()
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
}
In my view, i just display the ObservedObject places.
#ObservedObject var data: DataController = DataController.instance
And in my PlaceController, i update the table in the DataController
DataController.instance.places = getAllPlaces()
That works, but i have this warning :
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes
Also i'm pretty sure there is a better way to do this ...
Any idea what is this better way ?
Thanks,
Nicolas

Can't store object in user defaults

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.