Why UserDefaults data is not loading when app starts? - swiftui

Data saved is working fine but when app starts, it initializes the events as empty but doesn’t load UserDefaults data using the code below. Any ideas ?
import SwiftUI
class TaskData: ObservableObject {
#Published var tasks: [Task] = [Task].init()
static let saveKey = "Saved Data"
init() {
if let data = UserDefaults.standard.data(forKey: Self.saveKey) {
do {
if let decoded = try JSONDecoder().decode([Event]?.self, from: data) {
self.events = decoded
print("Loaded Data")
print(self.events)
return
}
} catch {
print(error)
}
}
}
func delete(_ task: Task) {
events.removeAll { $0.id == task.id }
save()
}
func add(_ task: Task) {
tasks.append(task)
save()
}
func save() {
if let encoded = try? JSONEncoder().encode(tasks) {
UserDefaults.standard.set(encoded, forKey: Self.saveKey)
print("Saved")
print(encoded)
}
}
func exists(_ task: Task) -> Bool {
tasks.contains(task)
}
…
I understand the init to load data from userDefaults above should load data and insert into tasks but it doesn’t at app in foreground or opened.

Related

CoreData adding entity

Here is how I add new entity.
func addCountry(name: String, code: String, flagImageUri: String?, wikiDataId: String) {
let newCountry = CountryEntity(context: container.viewContext)
newCountry.name = name
newCountry.code = code
newCountry.flagImageUri = flagImageUri
newCountry.wikiDataId = wikiDataId
save()
}
Here is my data:
However when I use the add function in my view, I got this error:
CoreData: error: +[CountryEntity entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
And this is my button:
Button(action: {
country.isFaved = !country.isFaved
coreDataModel.addCountry(name: country.name, code: country.code, flagImageUri: country.flagImageUri, wikiDataId: country.wikiDataId)
}) {
Image(systemName: "star.fill")
.foregroundColor(country.isFaved ? .black : .white)
.scaledToFit()
}
This is the whole class. I'm fetching, saving ,adding and deleting all data here. I did everything like the video I watched in youtube.
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "CountryCoreData")
#Published var savedCountries: [CountryEntity] = []
init() {
container.loadPersistentStores(completionHandler: { _, error in
if let error = error {
print("CoreData failed to load: \(error.localizedDescription)")
} else {
print("Successfully loaded")
}
})
}
func fetchCountries() -> [CountryEntity]? {
let request = NSFetchRequest<CountryEntity>(entityName: "CountryEntity")
do {
let fetchedCountries = try container.viewContext.fetch(request)
return fetchedCountries
} catch {
print("Something went wrong while data fetching \(error)")
return nil
}
}
func delete(code: String) {
guard let fetchedCountries = fetchCountries() else { return }
for country in fetchedCountries {
if country.code!.contains(code) {
container.viewContext.delete(country)
save()
}
}
}
func addCountry(name: String, code: String, flagImageUri: String?, wikiDataId: String) {
let newCountry = CountryEntity(context: container.viewContext)
print("OSMAN")
newCountry.name = name
newCountry.code = code
newCountry.flagImageUri = flagImageUri
newCountry.wikiDataId = wikiDataId
save()
}
func save() {
do {
try container.viewContext.save()
fetchCountries()
} catch {
print("Error while saving the data: \(error)")
}
}
}
How can I solve this problem?

Memory Leak with swiftUI List and Firebase database observing childAdded

I'm writing a chat application using firebase , I notice memory leak in ChatView while observing database changes which is when a message is sent or received.
when I comment out the database observation the memory leak dose not happen anymore so I'm guessing this is a firebase problem .
I'm sharing the code so please if you know what is acutely causing the memory leak help me out.
ChatViewModel:
class ChatViewModel : ObservableObject {
/// - sub ViewModels :
#Published private(set) var messages : [MessageModel] = []
private(set) var conversationID : String? = nil
/// set shared conversationID
/// - Parameter convesationID: shared conversationID if exist
func setConverationID(convesationID : String?){
guard let convesationID = convesationID else {
print("CONVERSATION ID DOUS NOT EXIT")
return
}
self.conversationID = convesationID
startObservingConversation()
}
/// start observing the conversation with viewModel conversationID
private func startObservingConversation(){
guard let conversationID = self.conversationID else {
return
}
DatabaseManager.shared.observeMessagesForConversation(conversationId: conversationID) { [weak self] message in
self?.messages += message
}
}}
ChatView :
struct ChatView: View {
#StateObject var viewModel = ChatViewModel()
var body: some View {
VStack(alignment : .leading , spacing: 0){
ScrollViewReader { scrollViewReader in
List{
ForEach(viewModel.messages) { item in
MessageView(messsage: item.text)
.id(item.id)
}
}
}
}
}}
observerMessages :
func observeMessagesForConversation(conversationId id :String,compelition : #escaping ([MessageModel]) -> Void ) {
database.child(id).child("messages").observe(.childAdded) { snapshot in
guard let value = snapshot.value as? [String:Any] else {
compelition([])
return
}
var messages : [MessageModel] = []
let decoder = JSONDecoder()
guard
let jsonData = try? JSONSerialization.data(withJSONObject:value),
let message = try? decoder.decode(MessageModel.self, from: jsonData) else {
compelition([])
return
}
messages.append(message)
compelition(messages)
}
}

