I have a synchronized realm running on Realm Object Server. It is a global realm created by the admin user. I created another user on the server (not an admin) and used the admin user to grant read-only permissions to that user for that particular realm.
I can see the permission when I query the admin's management realm and the normal user's permission realm. However, when I connect to the global realm from my iOS app (Swift 3) with the normal user, the server returns error code 206: permission denied and closes the realm file.
The strange thing is that the app manages to download a few objects from the realm, and it also loads a few more objects (about 3) every time I relaunch it.
An even stranger thing is that when I launch the app with the admin user the same thing happens except it loads about 100 extra objects in every relaunch and it doesn't show the permission denied error.
Note: I'm getting these results on the iOS simulator and still haven't tried the app on an actual device.
UPDATE: I have tried the app on a physical device and the problem persists.
I'm using Realm Swift v2.10.1 on Xcode 8.3.3 and Realm Object Server v1.8.3 on a Linux Ubuntu 16.04.3 x64 machine.
EDIT:
Here is how I apply the permission:
let per = SyncPermissionValue(realmPath: "/realmname", userID: "userId", accessLevel: .read)
realmUser!.applyPermission(per, callback: { (error) in
print("PERMISSION ERROR: \(String(describing: error))")
if error == nil {
print("no error")
}
})
Here is how connect from my app. These methods are in a custom class which is used by the view controller requesting the objects from the realm.
private func logInToServer() {
SyncUser.logIn(with: .usernamePassword(username: "username", password: "pass", register: false), server: URL(string: "http://server IP:9080")!, onCompletion: { (user, error) in
var realmUser = user
if realmUser == nil {
realmUser = SyncUser.current
}
self.connectToDatabase(realmUser: realmUser!)
})
} // end logInToServer()
private func connectToDatabase(realmUser: SyncUser) {
DispatchQueue.main.async(execute: {
let configuration = Realm.Configuration(syncConfiguration: SyncConfiguration(user: realmUser, realmURL: URL(string: "realm://server IP/realmname")!))
self.realm = try! Realm(configuration: configuration)
// I initially found out that implementing a small delay gave the app enough time to download all objects even though the permission denied error was still there. But, this no longer works.
Timer.scheduledTimer(timeInterval: self.delay, target: self, selector: #selector(self.populateResults), userInfo: nil, repeats: false)
})
} // end connectToDatabase()
#objc private func populateResults() {
self.categories = self.realm.objects(BookCategory.self)
self.books = self.realm.objects(Book.self)
delegate?.didPopulteResults(categories, books) // The delegate is the view controller requesting the objects.
}
Related
I'm newly learning iOS/swift programming and developing an application using MongoDB Realm and using Realm sync. I'm new to programming and realm, so please feel free to correct any terminology. My question is about listening for realm notifications, which I see referred to as change listeners and notification tokens. Regardless, here is the info:
My application has a list of locations with a status (confirmed/pending/cancelled). I open this list from my realm as a realm managed object and create my notification handler:
//This is called in it's own function, but assigns the locations
locations = publicRealm?.objects(Location.self)
//This is run after the function is called
self?.notificationToken = self?.locations!.observe { [weak self] (_) in
self?.tableView.reloadData()
print("Notification Token!!!")
I then populate my table view and let a user tap on a location, which passes the location and realm to another view controller where the user can update the status. That update is made in a separate view controller.
do{
try publicRealm?.write {
selectedLocation?.statusMessage = locationStatusTextField.text!
selectedLocation?.status = selectedStatus
}
}catch{
print("Error saving location data: \(error)")
}
At this point my notification token is successfully triggered on the device where I am making the location update. The change is shown immediately. However there is no notification token or realm refresh that happens on any other open devices that are showing the locations table view. They do not respond to the change, and will only respond to it if I force realm.refresh(). The change is showing in Atlas on MongoDB server, though.
I am testing on multiple simulators and my own personal phone as well, all in Xcode.
I'm very confused how my notification token can trigger on one device but not another.
When I first started the project it was a much simpler realm model and I could run two devices in simulator and updating one would immediately fire a change notification and cause the second device to show the correct notification.
I have since updated to a newer realm version and also made the realm model more complicated. Though for this purpose I am trying to keep it simple by doing all changes via swift and in one realm.
I also have realm custom user functions running and changing data but I think reading the docs I am realizing that those will not trigger a notification - I'm not sure if that's true though? I just know right now that if I change data in the DB via a user function no notifications are triggered anywhere - but if I do realm.refresh() then the change shows.
What is it that I am missing in how I am using these notifications?
***Updating information on Public Realm:
Save the realm:
var publicRealm:Realm?
Login as an anon user and then open the realm:
let configuration = user.configuration(partitionValue: "PUBLIC")
Realm.asyncOpen(configuration: configuration) { [weak self](result) in
DispatchQueue.main.async {
switch result {
case .failure(let error):
fatalError("Failed to open realm: \(error)")
case .success(let publicRealm):
self!.publicRealm = publicRealm
guard let syncConfiguration = self?.publicRealm?.configuration.syncConfiguration else {
fatalError("Sync configuration not found! Realm not opened with sync?")
}
It is after this realm opening that the locations are loaded and notification token is created.
I use a segue to pass the location object and realm to the next VC:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! UpdateLocationViewController
destinationVC.delegate = self
if let indexPath = tableView.indexPathForSelectedRow {
destinationVC.selectedLocation = locations?[indexPath.row]
}
if indexPathRow != nil {
destinationVC.selectedLocation = locations?[indexPathRow!]
}
destinationVC.publicRealm = self.publicRealm
}
Some notes:
original public realm opened by anon user
only a logged in user can click on a location... so in the 'UpdateLocation' VC that gets passed the public realm I am a logged in user. But, I'm just using the Dev mode of Realm to allow me to read/write however I like... and I am writing straight to that public realm to try and keep it simple. (I have a custom user function that writes to both public and the user's org realm, but I stopped trying to use the function for now)
I identify the object to update based on the passed in location object from the first VC
I needed to use try! when making my write call rather than just try. I updated my write blocks as such:
try! publicRealm?.write {
selectedLocation?.statusMessage = locationStatusTextField.text!
selectedLocation?.status = selectedStatus
}
Just wanted to follow up in case anyone finds this. Thank you!
Ok something really weird happened. Have an app that's about to go into production. This is how the application functions
There is a Home Page which fetches the logged in user's document every time the app comes to foreground
This is not a one time fetch but a listener so it will also listen for changes as the app is in foreground
I use this mechanism to keep the user document up to date and pass it to other pages in the app
When app goes to background, I remove the listener
The user document has close to 30 fields
So I was testing another functionality in app in the simulator and everything was working. I parallely ran the app with the same user logged in on a real device and this is what happened after the build:
As the app came to foreground the listener fired without stopping. I have the code in a such a way that every time the listener fetches the document from firestore, a part of the home page animates. So technically the home page refreshed without stopping
I killed the app and re-built it and the continuous firestore fetch happened yet again
So I all together deleted the app and re-built it - Now the continuous firestore fetch stopped
Here is the PROBLEM:
The document has all
30 fields on firestore
But on the real device, it fetches only 2 fields
I tried re-installing and re-building many times but this is the state
But on the simulator with the same user logged in, it works fine.
What could be the issue? Is there some corrupted cache for this particular user? Firestore is a solid product so never encountered anything like this before.
Here is the listener code:
#objc func viewEntersForeground(){
guard let currentUid = Auth.auth().currentUser?.uid else {return}
let ref = Firestore.firestore().collection("users").document(currentUid)
userListener = ref.addSnapshotListener({ (snapshot, error) in
if let error = error{
let alertController = ErrorAlertController(errorText: "\(error.localizedDescription)", errorTitle: "Something's wrong", viewController: self)
alertController.showAlertController()
return
}
//THE BELOW DICTIONARY FETCHES ONLY TWO FIELDS
guard let dictionary = snapshot?.data() else {return}
self.userDictionary = dictionary
self.user = User(dictionary: dictionary)
self.checkIfUserExists()
if self.user?.uid == "" || self.user?.uid == nil {
return
}
self.passDataToInbox()
self.passDataToSettings()
self.checkIfFirTokenExists()
self.checkIfProfilePictureExists()
if self.user?.userState == 3{
self.isUserState3 = true
} else {
self.userState = self.user?.userState
}
})
}
Working my way thru implementing the CKShare functionality of CloudKit.
I Haven manged to get to the part to share a record (via email for example) and also can confirm the user received the invite. The problem is that when user accepts the record then the app pops up but nothing happens. In order to assist here are some key Elements of the app, and please tell me if i am wrong.
1) The application does not require user to login using their Apple ID
2) I am testing the application via a direct built on two different phones (with seperate Apple IDs) when i Connect the phones to the computer with a Cable (aka not using TestFlight yet).
3) I have checked in the CloudkitDashboard and i can see the record that hah been shared and also see that the recored hah been shared, but instead of seeing the user email I sent the invite I see my email and the fact that the record hah been "accepted"
4) I Haven added the CKSharingSupported key in the Info.plist file.
5) The code in the AppDelegate.swift file am using to accept the CKShare is below. I world like to raw your attention to the fact that the string "User Accepted the Share" never gets printed, which makes me think that this part of the code never runs.
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("User Accepted the Share")
let acceptShareOperation: CKAcceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptShareOperation.qualityOfService = .userInteractive
acceptShareOperation.perShareCompletionBlock = {meta, share,
error in
print("The Record Share was Accepted")
}
acceptShareOperation.acceptSharesCompletionBlock = {
error in
/// Send your user to where they need to go in your app
let viewController: SNPDetailsViewController = self.window?.rootViewController as! SNPDetailsViewController
viewController.fetchShare(cloudKitShareMetadata)
}
CKContainer(identifier:
cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation)
}
Any insight to guide me where I am wrong, will be much appreciated.
Thank you for your time!
Please see this answer for more context: CloudKit CKShare URL Goes Nowhere
But make sure that:
You specify a fallback URL for your CloudKit Container that redirects to your application.
Inside your app in Xcode, you set up a URL scheme so that custom URLs like yourapp:// open your application and the query parameter from the fallback URL gets passed into userDidAcceptCloudKitShareWith.
After weeks of trial and error, research and LUCK I managed to find out the problem. All tutorials and online solutions relate to the below code in the AppDelegate.swift, to accept a CKShare record:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
let acceptShareOperation: CKAcceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptShareOperation.qualityOfService = .userInteractive
acceptShareOperation.perShareCompletionBlock = {meta, share,
error in
DispatchQueue.main.async() {
print("The Record Share was Accepted")
}
}
acceptShareOperation.acceptSharesCompletionBlock = {
error in
guard (error == nil) else{
print("Error \(error?.localizedDescription ?? "")")
return
}
let viewController: SNPDetailsViewController = self.window?.rootViewController as! SNPDetailsViewController
viewController.fetchShare(cloudKitShareMetadata)
}
CKContainer(identifier:
cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation)
}
The solution in my case was to add it in the SceneDelegate.swift in the following function:
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
(add above code)
}
Hope this helps others!
What I'm trying to do:
I am implementing the Spotify SDK into my iOS project. I am successfully receiving access tokens for Spotify's API as I am able to do things like search artists, search songs, and view playlists using said API.
The one thing I am struggling to do is play music with the SDK. I have a button that, upon clicking, I want the following flow to happen:
I request Spotify access by doing the following function and using the following Session Manager:
let SpotifyClientID = "###"
let SpotifyRedirectURL = URL(string: "bandmate://")!
lazy var configuration = SPTConfiguration(
clientID: SpotifyClientID,
redirectURL: SpotifyRedirectURL
)
lazy var sessionManager: SPTSessionManager = {
if let tokenSwapURL = URL(string: "https://bandmateallcaps.herokuapp.com/api/token"),
let tokenRefreshURL = URL(string: "https://bandmateallcaps.herokuapp.com/api/refresh_token") {
configuration.tokenSwapURL = tokenSwapURL
configuration.tokenRefreshURL = tokenRefreshURL
configuration.playURI = ""
}
let manager = SPTSessionManager(configuration: configuration, delegate: self)
return manager
}()
func requestSpotifyAccess() {
let requestedScopes: SPTScope = [.appRemoteControl, .userReadPrivate]
self.sessionManager.initiateSession(with: requestedScopes, options: .default)
}
Upon initiation of a SPTSession, I want to connect my remote:
lazy var appRemote: SPTAppRemote = {
let appRemote = SPTAppRemote(configuration: configuration, logLevel: .debug)
appRemote.delegate = self
return appRemote
}()
func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) {
self.appRemote.connectionParameters.accessToken = session.accessToken
self.appRemote.connect()
}
Upon app connection, I want to play the ID of a Spotify track that is declared globally:
var pendingSpotifyId: String!
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
print("connected")
self.appRemote.playerAPI!.delegate = self
self.appRemote.playerAPI!.subscribe(toPlayerState: { (result, error) in
if let error = error {
debugPrint(error.localizedDescription)
} else if self.pendingSpotifyId != nil {
self.appRemote.playerAPI!.play(self.pendingSpotifyId, callback: { (any, err) in
self.pendingSpotifyId = nil
})
}
})
}
My problem:
This flow is broken up as any time I try to initiate a session, sessionManager(manager: SPTSessionManager, didFailWith error: Error) is always called returning the following error:
Error Domain=com.spotify.sdk.login Code=1 "invalid_grant" UserInfo={NSLocalizedDescription=invalid_grant}
I need the session to initiate successfully so that sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) can be called and I can connect my remote and, ultimately, play my Spotify track.
What I've tried:
I have ensured a number of things:
Ensured the state of the Spotify app in the background on the user's device is playing (per this ticket: https://github.com/spotify/ios-sdk/issues/31)
Ensured that the correct scopes are in place when receiving an access token. Returned JSON looks something like:
{"access_token":"###","token_type":"Bearer","expires_in":3600,"refresh_token":"###","scope":"app-remote-control user-read-private"}
Things I'm suspicious of:
I am unaware if my token swap via Heroku is being done correctly. This is the only reason I can think of as to why I would be getting this issue. If I am able to use the Spotify API, is this evidence enough that my token swap is being done correctly? (I suspect it is)
Here's what we found out, hope it will help:
The SpotifySDK tutorial doesn't mention that Bundle ID and App Callback URL must precisely match across App Info.plist, source code, Spotify App Dashboard and Heroku Env Vars. The Bundle ID used must match your application Bundle ID.
The App Callback URL must not have empty path, ie: my-callback-scheme://spotify-login-callback
When using both Web Spotify SDK and iOS Framework Spotify SDK in app, take care that only one of them performs auth. Otherwise the App Callback URL will be called twice resulting in error.
The Spotify configuration.playURI may need to be set to empty string rather than nil. Sample app has a note on it.
It's best to have only one instance of object managing Spotify auth in the app. Otherwise ensuring that the correct object is called from the AppDelegate open url method can be tricky.
I have a small app in which users can upload local files to the server.
I am trying to handle unexpected situation like losing Internet connection. I want to use persistence feature which I have declared in AppDelegate.
Database.database().isPersistenceEnabled = true
At first, I export file to the server and if it succeeds then database reference is created.
let uploadTask = ref.putData(contents as Data, metadata: myMetadata, completion: { (metadata, error) in
if error != nil {
...
} else {
DataService.instance.usersRef.observeSingleEvent(of: .value) { (snapshot: DataSnapshot) in
...
}
}
)}
However, testing the solution (with turning airplane mode on and off) while exporting the file, the transfer is not restored. I am not sure if it's implementation issue or I'm missing key point of persistence feature.
Thanks in advance!