Realm: Update pre-populated Database while keeping data from a specific field - ios

I got a pre-populated database for my app. Occasionally I'm going to be updating this pre-populated database with new data.
The problem is that the object contains a boolean field called "completed". By default this field is false. When the user completes a certain task the object's boolean field becomes true.
I want to find a way when migrating to the new pre-populated database to keep that boolean field the same as before.
Here is the code I have so far
// copy over old data files
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let defaultParentURL = defaultURL.deletingLastPathComponent()
if let v0URL = bundleURL("database") {
//Delete if there's a file already
do {
try FileManager.default.removeItem(at: defaultURL)
print("Item Removed")
} catch {
print("error removing seeds: \(error)")
}
//Copy the database file to the directory
do {
try FileManager.default.copyItem(at: v0URL, to: defaultURL)
print("Item Copied")
}catch {
print("error copying seeds: \(error)")
}
}
// define a migration block
// you can define this inline, but we will reuse this to migrate realm files from multiple versions
// to the most current version of our data model
let migrationBlock: MigrationBlock = { migration, oldSchemaVersion in
migration.enumerateObjects(ofType: BookModel.className()) { oldObject, newObject in
// keep the old boolean completed value and assign it to the new one
let completed = oldObject!["completed"] as! Bool
newObject!["completed"] = completed
print(completed)
}
print("Migration complete.")
}
Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 5, migrationBlock: migrationBlock)
let realm = try! Realm()
return true
I am suspecting that my code does not work because before migration happens, I have already deleted my previous database (which contains the user's completed values). But how can I replace my previous database with the new one without delete-copy?
I can solve this with userDefaults but I would prefer the above solution.
Thank you in advance!

Related

Listener event not triggered when document is updated (Google Firestore)

I am struggling to understand why my event listener that I initialize on a document is not being triggered whenever I update the document within the app in a different UIViewController. If I update it manually in Google firebase console, the listener event gets triggered successfully. I am 100% updating the correct document too because I see it get updated when I update it in the app. What I am trying to accomplish is have a running listener on the current user that is logged in and all of their fields so i can just use 1 global singleton variable throughout my app and it will always be up to date with their most current fields (name, last name, profile pic, bio, etc.). One thing I noticed is when i use setData instead of updateData, the listener event gets triggered. For some reason it doesn't with updateData. But i don't want to use setData because it will wipe all the other fields as if it is a new doc. Is there something else I should be doing?
Below is the code that initializes the Listener at the very beginning of the app after the user logs in.
static func InitalizeWhistleListener() {
let currentUser = Auth.auth().currentUser?.uid
let userDocRef = Firestore.firestore().collection("users").document(currentUser!)
WhistleListener.shared.listener = userDocRef.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("INSIDE LISTENER")
}
}
Below is the code that update's this same document in a different view controller whenever the user updates their profile pic
func uploadProfilePicture(_ image: UIImage) {
guard let uid = currentUser!.UID else { return }
let filePath = "user/\(uid).jpg"
let storageRef = Storage.storage().reference().child(filePath)
guard let imageData = image.jpegData(compressionQuality: 0.75) else { return }
storageRef.putData(imageData) { metadata, error in
if error == nil && metadata != nil {
self.userProfileDoc!.updateData([
"profilePicURL": filePath
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
}
}
You can use set data with merge true it doesn't wipe any other property only merge to specific one that you declared as like I am only update the name of the user without wiping the age or address
db.collection("User")
.document(id)
.setData(["name":"Zeeshan"],merge: true)
The answer is pretty obvious (and sad at the same time). I was constantly updating the filepath to be the user's UID therefore, it would always be the same and the snapshot wouldn't recognize a difference in the update. It had been some time since I had looked at this code so i forgot this is what it was doing. I was looking past this and simply thinking an update (no matter if it was different from the last or not) would trigger an event. That is not the case! So what I did was append an additional UUID to the user's UID so that it changed.

Realm iOS: How to handle Client Reset

Basically, I want to handle a case where any device got SyncError with type ClientResetError then, want my device to re-login to realm again. but as per documentation, we have to closeRealmSafely before I login to realm again, but I am not sure how to close realm safely.
I am going through the doc (https://docs.realm.io/sync/using-synced-realms/errors#client-reset) to handle client reset error and found it's very confusing . I want help to understand about the following code.
First there is no method available to closeRealmsafely. Please help me understand how can I close the realm safely?
How can I backup and when I will use it? Should I skip the reset error because in documentation it's mentions if the client reset process is not manually initiated, it will instead automatically take place after the next time the app is launched, upon first accessing the SyncManager singleton. It is the app’s responsibility to persist the location of the backup copy if needed, so that the backup copy can be found later."
Below is the error handler sample code from the doc.
let syncError = error as! SyncError
switch syncError.code {
case .clientResetError:
if let (path, clientResetToken) = syncError.clientResetInfo() {
closeRealmSafely()
saveBackupRealmPath(path)
SyncSession.immediatelyHandleError(clientResetToken)
}
default:
// Handle other errors...
()
}
}```
Finally we figured out how to handle the client reset error. We have taken following steps To avoid the data loss incase user is offline and came online and got reset error.
Save the local realm to another directory
Invalidate and nil the realm
Initiate realm manual reset - Call SyncSession.immediatelyHandleError with clientResetToken passed and it will delete the existing realm from directory
Show client reset alert - This will intimate user to relaunch the app.
On next launch realm creates a fresh realm from ROS.
After new realm connects, restore the realm records (if any) from the old realm saved in backup directory above.
Delete the backup realm(old realm) from directory.
switch syncError.code {
case .clientResetError:
if let (path, clientResetToken) = syncError.clientResetInfo() {
// taking backup
backUpRealm(realm: yourLocalRealm)
// making realm nil and invalidating
yourLocalRealm?.invalidate()
yourLocalRealm = nil
//Initiate realm manual reset - Call `SyncSession.immediatelyHandleError` with `clientResetToken` passed and it will delete the existing realm from directory
SyncSession.immediatelyHandleError(clientResetToken)
// can show alert to user to relaunch the app
showAlertforAppRelaunch()
}
default:
// Handle other errors...
()
}
}```
The back up realm code look like this:
func backUpRealm(realm: Realm?) {
do {
try realm?.writeCopy(toFile: backupUrl)
} catch {
print("Error backing up data")
}
}
After doing this backup will be available at backup path. On next launch device will connect and download a fresh realm from ROS so after device connects restore the realm records from the backup realm saved in the backup path.
The restore merge backup code will look like this. place the below method when realm connects after relauch.The ```restoredRealm`` is fresh downloaded realm on launch
func restoreAndMergeFromBackup(restoredRealm: Realm?) {
let realmBackUpFilePath = isRealmBackupExits()
// check if backup exists or not
if realmBackUpFilePath.exists {
let config = Realm.Configuration(
fileURL: URL(fileURLWithPath: realmBackUpFilePath.path),
readOnly: true)
let realm = try? Realm(configuration: config)
guard let backupRealm = realm else { return }
//Get your realm Objects
let objects = backupRealm.objects(YourRealmObject.self)
try? restoredRealm?.safeWrite {
for object in objects {
// taking local changes to the downloaded realm if it has
restoredRealm?.create(YourRealmObject.self, value: object, update: .modified)
}
self.removeRealmFiles(path: realmBackUpFilePath.path)
}
} else {
debug("backup realm does not exists")
}
}
private func isRealmBackupExits() -> (exists: Bool, path: String) {
let documentsPath = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let realmPathComponent = documentsPath.appendingPathComponent("your_backup.realm")
let filePath = realmPathComponent.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
return (true, filePath)
}
return (false, "")
}
private func removeRealmFiles(path: String) {
let realmURL = URL(fileURLWithPath: path)
let realmURLs = [
realmURL,
realmURL.appendingPathExtension("lock"),
realmURL.appendingPathExtension("realm"),
realmURL.appendingPathExtension("management")
]
for URL in realmURLs {
do {
try FileManager.default.removeItem(at: URL)
} catch {
debug("error while deleting realm urls")
}
}
}```
In our testing we have found that there is a backup made by realm automatically so we deleted it for safety purpose. the path argument you will get in the if let (path, clientResetToken) = syncError.clientResetInfo()
func removeAutoGeneratedRealmBackUp(path: String) {
do {
try FileManager.default.removeItem(at: URL(fileURLWithPath: path))
} catch {
debug("error while deleting realm backUp path \(path)")
}
}

How to get ObjectID and search for specific ObjectID in CoreData in Swift 5?

I am currently working on a project with a multi user system. The user is able to create new profiles which are saved persistently using CoreData.
My problem is: Only one profile can be the active one at a single time, so I would like to get the ObjectID of the created profile and save it to UserDefaults.
Further I was thinking that as soon as I need the data of the active profile, I can simply get the ObjectID from UserDefaults and execute a READ - Request which only gives me back the result with that specific ObjectID.
My code so far for SAVING THE DATA:
// 1. Create new profile entry to the context.
let newProfile = Profiles(context: context)
newProfile.idProfileImage = idProfileImage
newProfile.timeCreated = Date()
newProfile.gender = gender
newProfile.name = name
newProfile.age = age
newProfile.weight = weight
// 2. Save the Object ID to User Defaults for "activeUser".
// ???????????????????
// ???????????????????
// 3. Try to save the new profile by saving the context to the persistent container.
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
My code so far for READING THE DATA
// 1. Creates an request that is just pulling all the data.
let request: NSFetchRequest<Profiles> = Profiles.fetchRequest()
// 2. Try to fetch the request, can throw an error.
do {
let result = try context.fetch(request)
} catch {
print("Error reading data \(error)")
}
As you can see, I haven't been able to implement Part 2 of the first code block. The new profile gets saved but the ObjectID isn't saved to UserDefaults.
Also Party 1 of the second code block is not the final goal. The request just gives you back all the data of that entity, not only the one with the ObjectID I stored in User Defaults.
I hope you guys have an idea on how to solve this problem.
Thanks for your help in advance guys!
Since NSManagedObjectID does not conform to one of the types handled by UserDefaults, you'll have to use another way to represent the object id. Luckily, NSManagedObjectID has a uriRepresentation() that returns a URL, which can be stored in UserDefaults.
Assuming you are using a NSPersistentContainer, here's an extension that will handle the storage and retrieval of a active user Profile:
extension NSPersistentContainer {
private var managedObjectIDKey: String {
return "ActiveUserObjectID"
}
var activeUser: Profile? {
get {
guard let url = UserDefaults.standard.url(forKey: managedObjectIDKey) else {
return nil
}
guard let managedObjectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else {
return nil
}
return viewContext.object(with: managedObjectID) as? Profile
}
set {
guard let newValue = newValue else {
UserDefaults.standard.removeObject(forKey: managedObjectIDKey)
return
}
UserDefaults.standard.set(newValue.objectID.uriRepresentation(), forKey: managedObjectIDKey)
}
}
}
This uses a method on NSPersistentStoreCoordinator to construct a NSManagedObjectID from a URI representation.

In swift, is there a way to add new data from second Realm bundle, without overwriting any existing data in the current Default Realm?

Upon initial load of the app, the Bundled Realm (Realm1) is copied to the documents folder. Now that the bundled realm is set as the default realm, I am able update the bool property so that the table view can show marked and unmarked cells. However I am looking for a way to bundle a second realm (Realm2) with a later update, that will add new data to the existing default realm, but without overwriting the current default realm. I am currently working in swift 5 and Xcode 11.1, if that is helpful.
So far the only thing that I can think of is adding block of code to add new entries to the default realm. First the view will check to see what the count is of the realm, and if the count is the same as the original bundle, then it will add new data, if the count is equal to the initial bundle plus the new entries, then it will not add the new data again. I was hoping for a simpler solution that is cleaner in my opinion.
Ideally the end result would be a way to update the existing default realm, without overwriting the already edited content. Although I am rather new to using realm, any help in pointing me in the right direction for a solution would be greatly appreciated. Thanks.
Attached below is the current code I have implemented to load the default realm from the bundle.
let bundlePath = Bundle.main.path(forResource: "preloadedData", ofType: "realm")!
let defaultPath = Realm.Configuration.defaultConfiguration.fileURL!.path
let fileManager = FileManager.default
// Copy Realm on initial launch
if !fileManager.fileExists(atPath: defaultPath){
do {
try fileManager.copyItem(atPath: bundlePath, toPath: defaultPath)
print("Realm was copied")
} catch {
print("Realm was not coppied \(error)")
}
}
return true
Once you've created your default Realm on disk, if you want to read data from the bundled one, here's the code
let config = Realm.Configuration(
// Get the URL to the bundled file
fileURL: Bundle.main.url(forResource: "MyBundledData", withExtension: "realm"),
// Open the file in read-only mode as application bundles are not writeable
readOnly: true)
let realm = try! Realm(configuration: config)
and once you've read the data, you can switch back
var config = Realm.Configuration()
config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(some_realm_name).realm")
Realm.Configuration.defaultConfiguration = config
as far as not overwriting, ensure your objects use a unique primary key and when they are written, nothing will be overwritten as objects will a unique primary key will be added instead of overwriting.
class MyClass: Object {
#objc dynamic var my_primary_id = NSUUID().uuidString
I am adding an additional answer that's somewhat related to the first but also stands on it's own.
In a nutshell, once Realm connects to a data source, it will continue to use that data source as long as the objects are not released, even if the actual file is deleted.
The way around that is to encapsulate the Realm calls into an autorelease pool so that those objects can be released when the Realm is deleted.
Here’s an example:
This function adds a GameData object to the default.realm file.
func addAnObject() {
autoreleasepool {
let realm = try! Realm()
let testData = GameData()
testData.Scenario = "This is my scenario"
testData.Id = 1
try! realm.write {
realm.add(testData)
}
}
}
At this point, if you run the addAnObject code, your file will have a GameData object.
GameData {
Id = 1;
GameDate = (null);
Scenario = This is my scenario;
GameStarted = 0;
}
Then a function that delete’s the old realm, and copies the bundled realm to it’s place. This works because all of the interaction with Realm was enclosed in an autorelease pool so the objects can be released.
func createDefaultRealm() {
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let defaultParentURL = defaultURL.deletingLastPathComponent()
if let bundledRealmURL = self.bundleURL("default") {
do {
try FileManager.default.removeItem(at: defaultURL)
try FileManager.default.copyItem(at: bundledRealmURL, to: defaultURL)
} catch let error as NSError {
print(error.localizedDescription)
return
}
}
let migrationBlock : MigrationBlock = { migration, oldSchemaVersion in
//handle migration
}
Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 18, migrationBlock: migrationBlock)
print("Your default realm objects: \(try! Realm().objects(GameData.self))")
}
func bundleURL(_ name: String) -> URL? {
return Bundle.main.url(forResource: name, withExtension: "realm")
}
and please note that if you access Realm inside the class but outside an autorelease pool, Realm will refuse to 'let go' of it's objects.
Do do NOT do this!!
class ViewController: UIViewController {
var realm = try! Realm()

Realm configuration returns random schemaVersions, causing migration to fail

We are currently using Realm inside our app, but when I try to perform a migration (because we want to delete a Model/class in our database), the Configuration returns some random huge value from the configuration.schemaVersion.
The migration isn't called, and nothing is deleted. The Realm database was called multiple times with let realm = try? Realm(configuration: Realm.compactConfiguration)
I tried to make one Config throughout the app and set it like the following:
let configuration = Realm.compactingConfiguration
Realm.Configuration.defaultConfiguration = configuration
but the large schemaVersion still appears, and the code doesn't go inside the migrationBlock
extension Realm {
public static var compactConfiguration: Configuration {
get {
// Realm is compacted on the first open if the configuration block conditions were met.
// Compacting when size is greater than 50MB (arbitrary amount, database size should be
// around 2.6MB, so should not reach 50MB)
let currentSchemaVersion: UInt64 = 1
var configuration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < currentSchemaVersion) {
migration.deleteData(forType: Office.className())
}
})
configuration.deleteRealmIfMigrationNeeded = true
return configuration
}
}
}
Does anyone know what is going on? I expected the versionScheme to be 0 since it was never set before.

Resources