IPB

Bienvenue invité ( Connexion | Inscription )

 
Reply to this topicStart new topic
> [Swift] NSTableView et NSDocument, Comment sauvegarder les données ?
Options
olivion
posté 8 May 2019, 03:07
Message #1


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Bonjour à tous,

pour une application MacOS que je voudrais programmer, je cherche à placer une (plusieurs) tableView dans un NSDocument.
Dans le StoryBoard, dans la fenêtre document, je fais le lien entre la tableView et un ArrayController. Tout ça fonctionne correctement.

Ce que je n'arrive pas à faire, c'est la sauvegarde des données du document, et donc des données de la TableView.

Quelqu'un aurait-il un lien vers un tutoriel qui explique comment faire? J'ai beau chercher sur Google, je n'ai pas réussi à trouver quelque chose qui me satisfasse.

Merci d'avance...


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
schlum
posté 9 May 2019, 00:54
Message #2


Terminaltor
Moderating Machine
*****

Groupe : Admin
Messages : 24 447
Inscrit : 25 Oct 2002
Lieu : Jeumont (59)
Membre no 4 319



Code
    override func data(ofType typeName: String) throws -> Data
    {
        // End editing
        tableView.window?.endEditing(for: nil)
        
        let saveDict: [String: Any] = [
            "keyForArray": arrayToSave
        ]
        return NSKeyedArchiver.archivedData(withRootObject:saveDict)
    }


Code
    override func read(from data: Data, ofType typeName: String) throws
    {
        let saveDict: [String: Any] = NSKeyedUnarchiver.unarchiveObject(with: data) as! [String: Any]
        let savedArray = saveDict["keyForArray"] as! [ArrayItemClassName]
        // Disable undo manager (update data calls may generate undo events)
        undoManager?.disableUndoRegistration()
        // Update datas
        // ...
        // End update
        undoManager?.enableUndoRegistration()
    }


--------------------
          I think therefore I Mac          
Go to the top of the page
 
