Realm sync with Realm Object Server - swift

Pre-Condition : I have 10 Dogs stored in a Realm Server
Is there a way to know when the results are ready?
let usernameCredentials = SyncCredentials.usernamePassword(username: email, password: pass)
SyncUser.logIn(with: usernameCredentials,server: Utils.sharedInstance.serverURL) { user, error in
if error != nil {
// handle error
} else {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user!, realmURL: Utils.sharedInstance.syncServerURL))
let realm = try! Realm(configuration : config)
let dogs = realm.objects(Dog.self)
print("I have : \(dogs.count) dogs")
// dogs count is 0 here
// ..............
// some time later i have the 10 Dogs
}
}

Unfortunately not at the moment. Realm's sync APIs and semantics right now are best suited for incremental sync use cases, but we're working on two features that should help address the use case you've shared.
The first is a "Download Realm" API which only makes the Realm available once its entire contents have been downloaded.
The second is "sync progress notifications", where you can register a progress update block to fire with information about how much 1) local data needs to be synced up and 2) remote data needs to be synced down.

Related

Swift Realm issue in iOS 14+

------LE: We ended up removing the encryption of the database because with realm team suggestions it got worse - all we could do was to remove the database and loose all stored info. Now we encrypt in keychain only the fields we need.------
I have an app released in store and after updating their iOS version to 14+, users started to complain about info not being populated from database. Not all users with iOS 14+ have this issue, it appears randomly on some devices.
The issue goes away for awhile if they reinstall the app or after they update it to another version, but after using it for a few minutes it happens again.
My database uses encryption as documented here.
The store version of my app uses Realm 5.4.8, but I tested their last version (10.0.0) and the issue is still present.
I checked this issue but it's not the case for me, I don't have a shared app group container or a share extension.
Here's how the initialisation of realm looks like:
override init() {
super.init()
do {
guard let config = getMigrationAndEncryptionConfiguration() else {
realmConfigured = try Realm()
return
}
realmConfigured = try Realm(configuration: config)
} catch let error as NSError {
// this is where I got the error:
//"Encrypted interprocess sharing is currently unsupported.DB has been opened by pid: 4848. Current pid is 5806."
}
}
func getMigrationAndEncryptionConfiguration() -> Realm.Configuration? {
let currentSchemaVersion: UInt64 = 19
if Keychain.getData(for: .realmEncryptionKey) == nil {
var key = Data(count: 64)
_ = key.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 64, bytes)
}
Keychain.save(data: key, for: .realmEncryptionKey)
}
guard let key = Keychain.getData(for: .realmEncryptionKey) else {
return nil
}
let fileUrl = Realm.Configuration().fileURL!.deletingLastPathComponent()
.appendingPathComponent("Explorer.realm")
var config = Realm.Configuration(fileURL: fileUrl,
encryptionKey: key,
schemaVersion: currentSchemaVersion, migrationBlock: { (migration, oldVersion) in
if oldVersion != currentSchemaVersion {
print("we need migration!")
}
})
return config
}
I had another question opened for the same issue on SO, but it was closed because I didn't have enough details. After another release of my app with more logs, I could find the error that appears at initialisation of realm:
"Encrypted interprocess sharing is currently unsupported.DB has been opened by pid: 4848. Current pid is 5806. "
This appears after the app goes to background, it gets terminated (crash or closed by the system/user) and when the users opens it again, realm fails to init.
I read all about encrypted realm not being supported in app groups or in a share extension, but I didn't implement any of that in my app, is there any other reason why this error happens?
I also removed Firebase Performance from my app because I read that this module could generate issues on realm database, but it didn't help.
I opened an issue on realm github page, but I got no answer yet.
Does anyone have any idea how to fix this or why is this happening?
Thank you.

How to add data to Firestore Cloud without hardcoding user collection document id in Swift

Initially I created a collection Users in my Firestore Cloud database with a subcollection Wishlist.
To add to the wishlist, I hardcoded the following to test it works:
let db = Firestore.firestore()
db.collection("users/JldiJEK5i84DZWhlTFg6/wishlist").addDocument(data: ["plant" : plants[0], "image" : plants[1]])
}
Once button is tapped, the plant is sent to that particular User's wishlist and it works -- I can see this being added to the database.
How do I...
I cannot figure out in the documentation how not to hardcode the document id. I would like it to just add the plant for every user signed in?
Get the current user:
let user = Auth.auth.currentUser
if let user = user {
_ = user.id
}
Then:
let db = Firestore.firestore()
db.collection("users/\(user?.uid ?? "error")").addDocument(data: ["plant" : plants[0], "image" : plants[1]])
This works for me when dealing with people/customers, I just have an error document in Firestore, but your user is more than likely never going to be nil if they log in or create an account.
Edit:
Just to be safe, this is probably better and will avoid unnecessary writes to Firestore when there's an error (saving money)
guard let currentUser = Auth.auth().currentUser {
print("handle the error")
return
}
let uid = currentUser.uid
...

