---
title: "Un workflow di sharing personalizzato con CloudKit"
url: https://www.sofapps.it/it/blog/custom-cloudkit-sharing/
date: 2018-06-02
author: Costantino Pistagna
language: it
translated_from: en
tags: [Swift, UIKit, Tutorial, CloudKit, WWDC18]
---

# Un workflow di sharing personalizzato con CloudKit

> Il flusso standard di CKShare richiede una UICloudSharingController, un link inviato con un'app di terze parti e dei delegate lato ricevente. Questo post conserva l'URL del CKShare nel campo privateShareUrl di un record pubblico: il proprietario aggiunge i partecipanti in modo silenzioso e questi leggono il record privato dal database condiviso. Include il codice Swift per zona, record, share e fetch, e un progetto di esempio su GitHub.

CloudKit dà grande importanza alla privacy e alla sicurezza degli utenti. Poiché una delle preoccupazioni principali del mio ultimo lavoro, Lookii, riguardava proprio la privacy degli utenti, è stata un'ottima occasione per approfondire le reali potenzialità del framework.

Da iOS 10 CloudKit include una funzionalità integrata per condividere dati privati tra i partecipanti di un container, chiamata CKShare. Purtroppo online ci sono pochissimi articoli sull'argomento e la documentazione di Apple non entra nel dettaglio dei temi avanzati. Tutti gli articoli che ho citato nelle note descrivono il flusso base della condivisione con CloudKit, con alcune varianti interessanti che però non si adattavano esattamente al mio caso. Non tratterò le basi, per le quali rimando alle note. Mi concentrerò invece su un workflow specifico e più avanzato, che potrebbe essere utile a qualcun altro.

### Il caso d'uso

Immaginiamo di avere un'applicazione che condivide informazioni degli utenti. Alcune devono restare private ed essere condivise solo tra amici, altre invece devono essere accessibili pubblicamente a tutti i partecipanti del container.

Supponiamo che il nostro tipo di CKRecord sia MyUser e contenga le seguenti informazioni pubbliche:

```swift
firstName: String
lastName: String
thumbnail: Data
privateShareUrl: String
```

Altri dati non saranno pubblici, per ragioni di privacy. Per questi useremo un altro CKRecord, che chiameremo PrivateInfo, con il seguente contenuto:

```swift
phoneNumber: String
email: String
```

Stavolta il record non farà parte del database pubblico. Verrà invece salvato in quello privato, accessibile solo al proprietario del record, e inserito in una RecordZone che sarà condivisa con i partecipanti scelti.

Uno schema generale del caso d'uso descritto potrebbe essere questo:

![Diagramma: il database pubblico con firstName, lastName, thumbnail e privateShareUrl, il database privato con phoneNumber ed email e, tra i due, la zona condivisa FavZone](https://www.sofapps.it/blog/custom-cloudkit-sharing/fig-1.png)

## Il workflow generale di CloudKit e i suoi limiti

Come detto, una delle preoccupazioni principali nell'usare il workflow di sharing predefinito di CloudKit riguardava le opzioni di personalizzazione della UI, molto limitate. In particolare il workflow base segue uno schema molto rigido:

>Chi condivide deve occuparsi di:

- Creare il CKShare
- Passarlo a una specifica UICloudSharingController
- Creare un URL da condividere con app di terze parti

>Gli utenti che vogliono essere aggiunti devono:

- Toccare il link per accettare l'URL condiviso
- Implementare alcuni delegate in UIApplication

Anche se la sua linearità lo rende facilissimo da integrare, questo approccio manca di alcune funzionalità avanzate. Per esempio, non vorrei disturbare gli utenti con flussi complicati che interrompono la naturale esperienza dell'app. Vorrei invece eseguire il workflow il più possibile in background, liberando mittente e destinatario da dialoghi inutili e da cambi di contesto tra un'app e l'altra.

>Sarebbe bello poter scegliere i destinatari delle mie informazioni private semplicemente toccando l'icona dell'utente nella mia app.

## Implementazione

La prima cosa da fare è abilitare la userDiscoverability tra gli utenti dell'app, altrimenti il resto del processo non funzionerà a causa dei vincoli di privacy.

Per fortuna Apple ha previsto un approccio molto semplice: basta una semplice richiesta di permesso all'utente, che potete inserire nelle prime fasi della vita dell'app:

```swift 
//ask for userDiscoverability otherwise you will obtain nil each time you try to search for him
CKContainer.default().requestApplicationPermission(CKApplicationPermissions.userDiscoverability) { (status, error) in
    switch status {
    case .granted:
        print("granted")
    case .denied:
        print("denied")
    case .initialState:
        print("initial state")
    case .couldNotComplete:
        print("an error as occurred: ", error ?? "Unknown error")
    }
}
```

Una volta ottenuto il permesso dell'utente siamo pronti a partire. Creiamo un CKRecord utente molto semplice (quello che sarà pubblico):

```swift 
func createPublicRecord() {
    let aRecord = CKRecord(recordType: "MyUser")
    aRecord.setObject("John" as CKRecordValue, forKey: "firstName")
    aRecord.setObject("Appleseed" as CKRecordValue, forKey: "lastName")

    let container = CKContainer.default()
    let publicDatabase = container.publicCloudDatabase
    
    publicDatabase.save(aRecord, completionHandler: { (record, error) -> Void in
        if let error = error {
            print(error)
        }
        else {
            print("record saved successfully")
        }
    })
}
```

Poi, come parte del processo iniziale, dobbiamo creare una zona specifica con cui esporre una vista dei nostri record privati agli utenti scelti (i nostri amici preferiti):

```swift 
func createFavZone(completionHandler:@escaping (CKRecordZone?, Error?)->Void) {
    let container = CKContainer.default()
    let privateDatabase = container.privateCloudDatabase
    let customZone = CKRecordZone(zoneName: "FavZone")
    
    privateDatabase.save(customZone, completionHandler: ({returnRecord, error in
        completionHandler(returnRecord, error)
    }))
}
```

Infine creiamo un record privato che contiene tutte le nostre informazioni preziose:

```swift
func createPrivateRecord() {
    let container = CKContainer.default()
    let privateDatabase = container.privateCloudDatabase

    let recordZone: CKRecordZone = CKRecordZone(zoneName: "FavZone")
    let aRecord = CKRecord(recordType: "PrivateInfo", zoneID: recordZone.zoneID)

    aRecord.setObject("+393331112223" as CKRecordValue, forKey: "phoneNumber")
    aRecord.setObject("john@appleseed.com" as CKRecordValue, forKey: "email")

    
    privateDatabase.save(aRecord, completionHandler: { (record, error) -> Void in
        if let error = error {
            print(error)
        }
        else {
            print("record saved successfully")
        }
    })
}
```

Notate che il record è legato alla CKRecordZone creata prima.

Come avrete notato, non abbiamo ancora scritto il campo privateShareUrl che fa parte del record pubblico: è la parte cruciale di tutto l'approccio descritto in questo articolo. Il comportamento standard, infatti, si basa sulla UICloudSharingController, che mostra all'utente un dialogo in cui si sceglie cosa e come condividere. Il risultato del dialogo è un URL da usare con app di terze parti come Messaggi, Mail o altro per comunicare l'intenzione di condividere qualcosa.

A peggiorare le cose, dall'altra parte il destinatario deve toccare il link che a sua volta apre la nostra app e chiama il corrispondente metodo delegate di UIApplication. Quest'ultimo ci restituisce alla fine un'istanza di CKShareMetadata, che possiamo usare per leggere le informazioni condivise.

## Il nostro approccio

Poiché la nostra applicazione ha già degli utenti, vorrei aggirare questo workflow usando il campo privateShareUrl come punto di ingresso per chiunque abbia bisogno di leggere le informazioni private di uno specifico utente.

>Apple dichiara che l'URL di condivisione di un CKShare è stabile e non cambia a meno che non si cambi il root record associato.

Il mio piano è simulare il workflow base di sharing usando il campo privateShareUrl che abbiamo nel database pubblico per ogni utente. Questo campo conterrà l'URL originale del CKShare. In questo modo ogni utente che vuole leggere le informazioni private di uno specifico partecipante accederà all'URL e verificherà se ha il permesso di accedere ai campi privati esposti.

Gli utenti che vogliono condividere le proprie informazioni private con gli amici, invece, possono semplicemente aggiungerli all'URL di condivisione, senza disturbare i destinatari con app di terze parti e messaggi. Ottimo!

La prima cosa da fare è impostare un accesso predefinito pari a nessuno per le nostre informazioni private. In questo modo, nello stato iniziale, nessuno può accedere ai nostri dati privati. Solo quando aggiungiamo qualcuno alla condivisione potrà accedere ai dati. Apple dichiara che il permesso pubblico predefinito di un CKShare è già configurato su nessun accesso.

```swift
func createDefaultShareProfileURL() {
    let container = CKContainer.default()
    let privateDatabase = container.privateCloudDatabase
    
    let query = CKQuery(recordType: "PrivateInfo", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
    let recordZone: CKRecordZone = CKRecordZone(zoneName: "FavZone")
    
    privateDatabase.perform(query, inZoneWith: recordZone.zoneID) { (results, error) -> Void in
        if let error = error {
            print(error)
        }
        else if let results = results, let ourRecord = results.first {
            let share = CKShare(rootRecord: ourRecord)
            
            let modOp: CKModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [ourRecord, share], recordIDsToDelete: nil)
            modOp.savePolicy = .ifServerRecordUnchanged
            modOp.modifyRecordsCompletionBlock = { records, recordIDs, error in
                if let error = error  {
                    print("error in modifying the records: ", error)
                }
                else if let anURL = share.url {
                    let container = CKContainer.default()
                    let publicDatabase = container.publicCloudDatabase
                    let query = CKQuery(recordType: "MyUser", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
                    var myPublicProfile:CKRecord?
                    
                    publicDatabase.perform(query, inZoneWith: nil) { (results, error) -> Void in
                        if let error = error {
                            print("error: ", error)
                        }
                        else if let results = results {
                            for aRecord in results {
                                if aRecord.creatorUserRecordID?.recordName == "__defaultOwner__" {
                                    myPublicProfile = aRecord
                                    break
                                }
                            }
                        }
                        if let myPublicProfile = myPublicProfile {
                            myPublicProfile.setObject(anURL.absoluteString as CKRecordValue, forKey: "privateShareUrl")
                            publicDatabase.save(myPublicProfile, completionHandler: { (record, error) -> Void in
                                if let error = error {
                                    print("error: ", error)
                                }
                                else {
                                    print("all done, folks!")
                                }
                            })
                        }
                    }
                }
            }
            privateDatabase.add(modOp)
        }
    }
}
```

Ora, ogni volta che un utente vuole vedere le informazioni private condivise da un altro, controllerà il privateShareUrl designato per l'utente in questione:

```swift
func refreshUserInformation(_ userRecordID: CKRecordID) {
    let container = CKContainer.default()
    let publicDatabase = container.publicCloudDatabase
    
    publicDatabase.fetch(withRecordID: userRecordID) { (record, error) in
        if let error = error {
            print(error)
        }
        else if let record = record {
            let firstName = record.object(forKey: "firstName") as? String
            let lastName = record.object(forKey: "lastName") as? String
            
            //do something with firstName and lastName, updating UI
            
            if let shareURL = record.object(forKey: "privateShareUrl") as? String {
                let sharedDatabase = container.sharedCloudDatabase
                let anURL = URL(string: shareURL)!
                
                let op = CKFetchShareMetadataOperation(shareURLs: [anURL])
                op.perShareMetadataBlock = { shareURL, shareMetadata, error in
                    if let error = error {
                        print(error)
                    }
                    else if let shareMetadata = shareMetadata {
                        if shareMetadata.participantStatus == .accepted {
                            let query = CKQuery(recordType: "PrivateInfo", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
                            let zone = CKRecordZoneID(zoneName: "FavZone", ownerName: (shareMetadata.ownerIdentity.userRecordID?.recordName)!)
                            sharedDatabase.perform(query, inZoneWith: zone, completionHandler: { (records, error) in
                                if let error = error {
                                    print(error)
                                }
                                else if let records = records, let firstRecord = records.first {
                                    let phoneNumber = firstRecord.object(forKey: "phoneNumber") as? String
                                    let email = firstRecord.object(forKey: "email") as? String
                                    
                                    //do something with private infos
                                }
                            })
                        }
                        else if shareMetadata.participantStatus == .pending {
                            let acceptOp = CKAcceptSharesOperation(shareMetadatas: [shareMetadata])
                            acceptOp.qualityOfService = .userInteractive
                            acceptOp.perShareCompletionBlock = { meta, share, error in
                                if let error = error {
                                    print(error)
                                }
                                else if let share = share {
                                    let query = CKQuery(recordType: "PrivateInfo", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
                                    let zone = CKRecordZoneID(zoneName: "FavZone", ownerName: (share.owner.userIdentity.userRecordID?.recordName)!)
                                    sharedDatabase.perform(query, inZoneWith: zone, completionHandler: { (records, error) in
                                        if let error = error {
                                            print(error)
                                        }
                                        else if let records = records, let firstRecord = records.first {
                                            let phoneNumber = firstRecord.object(forKey: "phoneNumber") as? String
                                            let email = firstRecord.object(forKey: "email") as? String
                                            
                                            //do something with private infos
                                        }
                                    })
                                }
                            }
                            container.add(acceptOp)
                        }
                    }
                }
                op.fetchShareMetadataCompletionBlock = { error in
                    if let error = error {
                        print(error)
                    }
                }
                container.add(op)
            }
        }
    }
}
```

Semplice ed efficace. Ho messo insieme un'applicazione di prova con un caso d'uso completo, disponibile su [GitHub](https://github.com/valvoline/CKShareTest).

Happy Coding! 🖖🏻
