Clearing :Date field in Realm when updating object - swift

I have the following data model
#objc dynamic var uid = UUID().uuidString;
#objc dynamic var completed: Bool = false;
#objc dynamic var dateCompleted: Date?;
When I create a new object it has nil for the dateCompleted, then when the user completes the task I try to update the object with primaryKey (uid) like so
let query = realm.objects(Tasks.self).filter("uid = %#", id).first!;
if query.completed == false {
var date = Date();
} else {
var date: Date = nil
}
let wayUpdate = ["uid": id, "completed": !query.completed, "dateCompleted": date] as [String : Any]
do {
try realm.write {
realm.create(WaysData.self, value: wayUpdate, update: true)
}
} catch {
print("Error adding update to experience! \(error)");
}
I get the following error Cannot convert value of type 'String' to specified type 'Date' in the else block.
My question is - is there a way to clear the date when updating completion to false, or should I just leave dateCompleted as Date() each time the user completes/uncompletes/completes the task and just do a check on the completed: Bool value whenever I want to update the completion button's label to "Complete" or "Revive"?

If you simply want to update a single property of your Object subclass instance, don't call create, simply modify that single property in a write transaction.
do {
try realm.write {
query.dateCompleted = query.completed ? nil : Date()
}
} catch {
print("Error adding update to experience! \(error)")
}
You should also use the primary key to retrieve the object, don't filter the query.
let query = realm.object(ofType: Tasks.self,forPrimaryKey: id)
Unrelated to your issue, but you shouldn't use ; at the end of your lines in Swift, this is not Objective-C.

Related

What is the best way to store a ToDo on Firestore?

I recently followed the build a to do list app with SwiftUI and Firestore run by Peter Friese on YouTube. The ToDos had a boolean completed attribute to indicate whether or not they are complete and they are fetched with a snapshot listener.
Below is an example of a ToDo item that is stored and retrieved from my Firestore collection.
struct ToDo: Identifiable, Codable {
#DocumentID var id: String?
var title: String = ""
var completionDate: Date?
var userId: String?
}
I wanted to achieve the following functionality:
Incomplete ToDos would be fetched
Complete ToDos in the last 24 hours would be fetched
Due to the time functionality I swapped the Boolean attribute for a completion date that gets updated every time the user checks or unchecks whether or not they have complete a ToDo.
Regarding my last question, I discovered that I cannot query for ToDos that have a nil value nor can I make this an OR statement to retrieve the completed ToDos in the last 24 hours in one query.
Is it possible to have the above functionality with one query and one snapshot listener?
Edit 1
Below is an extract from my data model class that fetches my tasks from my firestore collection. The comments are what I want the query below to reflect.
import Foundation
import Firebase
class TaskData: ObservableObject {
#Published var tasks: [Task] = []
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
init() {
subscribe()
}
deinit {
unsubscribe()
}
func subscribe() {
guard let userId = Auth.auth().currentUser?.uid else { return }
if listenerRegistration != nil {
unsubscribe()
}
/*
complete = true
completedDate = Date.now - 24 hours
OR
completed = false
*/
let query = db.collection("tasks")
.whereField("userId", isEqualTo: userId)
listenerRegistration = query.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("No documents in 'tasks' collection")
return
}
self.tasks = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Task.self)
}
}
}
func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
}
If you default the completionDate to a timestamp very far in the future, you can get all documents that were completed in the past 24h and that are not yet completed with a single query on that field.
todoRef.whereField("completionDate", isGreaterThan: timestampOfYesterday)
In your application code, you will then have to identify the documents with the default completionDate and mark them as incomplete in your UI.

Migrating existing Realm objects with duplicate values to updated model with primary key?

