How may participants reverse their acceptance of CKShare? - swift

Is there a way to implement the behaviour - participants reverse their acceptances of CKShares? After the participants accept the CKShares, they go over the shared and say oh no I don't want to contribute anything to the shared and I don't even want to keep the shared records in watch lists of their UI interface.
How do we do to remove them from such collaborations (from shared database to be exact)? The use case is very legitimate as people may accept somethings so that having a chance to go over a bit before decide wether they actually contribute their time and efforts into the shared.
I have tried to use CKModifyRecordZonesOperation to delete the shared zone of CloudKit shared database:
var zoneOp = CKModifyRecordZonesOperation(recordZonesToSave: nil, recordZoneIDsToDelete: [zoneID]
zoneOp.database = sharedDB
However, it seems any shared zones can't be deleted from any participants' iOS devices (except for using their development CloudKit Dashboards that may work though) as such attempt is followed by CKError - disallow delete zone...

From the docs: https://developer.apple.com/documentation/cloudkit/ckshare
If a participant attempts to delete the share, CloudKit removes the participant.
Deleting the share is simple:
try await container.sharedCloudDatabase.deleteRecord(withID: shareRecordId)
The question is how to get the original CKShare.recordID?
Zone-wide-share
Re-create the share id of the zone by using the zoneID:
let shareRecordID = CKRecord.ID(
recordName: CKRecordNameZoneWideShare, // "cloudkit.zoneshare"
zoneID: zone.zoneID
)
Other shares
For other shares I think you'll have to store the ID when accepting the share:
func windowScene(
_ windowScene: UIWindowScene,
userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata
) {
let isZoneWide = (metadata.share.recordID.recordName == CKRecordNameZoneWideShare)
if !isZoneWide {
// Store cloudKitShareMetadata.share.recordID somewhere!
}
let acceptSharesOperation = CKAcceptSharesOperation(
shareMetadatas: [cloudKitShareMetadata]
)
// ... Accept share code here
}
You might also be able to fetch the accepted shares, but I haven't tried this code:
let query = CKQuery(recordType: "cloudkit.share", predicate: NSPredicate(value: true))
let shares = try await container.sharedCloudDatabase.records(matching: query)

Related

How can I check if a RecordZone is shared with CloudKit?

I am interested in checking if a RecordZone has been shared. When I share a zone, I know there is a share record created in that zone. In Apple's CloudKit example, it checks to see if a specific record is shared:
func fetchOrCreateShare(contact: Contact) async throws -> (CKShare, CKContainer) {
guard let existingShare = contact.associatedRecord.share else {
let share = CKShare(rootRecord:contact.associatedRecord)
share[CKShare.SystemFieldKey.title] = "Contact: \.(contact.name)"
_ = try await database.modifyRecords(saving:[contact.associatedRecord, share], deleting: []).
return (share, container)
}
guard let share = try await database.record(for: existingShare) as? CKShare else {
throw ViewModelError.invalidRemoteShare
}
return (share, container)
}
The first line of the method guard let existingShare = contact.associatedRecord.share checks if a specific record is being shared, but in the case of a shared zone, given a zone name, how can I check the .share of that zone? When I try CKRecordZone(zoneName: "Contacts").share, the value is nil even though the zone is being shared.
I believe that is explained in the CKShare docs shown in bold below:
After CloudKit saves the share, a participant can fetch its
corresponding metadata, which includes a reference to the share,
information about the user’s participation, and, for shared
hierarchies, the root record’s record ID. Create an instance of
CKFetchShareMetadataOperation using the share’s URL and add it to the
container’s queue to execute it. The operation returns an instance of
CKShare.Metadata for each URL you provide. This is only applicable if
you manually process share acceptance. If a user receives the share
URL and taps or clicks it, CloudKit automatically processes their
participation.
To determine the configuration of a fetched share,
inspect the recordName property of its recordID. If the value is
CKRecordNameZoneWideShare, the share is managing a shared record zone;
otherwise, it’s managing a shared record hierarchy.
let isZoneWide = (metadata.share.recordID.recordName == CKRecordNameZoneWideShare)

Why does the display name of my users keep reverting in Firebase with swift?

I encountered the following error in my swift project: https://github.com/firebase/firebase-ios-sdk/issues/4393. To overcome this issue I change the display name to the characters before the # in a users email like so:
var displayName = user.email!
if let atRange = displayName.range(of: "#") {
displayName.removeSubrange(atRange.lowerBound.. < displayName.endIndex)
}
if user.displayName!.count < 2{
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = displayName
changeRequest?.commitChanges {(err) in
if let err = err{
print(err)
}
}
}
This works when I first log in but if I log out and then back in again the display name reverts back to Optional(""). Why would it be doing this?
Edit
I created a workaround where I run the above code not only when a user is first created, but every time a user logs in, this seems excessive though and there must be a reason why the display name keeps getting overwritten.
This is not an error, this is intended behaviour from Apple's side. By default, you should be updating the Firebase user with the appropriate display name rather than relying on 3rd party information since these are only available on first login.
Simply checking if the name exists on initial login and is longer than 0 characters, not null, etc. then you can update the name with the following:
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = displayName
changeRequest?.commitChanges { (error) in
// ...
}
This is also documented within the git-issue you posted https://github.com/firebase/firebase-ios-sdk/issues/4393#issuecomment-612193703
Ultimately, you are logging into your app as a Firebase user, not an apple user. Apple is only Authenticating the process, so you must update the Firebase user where possible as Apple is highly inconsistent across all platforms.

How to create a new shared record in Cloudkit without being the owner of the parent?

I am developing an app which is basically similar to a shared shopping list. I am willing to use Cloudkit.
It does not make sense to share individually every shopping list item, so the idea is to create a sharing at the list level. This way, all items on the list are shared, as explained here
I have successfully implemented this idea. Basically, the owner of the list can create items, and every time an item is created, users who have accepted to share the list with the owner can see the new item in the Shared Database. I have also checked that when a user modifies an existing record in their Shared Database, the modification is reflected in the owner's Private Database.
Now the problem is that someone who is not the owner of the list might want to create a new item. Basically, if 2 people share a shopping list, both of them should be able to add a new thing to to buy on the list!
It seems that this is not possible on Cloudkit. In other terms, only the owner of a parentrecord seems to be able to create children records. Has anyone found a solution for such a use case ?
I am answering my own question after investigating more.
It seems that it's not possible to create a shared record in the Cloudkit console without being the owner, but it's possible from Swift.
Here is a sample of code to do so:
let sharedDatabase = CKContainer.default().sharedCloudDatabase
sharedDatabase.fetchAllRecordZones { (recordZone, error) in
let myZoneID = recordZone![0].zoneID//Temporary hack
let query = CKQuery(recordType: "parentRecordType", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
sharedDatabase.perform(query, inZoneWith: myZoneID) { (records, error) in
print(records)
let newID = CKRecord.ID(zoneID: myZoneID)
let nameRecord = CKRecord(recordType: "childRecordType", recordID: newID)
nameRecord.setValue("Created by someone who is not the owner", forKey: "title")
nameRecord.parent = CKRecord.Reference(record:records![0], action: .none)
CKContainer.default().sharedCloudDatabase.save(nameRecord) { (savedRecord: CKRecord?, error: Error?) -> Void in
print(error)
}
}
}

Why doesn't Deletion in Parse-server require to obtain that object beforehand?

I came across a strange behaviour. Even though my unit tests are passing, I don;t quite understand this.
func testDeleteMoment() {
// Create a Moment for current user
let expectCreate = expectation(description: "create moment should succeed")
Datastore.shared.createMoment(notes: "myNotes", rating: 1, time: Date(), location: "", isPublic: true, completion: {(success, error) in
XCTAssertTrue(success)
expectCreate.fulfill()
})
waitForExpectations(timeout: 5) { (error) in
XCTAssertNil(error, "Test timed out. \(String(describing: error?.localizedDescription))")
}
let query = PFQuery(className: "Moment")
var objectId = ""
do {
query.whereKey("owner", equalTo:PFUser.current()!)
let object = try query.getFirstObject()
objectId = object.objectId!
} catch {}
let task = Datastore.shared.deleteMoment(id: objectId)
task.waitUntilFinished()
let query2 = PFQuery(className: "Moment")
query2.whereKey("owner", equalTo:PFUser.current()!)
let task3 = query2.countObjectsInBackground()
task3.waitUntilFinished()
XCTAssertEqual(task3.result, 0)
}
While writing my datastore.deleteMoment(), I noticed that unlike saveEventually(), deleteEventually() doesn't have a completion handler. Instead, it only comes with a BFTask<NSNumber>. Hence I experimented with the following code and the unit test passes to my surprise.
func deleteMoment(id: String) -> BFTask<NSNumber> {
let pfMoment = PFObject(className:"Moment")
pfMoment.objectId = id
return pfMoment.deleteEventually()
}
How comes that I don't have to retrieve the object before hand in order to delete it? Why isn't there then just a method to delete the object via an id, instead of doing it like this?
How comes that I don't have to retrieve the object before hand in order to delete it?
Parse lets you work with "shell objects". Basically, if you know the object id you can attach it to a blank instance of a Parse Object subclass and do some actions on it. You can create a shell, assign a couple values, and call object.save() and it will update just those fields. You can create a blank object, assign an id, then call object.fetch() to obtain it, without having to have gotten the object from a query or pointer on another object.
The reason it works is because in order to perform the database operations, the object id and class name are the only pieces of information required. If you just want to update a couple fields, you don't need to pull all of the rest of the data to do so. And if you want to destroy an object, why would you need all of it's data locally first? You just need to remove the entry in the database with the matching _id.
Why isn't there then just a method to delete the object via an id, instead of doing it like this?
Nobody has built it yet. Parse is open source, and while it's a pretty phenomenal resource today, there's certainly a lot of room for improvement. Feel free to add this feature and create a PR. Though you could easily build your own wrapper by extending the Parse.Object class with this function, that basically does what you already did under the hood.

Swift & Parse.com find by specific column not ID

Reference: http://blog.parse.com/2014/06/06/building-apps-with-parse-and-swift/
I'm trying to find a columns value: userPassword, based in the userName column. Using the above reference from Parse it shows that to get data from parse you should use:
var query = PFQuery(className: "GameScore")
query.getObjectInBackgroundWithId(gameScore.objectId) {
(scoreAgain: PFObject!, error: NSError!) -> Void in
if !error {
NSLog("%#", scoreAgain.objectForKey("playerName") as NSString)
} else {
NSLog("%#", error)
}
}
However, as you can see it is looking for (gameScore.objectId) - The problem is I do not know this value as the user isnt entering a complex parse generated ID. They're entering their chosen username. In the rows I have userName and Password set. How do I search the rows for the userPassword so I can verify it based on their specified userName.
Thanks in advance
Why are you querying the database for a username and password. Adding a new user is very simple with Parse. Taken directly from their docs:
Query User table on Parse
You can query the user table first, using a PFQuery:
PFQuery *query = [PFUser query];
[query whereKey:#"username" equalTo:username];
Adding New User
The idea of user accounts that let people access their information and share it with others in a secure manner is at the core of any social app. Whether your app creates its own sharing environment or integrates with existing social networks, you will need to add functionality to let people manage their accounts in your app.
We provide a specialized user class called PFUser that automatically handles much of the functionality required for user account management.
First make sure to include our SDK libraries from your .h file:
#import <Parse/Parse.h>
Then add this code into your app, for example in the viewDidLoad method (or inside another method that gets called when you run your app):
func myMethod() {
var user = PFUser()
user.username = "myUsername"
user.password = "myPassword"
user.email = "email#example.com"
// other fields can be set just like with PFObject
user["phone"] = "415-392-0202"
user.signUpInBackgroundWithBlock {
(succeeded: Bool!, error: NSError!) -> Void in
if error == nil {
// Hooray! Let them use the app now.
} else {
let errorString = error.userInfo["error"] as NSString
// Show the errorString somewhere and let the user try again.
}
}
}
This call will asynchronously create a new user in your Parse app. Before it does this, it checks to make sure that both the username and email are unique. It also securely hashes the password in the cloud.
You can learn more about Users, including how to verify emails and handle read and write permissions to data, by visiting our docs.
Run your app. A new object of the class User will be sent to the Parse Cloud and saved. When you're ready, click the button below to test if a User was created.
Further
I created a tutorial about connecting to parse if you still wish to go down the route of querying the server manually:
http://ios-blog.co.uk/tutorials/swift-create-user-sign-up-based-app-with-parse-com-using-pfuser/