How to auto update the UI in SwiftUI when changes are made in realm database?

My realm database structure looks like this example:
class Person: Object, Identifiable {
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var name: String = ""
var dogs = RealmSwift.List<Dog>()
}
class Dog: Object, Identifiable {
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var name: String = ""
let human = RealmSwift.LinkingObjects<Person>(fromType: Person.self, property: "dogs")
}
To simplify CRUD operations I have a database manager:
extension Realm {
public func safeWrite(_ block: (() throws -> Void)) throws {
if isInWriteTransaction {
try block()
} else {
try write(block)
}
}
}
class DatabaseManager {
private let realm: Realm
public static let sharedInstance = DatabaseManager()
private init(){
realm = try! Realm()
}
func save(_ obj: Object){
do {
try realm.safeWrite {
realm.add(obj, update: .all)
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func save(_ obj: Object,_ block: () -> Void){
do {
try realm.safeWrite{
realm.add(obj, update: .all)
block()
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func save(_ objs: [Object]){
do {
try realm.safeWrite {
realm.add(objs, update: .all)
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
func delete(_ obj: Object){
do {
try realm.safeWrite {
realm.delete(obj)
}
} catch {
NSLog("error deleting object: %#", [error])
}
}
func update(_ block: #escaping () -> Void){
do {
try realm.safeWrite{
block()
}
} catch {
NSLog("error updating object: %#", [error])
}
}
}
Now I have different views where I need to access my database and display some data. For this case I've created a view model class:
class PersonViewModel: ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var persons: Results<Person> = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
public func fetch(){
self.persons = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
}
public func addPerson(person: Person){
realm.save(Person)
self.fetch() // <---- necessary to update the UI: how to auto update?
}
}
This view model class will be passed as an #EnvironmentObject to the views where I need the data. As you can see I need to fetch after each database operation all data again to have a "fresh updated" Results<Person>. I know that Results<T> are live, but it has no effect to the UI. Is there any way to auto updating all views when I do a change on the database without fetching all data manually again?
While Realm automatically updates collections as new data is deleted, added or changed, there needs to be a trigger that notifies your code of those changes.
That's a notification aka observer.
In this case there's an Results object persons populated with the fetchData function:
#Published var persons: Results<Person> = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
So when a person is deleted, added or changed the persons results object will be automatically updated. However to be notified of that, an observer needs to be added, and there are two components: a class level notification token and then a function to handle the event.
A notification token is defined like this (note a strong reference is needed to keep it alive)
var notificationToken: NotificationToken? = nil
and ensure it's deallocated when the view closes
deinit {
self.notificationToken?.invalidate()
}
and then the code (in it's simplist form) that will execute when changes to people occurs
self.notificationToken = self.persons?.observe { changes in
switch changes {
case .initial:
print("initial load")
//the initial data is ready - reload tableviews/update ui etc
case .update(_, _, _, _):
print("update")
//some change occurred, reload tableviews/update ui etc
case .error(let error):
fatalError("\(error)")
}
}
Here's a link that covers this in depth Collection Notifications

Type 'Favorites.Type' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols

Please tell me what could be the problem with this error and how to fix it?
I'm use SwiftUI 2.0
"Type 'Favorites.Type' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols"
Code:
class Favorites: ObservableObject {
private var tasks: Set<String>
let defaults = UserDefaults.standard
init() {
let decoder = JSONDecoder()
if let data = defaults.value(forKey: "Favorites") as? Data {
let taskData = try? decoder.decode(Set<String>.self, from: data)
self.tasks = taskData ?? []
} else {
self.tasks = []
}
}
func getTaskIds() -> Set<String> {
return self.tasks
}
func isEmpty() -> Bool {
tasks.count < 1
}
func contains(_ task: dataTypeFont) -> Bool {
tasks.contains(task.id)
}
func add(_ task: dataTypeFont) {
objectWillChange.send()
tasks.insert(task.id)
save()
}
func remove(_ task: dataTypeFont) {
objectWillChange.send()
tasks.remove(task.id)
save()
}
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(Favorites) {
defaults.set(encoded, forKey: "Favorites")
}
}
}
Screenshot Error:
Error
Typo.
According to the load method you have to encode tasks not the class type
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(tasks) {
defaults.set(encoded, forKey: "Favorites")
}
}
And don't use value(forKey: with UserDefaults, there is a dedicated method
if let data = defaults.data(forKey: "Favorites") {

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