How to handle optional Realm sync? - swift

I have an app with local/offline Realm files/databases. I would like to add the option to log in to sync data across devices, and I figured Realm Object Server would be great for this. I know that when a user logs in, I will need to write a migration from a local realm to a synced realm. What do I do if a user decides to logout? What do I do if a user never logs in in the first place? Do I need to have two paths in my code (everywhere) to choose whether it should use the local or synced realm?

I use a RealmProvider class to get the realm instance.
class RealmProvider {
class func realm() -> Realm {
if let _ = NSClassFromString("XCTest") {
return try! Realm(configuration: Realm.Configuration(fileURL: nil, inMemoryIdentifier: "test", syncConfiguration: nil, encryptionKey: nil, readOnly: false, schemaVersion: 0, migrationBlock: nil, deleteRealmIfMigrationNeeded: true, objectTypes: nil))
} else {
let user = getUser()
guard let loggedInUser = user else {
do {
return try Realm()
} catch {
print(error)
}
return try! Realm()
}
// open up synced url to the username.
if let user = SyncUser.current {
let syncServerURL = URL(string: "realm://realm.myapp.com/~/\(loggedInUser.username.md5())")!
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
// Open the remote Realm
return try! Realm(configuration: config)
} else {
// login first.
let serverURL = URL(string: "http://realm.myapp.com")!
let token = getToken()
let customCredentials = SyncCredentials(customToken: token, provider: Provider("custom/myAuth"))
//
SyncUser.logIn(with: customCredentials,
server: serverURL) { user, _ in
if let user = user {
// can now open a synchronized Realm with this user
// Open Realm
let configuration = Realm.Configuration(
syncConfiguration: SyncConfiguration(user: user, realmURL: URL(string: "realm://realm.myapp.com/~/\(loggedInUser.username.md5())")!)
)
_ = try! Realm(configuration: configuration)
}
}
}
return try! Realm()
}
}
}
To answer your question,
What do I do if a user decides to logout?
The RealmProvider will give you a local realm file.
What do I do if a user never logs in in the first place?
The data will still be stored in a local realm file, once the user logs in, you can open up a synced realm and run the migration.
PS: There is one catch, if the synced realm is being opened the first time, it will still return the local realm file. :/

Related

user does not have permission to access gs:// xxx.appspot.com