+Quote Post
olivion
posté 9 May 2019, 14:53
Message #3


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Merci beaucoup pour la réponse. En fait j'avais un peu avancé dans mes recherches, ce qui va me permettre d'être beaucoup plus précis dans ma demande (désolé d'avoir été si peu précis dans mon message antérieur). Comme l'application est nouvelle, j'essaye d'utiliser le "nouveau" système Codable, plutôt que NSCoding et NSKeyArchiver.

Ceci est le code de ma structure:
Code
import Foundation

struct Sound: Codable {
    var id: Int
    var name: String
    var fileUrl: String
    var beginsAt: Double
    var endsAt: Double
}


J'ai ensuite dans le fichier Document.swift une variable sounds: [Sound], liée à la variable sounds du viewController qui me permet de remplir la NSTableView.

Ma fonction d'écriture du fichier est donc:
Code
override func data(ofType typeName: String) throws -> Data {
        if let content = viewController?.sounds {
            let encodedData = try! JSONEncoder().encode(content)
            return encodedData
        }
        
        throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }


Jusque là tout va bien, par contre, c'est pour la lecture de fichier que je bloque. Le truc qui me paraît le plus logique est:
Code
override func read(from data: Data, ofType typeName: String) throws {
        sounds = try! JSONDecoder().decode([Sound].self, from: data)

        throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }

Pas de message d'erreur dans la console, mais juste une alert box avec "The document could'nt be opened".
Si j'affiche le contenu de sounds dans la console, pas de problème, ça a l'air d'être correctement décodé. Mais que dois-je faire pour que le document soit ouvert avec ce contenu?


Ce message a été modifié par olivion - 10 May 2019, 02:01.


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
schlum
posté 10 May 2019, 19:40
Message #4


Terminaltor
Moderating Machine
*****

Groupe : Admin
Messages : 24 447
Inscrit : 25 Oct 2002
Lieu : Jeumont (59)
Membre no 4 319



Normal, tu envoies une exception systématiquement ^^

Code
    override func read(from data: Data, ofType typeName: String) throws {
        guard let rdSounds = try? JSONDecoder().decode([Sound].self, from: data) else {
            throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
        }
        sounds = rdSounds
    }


Ça devrait aller mieux wink.gif


--------------------
          I think therefore I Mac          
Go to the top of the page
 
+Quote Post
olivion
posté 11 May 2019, 21:10
Message #5


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Il y a des jours comme ça où on se sent très bête !!!

Merci énormément, Schlum !

Olivier


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
olivion
posté 13 May 2019, 00:03
Message #6


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Je continue sur ma lancée parce que je suis confronté à une erreur qui semble être une erreur d'encodage. Comme je l'avais montré dans les messages précédents, ma structure Sound a un attribut fileUrl, que je passe en String. Ma tableView ne l'affiche pas mais l'attribut est bien présent dans la datasource.
Quand je sauvegarde mon document, tout va bien. Quand j'ouvre un document tout va bien et les données de chaque Sound apparaissent bien dans la table. Avec un print dans la console, je peux voir que l'attribut fileUrl est correct aussi.

Dans un autre endroit de mon interface, j'affiche les détails qui ne sont pas dans la table lorsque je sélectionne une ligne. Le soundAsset me sert à déterminer la longueur du son qui est associé au fichier dans la fileUrl.

Code
func tableViewSelectionDidChange(_ notification: Notification) {
        let row = soundTableView.selectedRow
        
        if row != -1 {
            let sound = sounds[row]
            let soundAsset = AVURLAsset(url: URL(string: sound.fileUrl)!)
  
            durationField.stringValue = formatTime(time: CMTimeGetSeconds(soundAsset.duration))
            beginsAtTextField.stringValue = formatTime(time: sound.beginsAt)
            endsAtTextField.stringValue = formatTime(time: sound.endsAt)
        }
    }


Lorsque j'aboute un enregistrement à ma table, tout fonctionne parfaitement.
Code
        let resultat = openPanel.runModal()
        if resultat == NSApplication.ModalResponse.OK {
            guard let fileUrl = openPanel.url else { return }
            
            var idMax = 0
            for sound in sounds {
                idMax = max(sound.id, idMax)
            }
            let displayName:  String = fileUrl.deletingPathExtension().lastPathComponent
            let soundAsset: AVURLAsset = AVURLAsset(url: fileUrl)
            let endsAt = CMTimeGetSeconds(soundAsset.duration)
            let sound: Sound = Sound(id: idMax + 1, name: displayName, fileUrl: fileUrl.absoluteString, beginsAt: 0.0, endsAt: endsAt)
            }
            
            sounds.append(sound)
            
            soundTableView.reloadData()
        }

Si après avoir ajouté un élément à la table, je clique sur sa ligne, je vois apparaître la longueur du son dans le champ durationField.

Par contre, si j'ouvre un fichier précédemment sauvegardé, et alors que le fileUrl apparaît correctement comme attribut, le temps affiché dans durationField est égal à zéro. C'est à dire que dans un document ouvert, le fileUrl de mon soundAsset ne trouve pas le, fichier.
Si je rajoute alors le même fichier dans ma table, un clic sur la nouvelle ligne affiche le temps correct, et un clic sur l'ancienne ligne affiche le temps correct aussi.

Qu'est-ce que je peux bien avoir fait de mal? L'encodage est fait sur un String, pas directement sur l'Url. Je ne vois pas...


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
schlum
posté 13 May 2019, 01:53
Message #7


Terminaltor
Moderating Machine
*****

Groupe : Admin
Messages : 24 447
Inscrit : 25 Oct 2002
Lieu : Jeumont (59)
Membre no 4 319



Je ne vois pas… Essaye avec NSKeyedArchiver / NSKeyedUnarchiver pour voir si ce n’est pas le JSONEncoder qui te joue un tour.


--------------------
          I think therefore I Mac          
Go to the top of the page
 
