Re-synchronisation des callback de multiples NSItemProvider (question générale sur la concurrence)

J'ai une petite question relative à la gestion la concurrence avec Swift. L'idée n'est pas de toucher à la nouveauté Swift Concurrency mais rester avec du bon vieux Grand Central Dispatch.

Voilà le soucis: j'ai une View qui est censée accepter un drop de n fichiers. Avec SwiftUI on récupère un Array de NSItemProvider. J'ai l'habitude d'AppKit et de NSPasteBoard qui n'utilise que des API synchrones mais là c'est pas le cas. Je suis obligé de passer pas la méthode qui commence par load et fini avec un completion handler appelé de manière asynchrone.

C'est pas très grave quand on balade un objet mais là j'en ai n avec une spécificité si n == 1. Alors voilà ce que je voudrai faire de manière efficace :

En un bloc :

  1. Récupérer les NSItemProvider
  2. Loader les URL correspondantes au fichier
  3. Filtrer les URL qui ne correspondent pas à une image
  4. Créer les NSImages correspondantes
  5. Mapper les images vers un autre type avec un peu de metadata et ma soupe à moi

Pour au final obtenir un Array. Une fois que je l'ai je mets ça dans une propriété du modèle et il se démerde pour gérer le tout. Le soucis que j'ai c'est pour définir la méthode pour gérer le loading des URL en un bloc et finir avec un gros DispatchQueue.main.async { /* Mets la valeur dans le modèle */ } des familles.

Quelqu'un peut me filer un coup de main ?

Mots clés:

Réponses

  • CéroceCéroce Membre, Modérateur

    Dans mon appli, j'utilise PromiseKit pour ce genre de choses… qu'on peut parait-il remplacer assez facilement par Combine.

    Est-ce que ça te parle ? Je peux te donner un exemple de ce que ça donnerait.

  • Ben oui des promises et Combine ! J'ai vraiment besoin de vacances...
    Merci @Céroce 😃

  • CéroceCéroce Membre, Modérateur

    Heureux d'avoir servi de canard en plastique!

  • @Céroce a dit :
    Heureux d'avoir servi de canard en plastique!

    Je la connaissais pas cette expression tien !

    Mon Combine était un peu rouillé ça a pris plus de temps que prévu. Quoi qu'il en soit voilà le résultat, ça peut aider du monde. Le code intéressant se trouve dans le onDrop le reste a été grandement simplifié:

    struct ImagePicker: View {
        @EnvironmentObject private var model: Model
        @State private var isDropTarget = false
    
        var body: some View {
            PickerView(isDropTarget: isDropTarget)
                .onDrop(of: [.fileURL], isTargeted: $isDropTarget) { providers in
                    guard !providers.isEmpty else { return false }
    
                    let futures = providers.map { provider -> Future<URL?, Never> in
                        Future { promise in
                            provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { data, err in
                                guard
                                    err.isNil,
                                    let urlData = data as? Data,
                                    let url = URL(dataRepresentation: urlData, relativeTo: nil)
                                else { return promise(.success(nil)) }
    
                                promise(.success(url))
                            }
                        }
                    }
    
                    Publishers.MergeMany(futures)
                        .compactMap(ImageRepresenter.init(url:))
                        .collect()
                        .receive(on: DispatchQueue.main)
                        .assign(to: &model.$inputImageRepresenters)
    
                    return true
                }
        }
    }
    
  • Pas certain d'avoir compris le problème, mais s'il s'agit de donner un point de rendez-vous à plusieurs taches exécutées en parallèle, il faut utiliser les dispatch groups en gcd.
    Chaque fois que tu entres dans une nouvelle tâche tu fais un enter() sur le group, quand la tache se termine tu fais un leave(), quand tous les leave ont été fait un block que tu as fourni au préalable (via notify()) est exécuté.

  • Oui le soucis sous-jacent c'est exactement ça @FKDEV.
    Et maintenant, à tête reposée, c'est un cas d'école typique pour l'utilisation des sémaphores.

    Par contre pour la prochaine fois, c'est safe d'appeler les méthodes de DispatchGroup depuis n'import quel thread ? Je trouve pas l'info dans la doc.

  • Si j'ai bien compris comment ça s'utilise, je dirais oui, car tu es obligé de le faire.
    Notamment, dans l'exemple suivant, tu dois faire le enter() avant le début de la tâche sinon tu risques d'en louper.

    let group = DispatchGroup()
    
    for storeUrl in storeURLs {
        group.enter()
    
        let subtask = URLSession.shared.dataTask( with: URLRequest(url: URL(string: storeUrl)!)) { data, response, error in
            guard let data = data, error == nil else {
                //...
                group.leave()
                return
            }
            if let body_response = String(data: data, encoding: String.Encoding.utf8) {
                //...
            }
    
            group.leave()
        }
        subtask.resume()
    
    }
    group.notify(queue: DispatchQueue.main) {
    // ... ici toutes les urls ont été traitées
    }
    

    Les sémaphores, cela fonctionne aussi, si tu connais le nombre de tache avant, ou alors tu peux t'en servir pour limiter le nombre de tâches en parallèle. Ici 5 tâches maximum :

    let limitingSemaphoreTrack = DispatchSemaphore(value: 5);
    for track in tracks {
        newPlaylist.addItem(withProductID:track.productId) { (error:Error?) in
            if error != nil {
                log.error("add item error: \(error!)")
            } else {
                //...
            }
            limitingSemaphoreTrack.signal()
        }
        _ = limitingSemaphoreTrack.wait(timeout:DispatchTime.distantFuture)
    }
    
  • CéroceCéroce Membre, Modérateur

    @FKDEV est-ce qu'on peut annuler le DispatchGroup en cas d'échec? C'est l'un des intérêts de la solution à base de Promises/Futures.

  • Je ne pense pas.
    C’est très basique, c’est à toi de gérer les erreurs.

    L’utilisation de promises donne un code plus lisible, mais pour ma part je ne veux pas utiliser de librairie tierce partie pour gérer la structure d’une app.
  • @FKDEV a dit :
    mais pour ma part je ne veux pas utiliser de librairie tierce partie pour gérer la structure d’une app.

    Bah si t'es en mesure de comprendre et maintenir son code, pourquoi pas ?

  • FKDEVFKDEV Membre
    octobre 2021 modifié #12
    Déjà, quand c'est possible, je limite l'utilisation de librairies en général.
    Je préfère les décortiquer et prendre les parties qui m'intéressent, cela permet d'apprendre, de s'approprier le code.

    En ce qui concerne la structure, si tu veux réutiliser des morceaux de ton app ou faire un portage, t'es vite coincé.
    Pareil, s'il y a de une nouvelles features du système et que la librairie n'est plus maintenue.
    Et puis tu te retrouves à apprendre à utiliser une API dont tu n'es pas sûr de la pérennité. Autant apprendre des API first-party.

    Après tout dépend du contexte...
Connectez-vous ou Inscrivez-vous pour répondre.