In this application I created authencation, database with firebase. but in my post, which consists of title, content, and image, I only see my default image. I'm having a bit of a problem with storage in the database, first uploading the image to firebase storage and then taking its url.
import Foundation
import SwiftUI
import Firebase
import FirebaseStorage
class StorageStore: ObservableObject {
let storageRef = Storage.storage().reference()
func uploadImage(_ image: UIImage, completion: #escaping (URL?) -> Void) {
let imageRef = storageRef.child("images/"+timeString()+".jpg")
guard let imageData = image.jpegData(compressionQuality: 0.1) else {
return completion(nil)
}
let metadata = StorageMetadata()
metadata.contentType = "image/jpg"
imageRef.putData(imageData, metadata: metadata, completion: { [self] (metadata, error) in
if let error = error {
assertionFailure(error.localizedDescription) // 🛑
//Thread 1: Fatal error: User does not have permission to access gs://ios-post-193ec.appspot.com/images/2022-03-29T10:03:18Z.jpg.
return completion(nil)
}
imageRef.downloadURL(completion: { (url, error) in
if let error = error {
assertionFailure(error.localizedDescription)
return completion(nil)
}
completion(url)
})
})
}
func timeString() -> String {
let now = Date()
let formatter = ISO8601DateFormatter()
let datetime = formatter.string(from: now)
print(datetime)
return datetime
}
}
as a result, I can't upload the image to storage
In this case, I have an error (🛑) as shown above. instead of the default image, one of the 6 permanent images that would be in the simulator had to come out.
The error message shows that the user does not have permission to access the file. So it looks like you have security rules controlling who can access the files in your Cloud Storage through Firebase, and those rules reject this read operation. I recommend checking the documentation I linked (there's also an old, but still great, video in there) to learn how to allow the operation, while still keeping the files secure.

Thread 1: Exception: "Realm accessed from incorrect thread."

I have two views. The first one shows a list of the custom objects made of downloaded data, the second shows a list of objects that are the basis for objects from the first list.
If I choose an object in the second view to save in Realm and go back to the first one, the data to make a list of custom objects is downloaded from database. If I want to delete that object the app crash and this message appears:
Thread 1: Exception: "Realm accessed from incorrect thread."
The same situation is when I delete one, two or more objects in the first screen, go to another one, choose one, two, or more to save in the database and go back to the first one, where data is downloaded from database. App is crashing and same message.
I know it's all about threads, but I don't know how to resolve that.
I tried resolving that by DispatchQueue, but it doesn't work, or i'm doing it wrong. How to resolve this thread problem in my case?
These database functions are using in the first view:
func deleteAddItem(addItem: AddItem) throws {
do {
let realm = try! Realm()
try! realm.write {
if let itemToDelete = realm.object(ofType: AddItem.self, forPrimaryKey: addItem.id) {
realm.delete(itemToDelete)
realm.refresh()
}
}
}
}
}
func fetchStations() throws -> Results<Station> {
do {
realm = try Realm()
return realm!.objects(Station.self)
}
catch {
throw RuntimeError.NoRealmSet
}
}
func fetchSensors() throws -> Results<Sensor> {
do {
realm = try Realm()
return realm!.objects(Sensor.self)
}
catch {
throw RuntimeError.NoRealmSet
}
}
func fetchAddItems() throws -> Results<AddItem> {
do {
realm = try Realm()
return realm!.objects(AddItem.self)
}
catch {
throw RuntimeError.NoRealmSet
}
}
func fetchData() throws -> Results<Data> {
do {
realm = try Realm()
return realm!.objects(Data.self)
}
catch {
throw RuntimeError.NoRealmSet
}
}
If you want more code or information, please let me know.
It appears you have two different Realm threads going
func deleteAddItem(addItem: AddItem) throws {
do {
let realm = try! Realm() <- One realm thread, a local 'let'
and then
func fetchAddItems() throws -> Results<AddItem> {
do {
realm = try Realm() <- A different realm thread, a class var?
that can probably be fixed by using the same realm when deleting
func deleteAddItem(addItem: AddItem) throws {
do {
realm = try! Realm() <- references the same realm as in delete
There are few options to prevent this. Once is simply get the realm, every time you want to use it
let realm = try! Realm()
realm.read or realm.write etc
or create a singleton function (or a RealmService class) that can be accessed throughout the app. In a separate file (or whever you want to put it and this is just a quick example)
import RealmSwift
func gGetRealm() -> Realm? {
do {
let realm = try Realm()
return realm
} catch let error as NSError { //lots of error handling!
print("Error!")
print(" " + error.localizedDescription)
let err = error.code
print(err)
let t = type(of: err)
print(t)
return nil
}
}
Then to use it
if let realm = gGetRealm() {
realm.read, realm.write etc
}
Also, I noticed you appear to be getting an item from realm to just then delete it. That's not necessary.
If the item is already managed by Realm, it can just be deleted directly. Here's an updated delete function
func deleteItem(whichItem: theItemToDelete) {
if let realm = gGetRealm() {
try! realm.write {
realm.delete(theItemToDelete)
}
}
}

How to backup realm db to iCloud with Swift?

I am trying to implement a function to backup realm db to iCloud
https://medium.com/swlh/backup-your-applications-data-with-icloud-and-track-progress-48a00ebd2891
I finished the setting by referring to the site above.
And I entered the code according to the method of the site, but with no success.
So I tried again like below.
Still I can't see the file in the app in settings.
Is there something wrong?
And I am curious about how to restore to realm db in iCloud.
Thank you
func uploadDatabaseToCloudDrive()
{
let fileManager = FileManager.default
//let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents", isDirectory: true)
let iCloudDocumentToCheckURL = iCloudDocumentsURL.appendingPathComponent("default.realm")
let realmArchiveURL = iCloudDocumentToCheckURL
if(fileManager.fileExists(atPath: iCloudDocumentsURL.path ))
{
do
{
try fileManager.removeItem(at: realmArchiveURL)
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL)
}catch
{
print("ERR")
}
}
else
{
print("Need to store ")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL)
}
}

Converting local realms to synced realm

I want to convert my local realm database to a synced one to allow user authentication. I'm using swift and it wasn't included in the documentation, however, I found this method but it keeps giving SIGABRT exception and I don't know what seems to be the problem.
Here's what I added in the App Delegate:
import UIKit
import RealmSwift
import Realm
import Realm.Dynamic
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let sourceFilePath = Bundle.main.url(forResource: "fieldFlow", withExtension: "realm")
let configuration = RLMRealmConfiguration()
configuration.fileURL = sourceFilePath
configuration.dynamic = true
configuration.readOnly = true
let localRealm = try! RLMRealm(configuration: configuration)
let creds = SyncCredentials.usernamePassword(username: "admin#realm.io", password: "password")
SyncUser.logIn(with: creds, server: URL(string: "http://localhost:9080")!) { (syncUser, error) in
DispatchQueue.main.async {
if let syncUser = syncUser {
self.copyToSyncRealmWithRealm(realm: localRealm, user: syncUser)
}
}
}
let config = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
schemaVersion: 1,
// Set the block which will be called automatically when opening a Realm with
// a schema version lower than the one set above
migrationBlock: { migration, oldSchemaVersion in
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// Nothing to do!
// Realm will automatically detect new properties and removed properties
// And will update the schema on disk automatically
}
})
// Tell Realm to use this new configuration object for the default Realm
Realm.Configuration.defaultConfiguration = config
// Now that we've told Realm how to handle the schema change, opening the file
// will automatically perform the migration
let realm = try! Realm()
// Override point for customization after application launch.
return true
}
func copyToSyncRealmWithRealm(realm: RLMRealm, user: RLMSyncUser) {
let syncConfig = RLMRealmConfiguration()
syncConfig.syncConfiguration = RLMSyncConfiguration(user: user, realmURL: URL(string: "realm://localhost:9080/~/fieldRow")!)
syncConfig.customSchema = realm.schema
let syncRealm = try! RLMRealm(configuration: syncConfig)
syncRealm.schema = syncConfig.customSchema!
try! syncRealm.transaction {
let objectSchema = syncConfig.customSchema!.objectSchema
for schema in objectSchema {
let allObjects = realm.allObjects(schema.className)
for i in 0..<allObjects.count {
let object = allObjects[i]
RLMCreateObjectInRealmWithValue(syncRealm, schema.className, object, true)
}
}
}
}

Realm Object Server. Sync initial Local DB

I have a local Realm database, full of 160000 row. I want to copy it to the local path for Realm to be able to use it as official DB, and sync it online. (so my empty Db will be synced). Can I do that? (at this time, doesn't work because it copy the Db in local folder, but not in the user specific folder)
func loginCompletedA(user: SyncUser) {
let realmURL = URL(string: “realm://xxx.compute-1.amazonaws.com:9080/beerUsers”)!
var configuration = Realm.Configuration.defaultConfiguration
configuration.syncConfiguration = SyncConfiguration(user: user, realmURL: realmURL)
let defaultURL = configuration.fileURL!
//let defaultParentURL = defaultURL.deletingLastPathComponent()
if let v0URL = Bundle.main.url(forResource: “default”, withExtension: “realm”){
do {
//if !ifNotExists {
try FileManager.default.removeItem(at: defaultURL)
//}
try FileManager.default.copyItem(at: v0URL, to: defaultURL)
} catch {}
do {
try FileManager.default.copyItem(at: v0URL, to: defaultURL)
} catch {}
}else{
}
let realm = try! Realm(configuration: configuration)
let session = user.session(for: realmURL)!
downloadToken = session.addProgressNotification(for: .download, mode: .reportIndefinitely) {
print(“download progress: \($0)“) // This is never called.
}
uploadToken = session.addProgressNotification(for: .upload, mode: .reportIndefinitely) {
print(“upload progress: \($0)“) // This is never called.
}
}
Just to confirm what I think you're asking. You're pre-bundling a Realm database file, containing 160,000 rows of data along with your app. When a new user logs into the app, the data is then synchronized with their account.
Unsynchronized Realm files and synchronized Realm files are two different file formats, so it's not possible to convert one file to another. Copying a pre-bundled offline Realm to a user-controlled directory and then trying to apply a syncConfiguration object won't do anything.
The easiest solution to this is to make a new synchronized Realm, and then copy the data from the pre-bundled Realm into the synchronized Realm the first time the app launches.
let bundledRealmURL = Bundle.main.url(forResource: “default”, withExtension: “realm”)!
let localConfiguration = Realm.Configuration()
configuration.readOnly = true
configuration.fileURL = bundledRealmURL
let localRealm = try! Realm(configuration: configuration)
let syncConfiguration = Realm.Configuration()
syncConfiguration.syncConfiguration = SyncConfiguration(user: user, realmURL: realmURL)
let syncRealm = try! Realm(configuration: configuration)
let myObjects = localRealm.objects(MyObject.self)
try! syncRealm.write {
for myObject in myObjects {
let newObject = MyObject(value: myObject)
syncRealm.add(newObject)
}
}
We're exploring ways to make it easier to 'prefill' synchronized Realms for a future release of Realm. :)