We have a Realm object model that looks like this:
class RealmGroup: Object {
#Persisted var name: String?
#Persisted var groupId: Int? // this *should* have been the primary key
#Persisted var groupPath: String?
}
We also have multiple object models that define a "group" property of type RealmGroup. For example:
class A: Object {
// other properties removed for brevity
#Persisted var group: RealmGroup?
}
class B: Object {
#Persisted var group: RealmGroup?
}
class C: Object {
#Persisted var group: RealmGroup?
}
On one of my development phones, there are approximately 1000 A, B and C objects in the realm. And across all 1000 of those objects, there may only be 10 - 15 unique group IDs. The "groupId" property on the RealmGroup object should have been declared as the primary key so we'd only ever have one instance of RealmGroup with "groupId" = 12345, for example. And then, of course, every A, B and C object belonging to that group would reference the same RealmGroup object. But unfortunately, that wasn't caught until after we put this code into production. So now I'm looking into the possibility of "fixing" this retroactively through a migration.
It seems understandable that I can't simply make the "groupId" property the primary key on the existing RealmGroup model since we already have multiple instances of RealmGroup objects with the same "groupId" value in the realm. But beyond that, I'm not sure how I'd be able to implement this inside of a migration block. The Realm Swift SDK docs cover a few examples of migration but not exactly something like what I'm trying to accomplish.
Is it possible to do this without needing to create a new model (e.g., RealmGroupV2)?
I'm not sure if there's a better way to do this but I did finally get it working. First, I created a new RealmGroupV2 model with the group ID defined as the primary key. Then I added a new .group_v2 property to my A, B and C objects. Finally, I performed the migration like this:
// enumerate all of the existing RealmGroup objects and create one new RealmGroupV2
// object for each unique group ID
var groupIds: [Int] = [] // keep track of the group IDs we've processed
migration.enumerateObjects(ofType: RealmGroup.className(), { oldGroupObject, _ in
guard let oldGroupObject = oldGroupObject else { return }
guard let groupId = oldGroupObject.value(forKey: "groupId") as? Int else { return }
guard let name = oldGroupObject.value(forKey: "name") as? String else { return }
guard let groupPath = oldGroupObject.value(forKey: "groupPath") as? String else { return }
if groupIds.contains(groupId) == false {
// create a new RealmGroupV2 object for this group
let groupV2 = migration.create(RealmGroupV2.className())
groupV2["groupId"] = groupId
groupV2["name"] = name
groupV2["groupPath"] = groupPath
// add groupId to the groupIds array so we don't try to create another
// RealmGroupV2 for this groupId
groupIds.append(groupId)
}
})
// enumerate all of the existing A objects, assign a RealmGroupV2 object to the
// new .group_v2 property and set the now-deprecated .group property to nil
migration.enumerateObjects(ofType: A.className(), { oldObject, newObject in
guard let oldObject = oldObject else { return }
guard let newObject = newObject else { return }
guard let groupObject = oldObject["group"] as? Object else { return }
guard let groupId = groupObject.value(forKey: "groupId") as? Int else { return }
migration.enumerateObjects(ofType: RealmGroupV2.className(), { oldGroupV2Object, newGroupV2Object in
guard let newGroupV2Object = newGroupV2Object else { return }
if let groupIdFromV2Object = newGroupV2Object["groupId"] as? Int {
if groupIdFromV2Object == groupId {
newObject["group_v2"] = newGroupV2Object
newObject["group"] = nil
}
}
})
})
// *** repeat above migration for B and C objects ***
// finally, delete all of the old RealmGroup objects
migration.deleteData(forType: RealmGroup.className())
I'm sure there are ways to improve upon this but hopefully it will help someone else who might be struggling with a similar situation.

SwiftUI: compiler takes initialized value from ViewModel rather than the value that has been fetched