CloudKit Error: Change Token Expired, Reset Needed

Swift 3.1, Xcode 8.3.3
I keep getting an error from CloudKit and I don't know what to do about it.
I'm tracking notifications from CloudKit like this:
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: previousChangeToken)
//Hold the notification IDs we processed so we can tell CloudKit to never send them to us again
var notificationIDs = [CKNotificationID]()
operation.notificationChangedBlock = { [weak self] notification in
guard let notification = notification as? CKQueryNotification else { return }
if let id = notification.notificationID {
notificationIDs.append(id)
}
}
operation.fetchNotificationChangesCompletionBlock = { [weak self] newToken, error in
if let error = error{
print(error) //<-- <!> This is the error <!>
}else{
self?.previousChangeToken = newToken
//All records are in, now save the data locally
let fetchOperation = CKFetchRecordsOperation(recordIDs: recordIDs)
fetchOperation.fetchRecordsCompletionBlock = { [weak self] records, error in
if let e = error {
print("fetchRecordsCompletionBlock Error fetching: \(e)")
}
//Save records to local persistence...
}
self?.privateDB.add(fetchOperation)
//Tell CloudKit we've read the notifications
let operationRead = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDs)
self?.container.add(operationRead)
}
}
container.add(operation)
And the error says:
<CKError 0x174241e90: "Change Token Expired" (21/1016); server message
= "Error code: RESET_NEEDED"; uuid = ...; container ID = "...">
The CKServerChangeToken documentation don't mention anything about resetting the token, and the CloudKit dashboard doesn't offer any such option.
Any idea what I'm supposed to do?
This error code is CKErrorCodeChangeTokenExpired, and it's an indication that you need to re-sync your changes.
https://developer.apple.com/documentation/cloudkit/ckerror/2325216-changetokenexpired
This error code gets returned when the change token is too old, or the container has been reset (resetting the container invalidates old change tokens).
The comments related to this error code include:
(Describing the code itself):
The previousServerChangeToken value is too old and the client must re-sync from scratch
(On various fetch operation completion/updated blocks):
If the server returns a CKErrorChangeTokenExpired error, the serverChangeToken used for this record zone when initting this operation was too old and the client should toss its local cache and re-fetch the changes in this record zone starting with a nil serverChangeToken.

SyncCredentials Don't exist in Realm Swift

