[SWIFT] NotificationCenter & refresh tchat

Bonjour tout le monde,

Je suis actuellement en train de développer une messagerie interne et je bloque sur une fonctionnalité, celle d'aller chercher les messages reçus lorsqu'on est déjà sur la conversation.

Voici comment je m'y prend :

Lorsqu'un message est envoyé via l'appli, j'enregistre les infos dans ma bdd puis j'envois un push (via firebase) aux destinataires.

Mon push contient l'id de la conversation et l'id du dernier message envoyé..

D'après ce que j'ai compris sur le NotificationCenter, c'est que je peux surveiller un évènement et déclencher une fonction associé (j'ai bon ?)

J'ai donc créer mon évènement :

extension Notification.Name {
    static let NouveauMessage = Notification.Name("NouveauMessage")
}

Dans mon AppDelegate, j'ai ajouté l'observer pour détecter lorsque mon évènement est appelé..

func applicationDidBecomeActive(_ application: UIApplication) {
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(maFonctionDeTest), name: .NouveauMessage, object: nil)
}

// la fonction qui sera appelée à chaque push "nouveau message"
@objc func maFonctionDeTest(notification: Notification){
    print(Notification)
}

Toujours dans AppDelegate, chaque notification reçu, je vérifie si c'est bien un message, si je suis bien sur le bon controller (ma page de tchat) et si c'est le cas, je déclenche un évènement "NouveauMessage"

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    // verification si c'est bien un push "nouveau message", si je suis bien sur la bonne page (DiscussionController), etc etc,
    let notificationCenter = NotificationCenter.default
    notificationCenter.post(name: .NouveauMessage, object: self, userInfo: userInfo) // Envoi l'évènement "NouveauMessage"
}

Donc lorsque je reçois un push, je créé ma notification (.NouveauMessage) qui passe bien dans "maFonctionDeTest"..

C'est là que je m'y perds..

1/ Déjà, plutôt que d'utiliser maFonctionDeTest, est-ce-que je peux utiliser une fonction qui se trouve directement dans mon controller (ma page de tchat) ?

2/ Est-ce-que je peux remplir les variables de ce controller ?
Je m'explique..
Mon controller ressemble à ça :

class DiscussionController: UIViewController, etc etc {

var IdDiscussion = "0"

override func viewDidLoad(){
    // mon code, etc etc
    chargeMessage()
}


func chargeMessages(){
    // requête pour récupérer les derniers messages
    // ici j'ai besoin de l'IdDiscussion
    // ça fonctionne très bien quand je le remplis via prepare(for segue)
    // mais pas depuis AppDelegate
}

}

Lorsque je suis dans Appdelegate, comment faire pour remplir la variable IdDiscussion de mon controller (DiscussionController) ?
J'ai essayé avec :

DiscussionController().IdDiscussion = "10"

Mais il ne le prend pas en compte et reste toujours à zéro :/

Merci de votre aide :)