I'm writing a program where I reference a database where authenticated users each have a document whose ID corresponds to their User ID. Given the user's ID, I am trying to determine their name; I have managed to read all of the user's data and it is in my data model of class Users:
class Users {
var id: String
var name: String
var surname: String // ...
}
In my ViewModel, I have
#Published var specificUser = User(id: "", name: "", surname: "", email: "", profficiency: 0, lists: [[]])
which is an initialized user.
In that same ViewModel, I have a function that fetches the User Data from the database, which appears to work. It should then store the new user data in the specificUserData variable.
func getData() {
let db = Firestore.firestore()
guard let uid = auth.currentUser?.uid else { return }
db.collection("Users").getDocuments { result, error in
if error == nil {
print("Current User's ID found: \(uid)")
if let result = result {
// iterate through documents until correct ID is found
for d in result.documents {
if d.documentID == uid {
print("Document ID found: \(d.documentID)")
self.specificUser = User(
id: d.documentID,
name: d["name"] as? String ?? "",
// ...
)
print(self.specificUser)
print(self.specificUser.name) // This works; my compiler spits out the correct name from the database, so clearly the specificUser variable has been changed.
}
}
}
} else {
// Handle Error
print("Error while fetching user's specific data")
}
}
}
Here's how I initialized the getData() function:
init() {
model.getData()
print("Data Retrieval Complete")
print("User's Name: \(model.specificUser.name)")
}
I am trying to reference my ViewModel like this:
#ObservedObject var model = ViewModel()
Now here's the problem: when I try to reference the User's name from the view model in my struct with
model.specificUser.name
It gives me the default name, even though I have initialized the getData() function already. Checking my compiler log and adding a bunch of print statements, it appears that the initialization is in fact working, but it is printing data retrieval complete before it is printing the albeit correct name.
Any thoughts? It seems that the initializer function is taking the initialized value from my ViewModel rather than the correct value it should be computing.
try this
func getData(_ completion: #escaping (Bool, User?) -> ()) {
let db = Firestore.firestore()
guard let uid = auth.currentUser?.uid else { return }
db.collection("Users").getDocuments { result, error in
if error == nil {
print("Current User's ID found: \(uid)")
if let result = result {
// iterate through documents until correct ID is found
for d in result.documents {
if d.documentID == uid {
print("Document ID found: \(d.documentID)")
let user = User(
id: d.documentID,
name: d["name"] as? String ?? "",
// ...
)
completion(true, user)
print(self.specificUser)
print(self.specificUser.name) // This works; my compiler spits out the correct name from the database, so clearly the specificUser variable has been changed.
}
}
}
} else {
// Handle Error
completion(false, nil)
print("Error while fetching user's specific data")
}
}
}
init() {
model.getData() { res, user in
if res {
self.specificUser = user!
}
print("Data Retrieval Complete")
print("User's Name: \(model.specificUser.name)")
}
}

Can't save string to Realm Swift

I'm trying to save a string to Realm. For some reason, a nil value is being saved to Realm. Here is the function where I save to Realm (imageKey is the String)
func saveImageKey(_ imageKey: Data){
do{
try realm.write{
realm.add(imageKey)
}
}
catch{
print("Error saving imageKey: \(error)")
}
}
I'm not sure if this affects Realm, but when initializing the imageKey variable, I used a didSet to reorganize the imageKey objects by date created as follows:
var imageKey: Results<Data>?{
didSet{ //every time imageKey is updated, this will be called
imageKey = imageKey!.sorted(byKeyPath: "date", ascending: false)
print(imageKey)
}
}
This is how I converted the imageKey string into a Data object:
let newData = Data()
newData.imageKey = "item" + String(textCount)
saveImageKey(newData)
This is the class definition for Data:
class Data: Object{
#objc dynamic var text: String = ""
#objc dynamic var imageKey: String = ""
#objc dynamic var date = NSDate().timeIntervalSince1970
}
I have already checked for common errors, such as not initializing Realm, not adding the dynamic to the variable declarations in the Data class.
What could the issue be? Please let me know if you need more code/information.

checking undefined value or specific value before retrieving from parse

checking undefined value before retrieving from parse
I am making a simple app, and implementing the userInformation part. The user can edit his info, but I have trouble that if user doesn't put any info, it will crash when I try to retrieve data from an undefined column.
This is my code to retrieve the user data. If there is data to parse it won't crash, otherwise it will.
var query = PFQuery(className: "Note")
query.getObjectInBackgroundWithId("kg8KhAWCms", block: {
(obj, error)in
if let score = obj! as? PFObject {
print(score.objectForKey("title"))
var nickname = (score.objectForKey("title")) as! String
self.nickNameLabel.text = nickname
} else {
print(error)
}
})
I tried this code as well, but it has error which is binary operator '!=' cannot be applied to operands of type 'String' and 'NiLiteralConvertible'
var query = PFQuery(className: "Note")
query.getObjectInBackgroundWithId("kg8KhAWCms", block: {
(obj, error)in
if let obj = obj! as? PFObject {
print(obj.objectForKey("title"))
var nickname = (obj.objectForKey("title")) as! String
if (nickname != nil) {
self.nickNameLabel.text = nickname
}else{
self.nickNameLabel.text = "you don't have a nick name"
}
} else {
print(error)
}
})
So I am asking how can I handle retrieving undefined value before crash? (please write full code for me)
Is there a way I can check undefined value or specific value in column before I retrieve it?
///like this
if (value in parse == "ABC") {
print("yes")
}