Im trying to connect my app to a Realm Object Server. The documentation says, to do this you use the below:
// create a configuration object
let realmUrl = URL(string: "realms://example.com:9000/~/settings")!
let realmUser = SyncCredentials.usernamePassword(username: username, password: password)
let config = Realm.Configuration(user: realmUser, realmURL: realmUrl)
// open the Realm with the configuration object
let settingsRealm = try! Realm(configuration: config)
However for SyncCredentials.usernamePassword, XCode says SyncCredentials doesn't exist. From the looks you need to set SyncConfiguration on in Realm.configuration (or the only file I can find RealmConfiguration.swift)
Now I'm in that file theres no option to use SyncCredentials
My question is, how do I simply connect my app with a Realm Object Database using SyncCredentials (or however you're supposed to do it). Been pulling my hair out all day over this, surely it can't be that hard :-(
There seem some mistakes.
SyncCredentials is auth info that is used to log in. It is not user object. Realm.Configuration doesn't receive user and realmURL parameters in the initializer. You need to use SyncConfiguration instead.
The example code for logging in or instantiating Realm with existing user is the following.
let syncServerURL = URL(string: "realm://example.com:9080/~/settings")!
let syncAuthURL = URL(string: "http://example.com:9080")!
if let user = SyncUser.current {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
let realm = try! Realm(configuration: config)
...
} else {
let username = ...
let password = ...
let credentials = SyncCredentials.usernamePassword(username: username, password: password)
SyncUser.logIn(with: credentials, server: syncAuthURL) { user, error in
DispatchQueue.main.async {
if let user = user {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
let realm = try! Realm(configuration: config)
...
} else {
// Handle error
}
}
}
}
Please read our documentation again https://realm.io/docs/swift/latest/#sync
and also our RealmTasks sample project helps you to understand interacting Realm Object Server. https://github.com/realm-demos/realm-tasks
If you still see SyncCredentials not found error in above code, probably you didn't setup RealmSwift framework correctly. Please add more info that how did you set up Realm.
Swift Package Manager doesn't load SyncCredentials or SyncUser properly.
I was using the SPM to install RealmSwift as a dependency from IceCream. I removed Realm and IceCream from SPM. Then installed using Carthage to solve the issue.

Receipt validation iOS - The file could not be opened because there is no such file

I'm working on a subscription IAP. I set all purchases up, their details come back well, I can do the purchases in sandbox and get all the messages alright. The problem I have now is checking the receipt. I always get a URL returned alright, but when I try to read it I keep getting error that the file does not exist. So I try and refresh with SKReceiptRefreshRequest. Try again, still same.
I have uninstalled app on simulator and two real devices, try again from new install and same problem. One thing I realised, one of the real devices displays the password prompt request with [Sandbox] mention. However after two prompts (including accepting password), instead of purchase completed I get a "user/password don't match" message. On simulator when prompted for itunes account and password it all goes through but the actual purchase confirmation never comes (I waited 4 minutes, stable internet connection).
This is the validation process (I have changed it quite a few times, from different tutorials and other people's problems)
let receiptURL = Bundle.main.appStoreReceiptURL
func receiptValidation() {
print("1")
print("2", receiptURL)
do {
print("3")
let receiptData = try Data(contentsOf: receiptURL!, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
let dict = ["receipt-data" : receiptString, "password" : "\(password)"] as [String : Any]
do {
print("4")
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
if let sandboxURL = Foundation.URL(string:"https://sandbox.itunes.apple.com/verifyReceipt") {
print("5")
var request = URLRequest(url: sandboxURL)
request.httpMethod = "POST"
request.httpBody = jsonData
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: request) { data, response, error in
print("6")
if let receivedData = data,
let httpResponse = response as? HTTPURLResponse,
error == nil,
httpResponse.statusCode == 200 {
print("7")
do {
print("8")
if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject> {
print(jsonResponse, jsonResponse.count)
// parse and verify the required informatin in the jsonResponse
} else { print("Failed to cast serialized JSON to Dictionary<String, AnyObject>") }
}
catch { print("Couldn't serialize JSON with error: " + error.localizedDescription) }
}
}
print("51")
task.resume()
} else { print("Couldn't convert string into URL. Check for special characters.") }
}
catch { print("Couldn't create JSON with error: " + error.localizedDescription) }
}
catch {
let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
appReceiptRefreshRequest.delegate = self
appReceiptRefreshRequest.start()
print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
func requestDidFinish(_ request: SKRequest) {
print("???")
do {
let receipt = try Data(contentsOf: receiptURL!) //force unwrap is safe here, control can't land here if receiptURL is nil
print(receipt)
} catch {
print("WTF NO RECEIPT")
// still no receipt, possible but unlikely to occur since this is the "success" delegate method
}
}
And this is the Debugging output from running the app. receiptURL varies between simulator/real device, but other then that everything remains the same.
1
2 Optional(file:///Users/apple/Library/Developer/CoreSimulator/Devices/47EA3293-9B13-4808-BD0B-13D884D14BFE/data/Containers/Data/Application/2F1B7E4E-C523-4270-BF46-6D77F7A2220C/StoreKit/receipt)
3
Couldn't read receipt data with error: The file “receipt” couldn’t be opened because there is no such file.
???
WTF NO RECEIPT
???
WTF NO RECEIPT
Why can't I get the receipt created, or found? Is it a device problem, a bug or am I oblivious to something?
The whole IAP process works asynchronously due to which you will not receive the receipt data unless the whole process has been completed successfully. I can't see the whole code based on what you have pasted in your question above but if you are trying to access the receipt data immediately on the action of a button or something similar, you will not get it.
The correct way to access receipt data is to try accessing the receipt based on success completion handler callback of your IAP request. Once you submit the IAP request there is a server side process which takes care of processing the IAP and then a callback handler from IAP SKPaymentTransactionObserver class is triggered. Using the notification handler from this class you can send the update to your ViewController to check for receipt data.
While further researching, I have found the following article, which solved the problem
article
Important: If you mistakenly use a sandbox tester account to log in to a production environment on your test device instead of your test environment, the sandbox account becomes invalid and can’t be used again. If this happens, create a new sandbox tester account with a new email address.