+Quote Post
olivion
posté 15 May 2019, 02:17
Message #8


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Merci Schlum de ta réponse. Comme j'ai du mal à coder le NSKeyedArchive, j'ai continué de chercher du côté du soundAsset.
J'ai un fichier sur mon bureau dont je connais l'exact path: /Users/MonNom/Desktop/Telegraph_Road.mp3

J'ai donc testé le code suivant:

Code
let pathString = "/Users/MonNom/Desktop/Telegraph_Road.mp3"
let url = URL.init(fileURLWithPath: pathString, isDirectory: false)
let soundAsset = AVURLAsset(url: url)
print(soundAsset.url.path)
print(CMTimeGetSeconds(soundAsset.duration))


Au moment de créer l'Asset, ce code ne prend pas l'url qui vient de la lecture du fichier., mais directement le pathString proposé. Mais là aussi, il ne voit pas le fichier, le soundAsset.url.path est correct mais il me donne un soundAsset.duration de 0.0 au lieu de 859...
Le problème ne semble donc pas venir de JSON.
Je suis en train de m'arracher le peu de cheveux qui me restent.


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
olivion
posté 15 May 2019, 12:43
Message #9


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Je précise un peu plus mes recherches sur le sujet:

Le sound.fileUrl est maintenant simplement le path du fichier, qui est sauvegardé comme String avec JSON.
Le path est récupéré correctement, le temps du son est nul, au lieu de 859 secondes, et le fichier existe...

Code
let url = URL.init(fileURLWithPath: sound.fileUrl, isDirectory: false)
let soundAsset = AVURLAsset(url: url)
print("Path : \(soundAsset.url.path)")
print("Sound Duration : \(CMTimeGetSeconds(soundAsset.duration))")
            
let fileManager = FileManager.default
print("File at this path exists : \(fileManager.fileExists(atPath: sound.fileUrl))")

Résultat dans la console:
Code
Path : /Users/oliviernoel/Desktop/Telegraph_Road.mp3
Sound Duration : 0.0
File at this path exists : true


En fait, je pense que le problème vient de ma manipulation du fichier. Lorsque je charge un fichier dans la table avec NSOpenPanel, je suppose que d'une certaine façon le fichier doit être chargé en mémoire. Alors que dans le deuxième cas, je donne juste l'adresse du fichier sans le charger réellement en mémoire. Je ne sais pas si je suis clair. Cela expliquerait que quand je rajouter à nouveau le fichier dans la table, le lien "invalide" redevient valide. Il me semble que le problème est donc un problème de manipulation du fichier sans être un problème de path ou d'url à proprement parler.


Ce message a été modifié par olivion - 15 May 2019, 16:44.


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
olivion
posté 21 May 2019, 11:23
Message #10


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Bon, j'ai eu ma solution. Le problème de lecture d'un fichier externe mp3 tient au sandboxing de toute application.
Lorsqu'on rajoute un fichier à ma table via NSOpenPanel, l'utilisateur donne un droit implicite à la lecture du fichier, donc pas de problème. Mais quand on charge le fichier de sauvegarde, on ne donne pas le droit en question.

Au lieu de conserver simplement le path ou l'Url, il faut donc passer par une URL sécurisée (security scoped). On crée un Bookmark à partir de l'Url, et au moment du chargement du document, on recrée les Url à partir du Bookmark, afin de récupérer le droit à l'accès aux fichiers externes associés.

Je ne suis pas sûr et certain à 100% de la qualité de mon code à ce niveau mais ça fonctionne. J'ai écrit le code en fonction de ce que j'ai compris.
Donc j'ai rajouté à ma classe sound un attribut "bmUrl: Data" (comme BookmarkURL du type Data).

Dans ma fonction @IBAction func addSound():, après avoir récupéré l'URL du fichier sélectionné, je crée le BookMark:
Code
let bmUrl = try! fileUrl.bookmarkData(options: .securityScopeAllowOnlyReadAccess, includingResourceValuesForKeys: nil, relativeTo: nil)