Réponses

  • CéroceCéroce Membre, Modérateur
    novembre 2019 modifié #2

    @Insou a dit :
    D'après ce que j'ai compris sur le NotificationCenter, c'est que je peux surveiller un évènement et déclencher une fonction associé (j'ai bon ?)

    Oui, il s'agit de signaux globaux à l'application.
    Tu postes un signal dans le NotificationCenter et il avertit tous ses abonnés.

    Dans mon AppDelegate, j'ai ajouté l'observer pour détecter lorsque mon évènement est appelé..

    func applicationDidBecomeActive(_ application: UIApplication) {
        let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self, selector: #selector(maFonctionDeTest), name: .NouveauMessage, object: nil)
    }
    

    Déjà, il existe une version de addObserver() qui utilise une closure. Je trouve cela plus pratique, et moins "Objective-C".

    Ensuite, il me semble que la seule chose que devrait faire la UIApplication sur la réception de la notification est éventuellement d'amener le DiscussionController au premier plan.

    Ce qui amène au deuxième point: il peut tout à fait y avoir plusieurs abonnés à une même notification. S'il n'existe qu'une seule instance de DiscussionController, alors il devrait s'y abonner pour savoir qu'un nouveau message est arrivé, récupérer son id et l'afficher.

    1/ Déjà, plutôt que d'utiliser maFonctionDeTest, est-ce-que je peux utiliser une fonction qui se trouve directement dans mon controller (ma page de tchat) ?

    En théorie oui, tu pourrais écrire:
    NotificationCenter.default.addObserver(discussionController, selector: #selector(didReceiveNewMessage), name: .NouveauMessage, object: nil).
    Mais ça ne présente pas d'intérêt: le DiscussionController devrait s'abonner lui-même.

    2/ Est-ce-que je peux remplir les variables de ce controller ?

    J'ai essayé avec :

    DiscussionController().IdDiscussion = "10"
    

    C'est normal.
    Là tu instancies un nouveau DiscussionController, mais tu ne le présentes pas. Il faut que tu conserves une référence vers le DiscussionController courant:

     private var discussionController: DiscussionController?
    

    Deux choix s'offrent à toi:
    1. soit à tout moment, il n'existe qu'une seule instance de DiscussionController et lui passer un nouveau idDiscussion rafraîchit son contenu. Utilise var idDiscussion { didSet {} } pour cela.
    2. soit quand on change de discussion, on crée un nouveau DiscussionController avec cette discussion, on retire celui qui est actuellement présenté puis on présente le nouveau.

    Je partirais plutôt sur la première solution, parce que de toute façon, de nouveaux messages peuvent arriver, et il faudra rafraichir l'écran. Le DiscussionController doit avoir un moyen de récupérer les messages.
    Mais ça se discute, parce que par exemple, tu pourrais avoir plusieurs discussions ouvertes en même temps, et ça a le côté pratique de laisser la discussion dans l'état où l'utilisateur l'a laissée.

  • Tu postes un signal dans le NotificationCenter et il avertit tous ses abonnés.

    Quand tu dis "il avertit tous ses abonné", je m'abonne à un signal avec addObserver() .. on est d'accord ? ^^

    Ensuite, il me semble que la seule chose que devrait faire la UIApplication sur la réception de la notification est éventuellement d'amener le DiscussionController au premier plan.

    Dans mon utilisation, j'étais parti sur ce genre de comportement :

    • Si je suis sur la conversation, j'ajoute le nouveau message
    • Si je ne suis pas dessus, j'affiche une bannière qui descend du haut de l'écran pour prévenir qu'il y a un nouveau message dans tel conversation

    Ce qui amène au deuxième point: il peut tout à fait y avoir plusieurs abonnés à une même notification. S'il n'existe qu'une seule instance de DiscussionController, alors il devrait s'y abonner pour savoir qu'un nouveau message est arrivé, récupérer son id et l'afficher.

    Il existe bien qu'une instance de DiscussionController.

    C'est normal.
    Là tu instancies un nouveau DiscussionController, mais tu ne le présentes pas. Il faut que tu conserves une référence vers le DiscussionController courant

    C'est ce que je m'étais dit, j'attendais confirmation ^^

    private var discussionController: DiscussionController?

    Mais cette déclaration, je l'a met où ? Dans mon AppDelegate nan ?
    Le but c'est que mon DiscussionController soit disponible de partout si je comprend bien ?

    Je partirais plutôt sur la première solution, parce que de toute façon, de nouveaux messages peuvent arriver, et il faudra rafraichir l'écran. Le DiscussionController doit avoir un moyen de récupérer les messages.

    Ouai, c'est le comportement choisi ici :)

    Merci de ton aide en tout cas ^^
    Je retourne tester tout ça :)

  • InsouInsou Membre
    novembre 2019 modifié #4

    Et rien à voir mais c'était une mauvaise idée de mettre l'ajout de l'observer dans applicationDidBecomeActive
    Je l'ai déplacé dans didFinishLaunchingWithOptions
    Comme ça je ne l'ai qu'une fois..
    Pcq si je met l'application en background et que je reviens dessus, il re-ajoute l'observer.. du coup il passe 2x dedans quand je reçois une notification "NouveauMessage".

    J'suppose que j'aurai pu aussi l'enlever quand l'application passe en background..
    ça revient au même au final.. (dites moi si j'met trompe ^^)

  • InsouInsou Membre
    novembre 2019 modifié #5

    Bon, du coup j'ai réorganiser un peu le code par contre, je me heurte au soucis évoqué plus haut, du fait que je re-déclare une nouvelle instance de mon controller..
    Lorsque je passe dans ma fonction pour récupérer mes nouveaux messages, impossible de refresh la tablebview (qui est à nil), ce qui est normal, vu que c'est un nouveau controller et pas celui qui est affiché..

    1. soit à tout moment, il n'existe qu'une seule instance de DiscussionController et lui passer un nouveau idDiscussion rafraîchit son contenu. Utilise var idDiscussion { didSet {} } pour cela.

    Je pense que c'est cette solution la plus adaptée mais je n'ai aucune idée de comment faire ça..

    Est-ce-que tu aurais un exemple de comment faire concrètement ?
    En gros, je cherche à appeler la fonction chargeMessages() qui est dans ma class DiscussionController depuis AppDelegate mais en ayant accès à la tableview présente dans la class.

    [Edit]
    Je continue mes recherches et j'ai l'impression que je cherche à faire un truc comme ça :

    NotificationCenter.default.addObserver(DiscussionController.self, selector: #selector(DiscussionController.recupMessages), name: .NouveauMessage, object: nil)

    Je le comprend comme : J'ajoute un observer .NouveauMessage sur DiscussionController qui déclenchera la fonction recupMessages

    Est-ce-que j'ai bon déjà dans mon cheminement ?

    En testant je me retrouve avec une erreur :
    unrecognized selector sent to class
    J'continue de chercher mais j'sais pas si je dois chercher à résoudre ce soucis et ensuite j'aurai accès à ma fonction, ma tableview, etc etc ou si je fais fausse route car j'y aurai quand même pas accès :neutral:

  • CéroceCéroce Membre, Modérateur

    @Insou a dit :

    Tu postes un signal dans le NotificationCenter et il avertit tous ses abonnés.

    Quand tu dis "il avertit tous ses abonné", je m'abonne à un signal avec addObserver() .. on est d'accord ? ^^

    Oui, c'est ça.

    private var discussionController: DiscussionController?

    Mais cette déclaration, je l'a met où ? Dans mon AppDelegate nan ?

    Dans l'objet parent du DiscussionController. Et je crois comprendre que c'est l'AppDelegate chez toi.
    (mauvais idée dans l'absolu, mais ne changeons pas de sujet).
    D'ailleurs ça va être assez ennuyeux parce que tu vas devoir écrire un truc du genre:

    func applicationDidLaunch()… {}
         let tabBarController = window?.rootViewController as? UITabBarController
         discussionViewController = tabBarController.viewControllers[2]
    }
    

    Ce n'est pas le code exact, mais tu vois l'idée: comme le Main.storyboard est instancié automatiquement, il faut ce moyen assez dégueulasse pour obtenir une référence sur les objets.

    Le but c'est que mon DiscussionController soit disponible de partout si je comprend bien ?

    Non, pas de partout, juste de l'objet parent. Il doit garder une référence pour pouvoir y accéder.
    L'AppDelegate n'est pas un fourre-tout.
    Si tu écris ça:

    let appDelegate = UIApplication.shared.delegate as? AppDelegate
    

    alors tu t'y prends mal, parce qu'il y a alors un couplage fort avec AppDelegate.

  • CéroceCéroce Membre, Modérateur

    @Insou a dit :
    J'suppose que j'aurai pu aussi l'enlever quand l'application passe en background..
    ça revient au même au final.. (dites moi si j'met trompe ^^)

    Non, tu n'as pas besoin, parce qu'en background la notification ne sera pas émise.
    Tu dois pouvoir abonne l'AppDelegate dans sa méthode init() tout simplement. Tu es sûr qu'elle ne sera appelée qu'une fois ;-)

  • CéroceCéroce Membre, Modérateur

    @Insou a dit :smile:

    Je pense que c'est cette solution la plus adaptée mais je n'ai aucune idée de comment faire ça..
    Est-ce-que tu aurais un exemple de comment faire concrètement ?

    // Dans AppDelegate
    
    NotificationCenter.default.addObserver(forName: .newMessage) { (_) in
        // Afficher un bandeau "Nouveau message"
    }
    
    // Dans DiscussionController
    
    NotificationCenter.default.addObserver(forName: .newMessage) { [weak self] (notification) in
        guard let discussionId = notification.userInfo("discussionId") as? String else {
            // Ne devrait pas arriver
            return
        }
    
        self?.loadDiscussion(id: discussionId)
    }
    

    En gros, je cherche à appeler la fonction chargeMessages() qui est dans ma class DiscussionController depuis AppDelegate mais en ayant accès à la tableview présente dans la class.

    Pour préciser: l'AppDelegate n'a pas à accéder à la tableview.

    Je le comprend comme : J'ajoute un observer .NouveauMessage sur DiscussionController qui déclenchera la fonction recupMessages
    Est-ce-que j'ai bon déjà dans mon cheminement ?

    Oui.

    Je continue mes recherches et j'ai l'impression que je cherche à faire un truc comme ça :

    NotificationCenter.default.addObserver(DiscussionController.self, selector: #selector(DiscussionController.recupMessages), name: .NouveauMessage, object: nil)

    unrecognized selector sent to class

    C'est normal, ce n'est pas la classe DiscussionController qui doit s'abonner, mais une instance de cette classe.

    J'ai l'impression que tu confonds classe et instance.
    La classe, c'est le moule qui permet de fabriquer les objets.
    L'instance, c'est l'exemplaire de l'objet.

  • Dans l'objet parent du DiscussionController. Et je crois comprendre que c'est l'AppDelegate chez toi.

    Je suis pas sûr..

    Je vais essayé de t'en dire plus sur l'appli :
    Quand je l'ouvre, j'ai une page de login, elle me renvoi vers un TabBarController (3 icônes en bas)
    Le dernier m'affiche la liste des discussions et lorsque j'en sélectionne une, j'arrive enfin sur mon DiscussionController

    Du coup, j'ai du mal à savoir ce que t'appelle "l'objet parent du DiscussionController" dans ce cas là..

    J'ai juste mis ce code dans AppDelegate parce que c'est ici que ça me semblait le plus approprié, pour gérer le déclenchement du NotificationCenter lorsque je reçois un push "nouveau message"

    Non, tu n'as pas besoin, parce qu'en background la notification ne sera pas émise.

    Ouai mais ce que je voulais dire c'est que quand je mettez l'application en background et que je l'a remettez au premier plan, je repassais dans applicationDidBecomeActive et du coup, je re-créai l'observer.
    Quand je recevais mon push, l'observer se déclenchait autant de fois que j'avais ré-ouvert mon appli..
    Du coup en effet, je l'ai déplacé pour qu'il ne se créer qu'une seule fois et c'est réglé ^^

    [Edit]

    Je vais tester des trucs avec le code de ton dernier message mais je pense qu'il va y avoir un soucis en faisant comme ça..

  • Bon, j'ai du un peu bidouiller mais ça fonctionne..
    J'trouve ma solution dégueulasse mais pour un vendredi soir, ça ira bien :#

    1/ J'ai créé une variable globale (vous l'a voyez venir la bidouille ?)
    var observerEnPlace = false

    2/ Dans AppDelegate

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    
    // vérifie sur quel controller je suis
    // Si je suis sur mon controller Discussion
    // j'envoi ma notification "NouvelleNotification"
    NotificationCenter.default..post(name: .NouveauMessage, object: self, userInfo: userInfo)
    // Sinon j'affiche le bandeau "Nouveau Message"
    // code bandeau ici
    }
    

    3/ Dans viewDidLoad sur DiscussionController

    // Si l'observer n'a pas été créé, je le crée
    if(observerEnPlace == false){
    NotificationCenter.default.addObserver(forName: .NouveauMessage, object: nil, queue: .init(), using: { (notification) in
                    guard let IdMessage = notification.userInfo!["IdMessage"] as? String else {
                        // Ne devrait pas arriver
                        return
                    }
                    print(IdMessage)
                    // traitement des infos ici
                    // ex: si je suis dans la bonne conversation, etc etc
                    self.recupNouveauxMessages() // j'ai accès aux fonctions de ma class, c'est super !
                })
                observerEnPlace = true // met la variable a true pour pas recréer un observer à chaque fois
    }
    

    Je passe par une variable globale que je change quand je crée mon observer, comme ça à chaque fois que je reviens sur mon DiscussionController, il ne recrée pas l'observer.
    Je suis pas fan de cette solution mais bon..

    J'ai essayé de créer l'observer et de le supprimer avec la méthode deinit, j'ai dû mal m'y prendre parce qu'il ne supprimait jamais l'observer, ça m'a gonflé donc je suis passé par la variable globale dégueulasse :disappointed:

    Je crois que je vais me contenter de ça pour le moment ^^

    En tout cas, merci de ton aide, je m'en serai pas sorti sinon :)

  • CéroceCéroce Membre, Modérateur

    Une solution plus propre est de s'abonner dans viewWillAppear() et se désabonner dans viewWillDisappear(). Ainsi il ne chargera pas inutilement les données s'il n'est pas visible.

  • ah bah je repassais ici pour dire que c'est ce que j'avais fait ^^

    La dernière fois j'avais du mal à supprimer l'observer du coup je tournais en rond mais après un petit week-end de repos et les idées plus claires, j'y suis arrivé ^^

    Merci de ton aide :D

Connectez-vous ou Inscrivez-vous pour répondre.