I'm creating an iOS application, where I intend to provide data sync across device feature, only to the premium users. I find Realm Sync as a good solution to keep the local on-device database and cloud MongoDB Atlas in sync. However, I don't want to sync the data of the non-premium users to the cloud database.
I'm enlisting a couple of ways that I can think of to prevent Realm Sync from triggering for non-premium users, but I'm not sure on what is the best way for this problem.
Prevent syncing by leveraging Sync permissions - I can store list of premium user ids and only give sync permissions to those users.
{
"%%user.id": [
"5f4863e4d49bd2191ff1e623",
"5f48640dd49bd2191ff1e624",
"5f486417d49bd2191ff1e625"
]
}
Configure Realm objects on client side i.e. only allow all Realm objects / models if the user is premium.
// Get a configuration to open the synced realm.
var configuration = user.configuration(partitionValue: "user=\(user.id)")
// For non-premium user it would be [User.self]
configuration.objectTypes = [User.self, Project.self]
Realm.asyncOpen(configuration: configuration) { [weak self](result) in /*...*/ }
I'm looking for insights / recommended approach to this problem.
Edit
I've a few additional questions about handling two use cases differently - non-premium one by opening a local only Realm() and the premium one with Realm.asyncOpen().
How to handle a use case when an existing user switches to a premium subscription? Should calling Realm.asyncOpen() suffice or do I need to do any special handling?
I plan to sync all my User (custom document in a collection) records for all users (premium + non-premium). My guess is I should open a normal Realm for all my conent and synced Realm with just [User.self] object in the configuration.
This is super easy to do!
When you only want to work with a local realm, connect to it with no config - like this
let realm = try! Realm()
let someObject = realm.results(SomeObject.self)
or a config that maybe contains a local file name. All of the app data will only be read and written locally with no sync'ing.
When you want to use MongoDB Realm Sync, connect to it like this
let app = App(id: YOUR_REALM_APP_ID)
// Log in...
let user = app.currentUser
let partitionValue = "some partition value"
var configuration = user!.configuration(partitionValue: partitionValue)
Realm.asyncOpen(configuration: configuration) { result in
switch result {
case .failure(let error):
print("Failed to open realm: \(error.localizedDescription)")
// handle error
case .success(let realm):
print("Successfully opened realm: \(realm)")
// Use realm
}
}
and then later with a config
let config = user?.configuration(partitionValue: "some partition")
let realm = try! Realm(configuration: config)
EDIT
Answering the two followup question:
How to handle a use case when an existing user switches to a premium
subscription? Should calling Realm.asyncOpen() suffice or do I need to
do any special handling?
Connecting to MongoDB Realm with the Sync'ding solution will add additional files and start syncing. If this is a new user that's 'premium', theres nothing else to do, other than (initially) ensure your objects are correctly structured with _id and partitionKey properties.
If this user is upgrading from a non-premium local only to a premium that's sync'd you will need to copy your realm objects from the local only realm to a sync'd realm.
There are several ways to to that; probably the simplist is to include code in your app then when upgrading, connects to a sync realm (using .async), then connects to your existing local realm and finally iterate over the objects to copy to the sync'd realm.
Another option is to export the the realm objects as JSON and then write them to the server directly. The next time your app connects with .async, it will force a client reset and download and create the locally sync'd files. There are some tidbits of information that may help with this particular process in the Realm Legacy Migration Guide
I plan to sync all my User (custom document in a collection) records
for all users (premium + non-premium). My guess is I should open a
normal Realm for all my conent and synced Realm with just [User.self]
object in the configuration.
Non-premium users don't sync so they are not really 'users' as such. You wouldn't need to store them or sync them so you really don't need any authentication or store any data on the server - it's just a locally run and used app so there isn't even a 'user' object to worry about. You will need to do that once they upgrade.
Related
Is it possible to load data from app's local database in app extension?
I have one requirement to use app's local database in app extension.
Is it possible? if yes then give some sources.
The only way you can share data between the main app and the extension is through userDefaults with suiteName by using app groups , so if you can write all your data from database to defaults then it'll be shared , but the problem is that it's not a recommended way as always defaults holds configuration data , also extension must be launched first so app can read data from it
You have to use Application group identifier, the you can share data with the NSUserDefaults. https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html
You can use the same Application group identifier to acces a shared directory:
NSURL *groupURL = [[NSFileManager defaultManager]
containerURLForSecurityApplicationGroupIdentifier:
#"com.yourapp.domain"];
Extension can use the network to fetch data.
You can share everything between a host app and its app extensions:
Keychain via keychain sharing
UserDefaults via app groups
Files via app groups
Database via SQLite file and app groups
To share a database between host app and extension you simply put the database file into the shared storage of the app group. This way, any change in the database of the host app is instantly available in the database of the extension and vice versa. With the CoreStore framework it is very easy to set up a database in the app group storage:
import CoreStore
let dataStack: DataStack = {
let dataStack = DataStack(xcodeModelName: "App")
let storagePathUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.company.App")!.appendingPathComponent("App.sqlite")
do {
try dataStack.addStorageAndWait(SQLiteStore(fileURL: storagePathUrl, configuration: "Default", localStorageOptions: .
recreateStoreOnModelMismatch))
} catch let error {
print("Cannot set up database storage: \(error)")
}
return dataStack
}()
An example to insert a user object (User must be definded in your App.xcdatamodeld) into the database would then be:
dataStack.perform(asynchronous: { transaction in
let user = transaction.create(Into<User>())
user.email = "user#test.com"
user.name = "test"
}, completion: { _ in })
Please note that you must handle locking by yourself if host app and extension run at the same time and write to the database at the same time.
So, I have been coming across a problem where my Firebase app does not update user values when a user makes an update. To be more clear: Lets say user 1 has a photo of a dog and then changes it to a cat.
Once they change it to a cat, my node value in Firebase is successfully updated but the user themselves won't be able to see the change in other previously loaded areas in the app (other places with the dog picture) until they log out and then log back in.
For this reason I was wondering if there was any way to conduct a background app refresh that way all previous dog values in the app are changed to cat values without the user having to log out and then log back in. Please note that this same problem occurs not only with my user's profile picture but also any other user field I have setup.
Here is how I am updating a node value for my user in Firebase:
let storageRef = FIRStorage.storage().reference()
_ = FIRStorageMetadata()
let filePath = "\(FIRAuth.auth()?.currentUser?.uid)/\("userPhoto")"
let profileImageData = UIImageJPEGRepresentation(self.profilePicture.image!, 1.0)
if let data = profileImageData {
storageRef.child(filePath).put(data, metadata: nil){(metaData,error) in
if let error = error {
print(error.localizedDescription)
return
} else {
let downloadURL = metaData!.downloadURL()!.absoluteString
let userPhotoUpdateRef = FIRDatabase.database().reference().child("users").child(self.currentUser).child("userPhoto")
userPhotoUpdateRef.setValue(downloadURL)
}
}
}
If you have any questions please ask! Any help would be appreciated!
The Firebase SDK for Cloud Storage provides an easy way to read file from and write files to cloud storage. It does not provide a way to monitor those files.
The easiest way to provide a monitoring approach is to write the metadata of the files to the Firebase Realtime Database. See this short section in the Storage docs for a brief mention of that: https://firebase.google.com/docs/storage/ios/file-metadata#custom_metadata
When you write data to a location in the Firebase Database, all apps that are actively monitoring that location will be instantly updated. When they get that update, you can reload the image from Cloud Storage for Firebase.
I have created an Amazon Web Services EC2 instance and deploy one of the AMIs with a Realm Object Server as its documentation explains:
https://realm.io/docs/realm-object-server/#install-realm-object-server
Once installed and created my admin user, I have completed the iOS tutorial: https://realm.io/docs/tutorials/realmtasks/, just until point 7, enough for creating task, but when I add new task in app, nothing happens. Debugging, I notice that next sentence try, is not executing:
let items = self.items
try! items.realm?.write {
items.insert(Task(value: ["text": text]), at: items.filter("completed = false").count)
}
The items collection seems to be initialized properly:
In the ROS dashboard, can see the database referenced in Xcode:
In image can be see "Default permissions" property is no access, is this the reason of not creating new task? If so, how can I change that permissions? If that is not the reason, anyone could help me?
thanks in advance
The problem was that I did not follow al the complete tutorial because I do not want to use the desktop application, just mobile example, but realm init objects in desktop app, so I never got a valid realm where perform actions.
For a quick and simple start with this realm tutorial pointing to an online server, not local, you must initialize the TaskList object and add it to self.realm on setup
// Show initial tasks
func updateList() {
if self.realm.objects(TaskList.self).count == 0 {
let list = TaskList()
list.id = "000001"
list.text = "lista de prueba"
// Add to the Realm inside a transaction
try! self.realm.write {
self.realm.add(list)
}
}
if self.items.realm == nil, let list = self.realm.objects(TaskList.self).first {
self.items = list.items
}
self.tableView.reloadData()
}
checking if there is not a TaskList with if self.realm.objects(TaskList.self).count == 0 {, you can create one and init realm.
You probably forgot to launch Mac demo app first or login with a different user. The tutorial assumes that existing data will be synced at login. If you have never launched the Mac app or logged in a different user, it may happen that items are not managed by Realm.
The tutorial says the following:
First, please follow the Get Started instructions to get set up with the Realm Mobile Platform and to launch the macOS version of RealmTasks.
Also, you attempt to try this tutorial with ROS on AWS. The tutorial assumes running ROS on a same local machine.
So you should modify the Mac app code to connect to the AWS, then run it to synchronize the initial data. Then run the tutorial iOS app.
The default permissions here show whether all other users can access the Realm or not, which isn't the case here. We already have an internal issue around clarifying this.
The registered user who owns the Realm has individual permissions to it by default. If you wouldn't have permissions opening the synchronized Realm from the client would also fail because of insufficient permissions, so this isn't the problem here.
So going back to your code:
try! items.realm?.write { … }
My guess would be that the problem here is that the collection isn't already attached to a Realm, so that items.realm? evaluates to null. In that case the write transaction wouldn't be executed.
You can resolve this by making sure to add the items first to a Realm or executing the write directly on a synchronized Realm.
I’m using Firebase with the offline ability set to true.
let ref = FIRDatabase.database().referenceWithPath(“my data”).child(“my users id”)
scoresRef.keepSynced(true)
This path also has keep Synced set to true, as with out this with changes in the database are not seen within the app immediately as it is using the local cache.
I have another top level node / path in my app that I want to search - containing other users.
I want to use a singleEvent query and find an email address, I’m doing this via
studios.queryOrderedByChild("email").queryEqualToValue(email).observeSingleEventOfType(.Value, withBlock: { snapshot in // etc
I am able to find the node, however I keep getting the local cached version and not the most recent one in the firebase online store.
If I make some changes to the node online, I don’t get these back within the fetch.
If I changed my fetch to a monitor type i.e.
studios.queryOrderedByChild("email").queryEqualToValue(email).observeEventType(.Value, withBlock: { snapshot in // etc
I get the local cache node first, then get the online updated version as well.
I would rather use the SingleEvent fetch, but I don’t want to monitor the users entire node with keepSynced as it is a high level node and I don’t want to keep all that data locally, as its not directly related to the user.
One fix I found was prior to the single query was add .keepSynced(true) and in the completion block add .keepSynced(false). I'm not sure how much of the node is downloaded this was and may as well use the monitor fetch rather than the singleEvent.
Should I just use the monitorEvent or is there a better way to use SingleEventFetch that goes to the online store and instead of just returning my local node.
PS I am online and this is confirmed via
var connectedRef = FIRDatabase.database().referenceWithPath(".info/connected")
connectedRef.observeEventType(.Value, withBlock: { snapshot in
let connected = snapshot.value as? Bool
if connected != nil && connected! {
println("Connected")
} else {
println("Not connected")
}
})
Thanks
My question is related to the "Delta Download" thing as it was named in WWDC 2014 Advanced CloudKit.
I'm trying to make syncronization for my Core Data app, which is iPhone only for now (think: there is only one device active). So, basically the app will store user records in the cloud from one same device, for the most cases for now.
I have trouble with understanding custom zone feature which is based on CKFetchRecordChangesOperation aka Delta Download.
As I got it right, we have CKServerChangeToken's to maintain sync operations (I mean download only those records which was added/modified/deleted by another device), as was presented on WWDC.
But, what I can't understand is that we recieve that token only after CKFetchRecordChangesOperation, when we save records to the cloud we don't get new token.
And if we make fetch with the current available token (since it changes only after fetch), we recieve records that was saved from our previous save operation. Basicaly we get save recods that already have on our device. Why? I'm missing something here?
What if we seeding some data to the cloud (from device A), it is justified for situation when device B is fetching the zone records, but what if device A be? Download all the records again?
I found recordChangeTag in the CKRecord, is this a property I can use for resolving conflicts with local objects - fetched objects (same or different version), if so can somebody give me example of how I need to do this: save recordChangeTag to Core Data when save record to CloudKit for the first time or how?
The lack of documentation is such a headache.
I found a time to write an answer for this question. I won't dig into implementation, but I will discuss the concept.
CloudKit provides a way to data synchronisation between your device and the CloudKit server.
What I use to establish synchronisation process in my case between iPhone and server only (again, if you have iPhone + iPad app, the process require more steps.):
I have custom zone in the private cloud database.
I use OperationQueue to establish different asynchronous processes which depend on each other. Some operations have own operation queues.
Steps:
1) Check if my custom zone is exist
1.1) If there is no custom zone
1.2) Create new custom zone. (Optional: add records)
1.3) Refresh zone change token
You can refresh zone change token by: performing
CKFetchRecordChangesOperation,
fetchRecordChangesCompletionBlock returns CKServerChangeToken
save it to UserDefaults (for example) using NSKeyedArchiver). This operation's task is to refresh token and it's performed at the end synchronisation process.
2) If there is custom zone already
2.1) Get changes from zone using previously saved zone change token. (CKFetchRecordChangesOperation)
2.2) Update and delete local records.
2.3) Refresh zone change token.
2.4) Check for local changes (I'm using last cloud sync timestamp to check what records was modified after).
2.5) Upload records to cloud kit database
2.6) Refresh zone change token again.
I highly recommend Nick Harris article series: https://nickharris.wordpress.com/2016/02/09/cloudkit-core-data-nsoperations-introduction/
You'll find there implementation and design concepts. It worth reading. I hope somebody'll find all of this helpful.
As of iOS 13 there is a super helpful method in Core Data called NSPersistentCloudKitContainer. This method will automatically take care of all local caching and syncing with iCloud on private databases. You can set it up by simply changing
static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "ShoeTrack")
container.loadPersistentStores(completionHandler: {
(storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
to
static var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "ShoeTrack")
container.loadPersistentStores(completionHandler: {
(storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
You will have to modify the Core Data Model file in your project and check "Use with CloudKit on each configuration.