Je ne touche pas aux fonctions d'encodage et décodage de Document.swift.
Code
override func data(ofType typeName: String) throws -> Data {
        if let content = soundViewController?.sounds {
            let encodedData = try! JSONEncoder().encode(content)
            return encodedData
        }
        
        throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }

    override func read(from data: Data, ofType typeName: String) throws {
        guard let decodedData = try! JSONDecoder().decode([Sound]?.self, from: data) else {
            throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
        }
        sounds = decodedData
    }


Dans ma classe SoundViewController, je réinitialise les droits d'accès:

Code
override func viewDidAppear() {
        sounds = document.sounds ?? []
        
        var isStale = false
        
        for sound in sounds{
            _ = try? URL.init(resolvingBookmarkData: sound.bmUrl, options: URL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
        }
        
        soundTableView.reloadData()
    }

La variable isStale est retournée comme true si tout se passe bien. Comme je sauvegarde aussi le lien décodé, je n'ai pas vraiment besoin de recréer complètement le lien. D'où le _ au lieu d'un sound.fileUrl = ... La ligne de code redonne donc le droit d'accès au fichier.

Problème posé le 12 mai, et résolu le 20. 9 jours à m'arracher les cheveux. Donc je laisse ce petit témoignage pour le cas où ça servirait à quelqu'un rolleyes.gif



--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
schlum
posté 25 May 2019, 00:59
Message #11


Terminaltor
Moderating Machine
*****

Groupe : Admin
Messages : 24 447
Inscrit : 25 Oct 2002
Lieu : Jeumont (59)
Membre no 4 319



Désolé, pas eu les notifications de réponses… Bravo pour avoir trouvé, ça ne semble effectivement pas évident.


--------------------
          I think therefore I Mac          
Go to the top of the page
 
+Quote Post
olivion
posté 25 May 2019, 03:48
Message #12


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Salut Schlum. En fait, c'est pas complètement terminé. Si je sauvegarde la table, ferme l'application, la rouvre et recharge la table. pas de problème, tout fonctionne. Mais si j'éteins l'ordinateur, puis le rallume, j'ai de nouveau perdu les autorisations. Je vais me replonger dans le truc...


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post
schlum
posté 27 May 2019, 08:56
Message #13


Terminaltor
Moderating Machine
*****

Groupe : Admin
Messages : 24 447
Inscrit : 25 Oct 2002
Lieu : Jeumont (59)
Membre no 4 319



Tu as vu cette technote d’Apple (section « Security-Scoped Bookmarks and Persistent Resource Access ») ?

https://developer.apple.com/library/archive...011183-CH3-SW16


--------------------
          I think therefore I Mac          
Go to the top of the page
 
+Quote Post
olivion
posté 27 May 2019, 12:11
Message #14


Adepte de Macbidouille
*

Groupe : Membres
Messages : 192
Inscrit : 15 May 2005
Lieu : Buenos Aires - Argentina
Membre no 39 263



Merci, mais oui, j'avais vu. Et mon problème est là:
Citation
"When your app reopens, you have to start over. (The one exception to this is for files open at the time that your app terminates, which remain in your sandbox thanks to the macOS Resume feature)."

Ça réouvre correctement parce que c'est resté dans la SandBox. En fait, il faut que je rajoute une ligne qui consiste à fermer les liens au moment l'app se ferme, ce qui empêcherait de réouvrir les liens automatiquement en relançant l'application. Mais je dois avoir des choses qui manquent dans la création ou à l'ouverture, puisqu'en fin de compte, l'état normal est celui que j'obtiens quand je lance l'application après avoir éteint et rallumé l'ordinateur. Je continue de creuser (mais je suis un peu débordé en terme de boulot en ce moment).


--------------------
MacBook Air 13'', 8Go RAM, 256 Go / MacBook Pro, 8Go RAM, 500Go / — MacMini Intel (2006), 2Go RAM, 500Go
Go to the top of the page
 
+Quote Post

Reply to this topicStart new topic
1 utilisateur(s) sur ce sujet (1 invité(s) et 0 utilisateur(s) anonyme(s))
0 membre(s) :

 



Nous sommes le : 19th March 2024 - 15:04