SwiftUI Combine EnvironmentObject ObservableObject

Salut a tous !

Désolé si je n’écris pas dans une catégorie qui serait peut être plus appropriée mais c’est mon premier post sur ce forum… si un modérateur veux le bouger a la bonne place, ce n’est pas un problème. :grin:

Je suit entrain d’essayer de réécrire la structure de mon application en SwiftUI et j’ai du mal a comprendre l’utilisation de EnvironmentObject.

Mon app fonctionne de la façon suivante:

  • L’utilisateur lance l’application pour la premiere fois, il arrive sur une page d’accueil qui lui présente l’application et lui demande de choisir entre:

– « Continuer »:
Un UID est stocké dans LocalStorage et la page « MainView »

– « Se connecter avec un compte existant »:
La page « LoginView » est presentee pour se connecter au compte et récupérer le backup.

  • L’application a deja été lancée, la variable dans le LocalStorage est détectée et la page « MainView » est presentee directement.
import SwiftUI
import Combine

class ViewRouter: ObservableObject {
    
    let objectWillChange = PassthroughSubject<ViewRouter,Never>()
    
    var currentPage: String = "WelcomeView" {
        didSet {
            objectWillChange.send(self)
        }
    }
}
import SwiftUI

struct ParentView : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            if viewRouter.currentPage == "WelcomeView" {
                WelcomeView()
            }
            else if viewRouter.currentPage == "MainView" {
                MainView()
            }
            else if viewRouter.currentPage == "LoginView" {
                LoginView()
            }
        }
    }
}
import SwiftUI

struct WelcomeView: View {
    
    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        ZStack{
            // VStack { [plus de code ici] }
            VStack {
                    LoginButtons().environmentObject(ViewRouter())
            }
            // VStack { [et plus de code là] }
        }
    }
}
import SwiftUI

struct LoginButtons: View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {

            Button(action: {
                self.viewRouter.currentPage = "MainView"
            }) {
                Text("NEW USER")
            }
            
            Button(action: {
                self.viewRouter.currentPage = "LoginView"
            }) {
                Text("I ALREADY HAVE AN ACCOUNT")
            }
        }
    }
}
import SwiftUI

struct MainView : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            // Juste pour savoir si ça fonctionne bien
            Button(action: {
                self.viewRouter.currentPage = "WelcomeView"
            }) {
                Text("MainView - GO BACK")
            }
        }
    }
}
import SwiftUI

struct LoginView : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            // Juste pour savoir si ça fonctionne bien
            Button(action: {
                self.viewRouter.currentPage = "WelcomeView"
            }) {
                Text("LoginView - GO BACK")
            }
        }
    }
}

J’ai passé plusieurs heures à comprendre ce qu’il n’allait pas dans mon code…

Je passais dans mon fichier « WelcomeView » mon « .environmentObject(ViewRouter()) » avant de passer la valeur à ma Sub-View « LoginButtons() »:

import SwiftUI

struct WelcomeView: View {
    
    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        ZStack{
            // VStack { [...] }
            VStack {
                    LoginButtons()
                           // -->  .environmentObject(ViewRouter())
            }
            // VStack { [...] }
        }
    }
}

Mais je ne comprends pas pourquoi ca ne fonctionne pas… est-ce que quelqu’un pourrait m’expliquer ?

Et également comment debugger (lire la variable contenue dans ViewRouter.currentPage)?

Cedric.

Je ne sais pas où est ton problème, mais pour commencer ton ViewRouter utilises une vieille syntaxe, datant d’une des premières bêtas version de SwiftUI (celle utilisée par Maxime pour faire ses vidéos).

Il faut faire comme ça, maintenant :

class ViewRouter: ObservableObject {
  @Published var currentPage: String = "WelcomeView"
}

C’est nettement plus simple …

Ta variable viewRouter n’est jamais créé, du moins pas dans le code montré ici. Elle doit être initialisée dans le fichier SceneDelegate de l’application.

Ce petit tutoriel (in English) montre comment faire :

https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views

print (viewRouter.currentPage)

Ah ok - merci de me corriger @Published, je pensais au contraire que c’était la nouvelle syntaxe !
En effet, plus facile a utiliser et a retenir.

Il n’y a pas de variable « viewRouter » :

J’ai mon « ViewRouter:ObservableObject » qui contient la variable « currentPage » qui est ‹ observée ›, comme tu me l’a corrigée

@Published var currentPage: String = "WelcomeView"

Et si je comprends bien, cette variable est accessible dans toutes les Views de mon application, a partir du moment que j’appelle dans le Struct :

@EnvironmentObject var viewRouter: ViewRouter

Pour changer la valeur, je mets un nouveau String en utilisant :

self.viewRouter.currentPage = ""

Dès que le changement a été effectué (et vu qu’elle est observée par l’object ViewRouter), la View reload et affiche la bonne page.

C’est bien ça, n’est-ce pas ?

Mais là ou je bloque, c’est que lorsque je passe la variable à ma sous-vue « LoginButtons », les pages ne s’affichent pas.

LoginButtons().environmentObject(ViewRouter())

Et c’est là que je n’arrive pas à vérifier les valeur de ma variable…

Je ne sais pas comment appeler les fonctions comme print() ou printDebug() dans les struct (View).
Quelque chose comme en html <?php ?> peut être ?

Ou alors je me complique la vie et c’est bien plus simple que ca n’y parait !! :rofl:

Pour la part je me suis basé sur ceci pour mettre en place la logique de login avec Realm et ça fonctionne nickel

1 « J'aime »

Bonjour pour faire un print dans une View tu peux faire une extension

extension View {
func printing( _ value: A) -> Self {
print(value)return self
}
}
et utiliser .printing() dans la vue

1 « J'aime »

Je ne sais toujours pas où est ton problème. J’ai tapé un code similaire au tien, et ça fonctionne du premier coup :

import SwiftUI

class ViewRouter : ObservableObject {
  @Published var currentPage = "WelcomeView"
}

///
struct WelcomeView: View {
  @EnvironmentObject var viewRouter : ViewRouter
  
  var body: some View {
    VStack {
      Text("Ici, c'est WelcomeView")
        .font(.largeTitle)
        .padding()
      Button(action: {
          self.viewRouter.currentPage = "LoginView"
      }) {
          Text("GO => LoginView")
      } .padding()
      Button(action: {
          self.viewRouter.currentPage = "MainView"
      }) {
          Text("GO => MainView")
        } .padding()
    }
  }

}

///
struct LoginView: View {
  @EnvironmentObject var viewRouter : ViewRouter
  
  var body : some View {
    VStack {
      Text("Ici c'est LoginView").font(.largeTitle)
      Button(action: {
          self.viewRouter.currentPage = "WelcomeView"
      }) {
          Text("GO BACK")
        .padding()
      }
    }
  }
}

///
struct MainView: View {
  @EnvironmentObject var viewRouter : ViewRouter
  
  var body : some View {
    VStack {
      Text("Ici c'est MainView").font(.largeTitle)
      Button(action: {
          self.viewRouter.currentPage = "WelcomeView"
      }) {
          Text("GO BACK")
        .padding()
      }
    }
  }
}


struct ContentView: View {
    @EnvironmentObject var viewRouter : ViewRouter
  
    var body: some View {
      VStack {
        if viewRouter.currentPage == "WelcomeView" {
          WelcomeView()
        }
        if viewRouter.currentPage == "LoginView" {
          LoginView()
        }
        if viewRouter.currentPage == "MainView" {
          MainView()
        }
      }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
      ContentView().environmentObject(ViewRouter())
    }
}

Avec une petite modification du ficher SceneDelegate pour créer un objet de type ViewRouter dans l’environmentObjet au lancement de l’application :

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

    // Create the SwiftUI view that provides the window contents.
    let contentView = ContentView()

    // Use a UIHostingController as window root view controller.
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
      // window.rootViewController = UIHostingController(rootView: contentView)
        window.rootViewController = UIHostingController(rootView: contentView.environmentObject(ViewRouter()))
    
        self.window = window
        window.makeKeyAndVisible()
    }
  }
1 « J'aime »

Merci beaucoup Michel !

Ça ma aidé à trouver :

extension View {
    func Print(_ varToPrint: Any...) -> some View {
        for value in varToPrint { print(value) }
        return EmptyView()
    }
}

En fait c’est vraiment moi qui me compliquait la vie :sweat_smile:

Ja partais simplement du principe que la valeur devait « passer » dans chaque sous-vues par l’intermédiaire du parent… je veux dire que la valeur de « viewRouter » soit transmise de ParentView vers WelcomeView vers LoginButtons dans mon cas pour pouvoir être « vue / lue ».
Alors que pas du tout (et c’est en fait tout a fait normal :wink: ).

Merci en tout cas pour ton aide précieuse !

L’utilisation d’une chaîne de caractères pour communiquer entre plusieurs pages n’est pas une bonne idée. On peut se tromper en tapant : minuscule à la place d’une majuscule, caractères invisibles dans le texte à la suite d’une mauvaise manipulation, etc … C’est une source potentielle de bugs.

Il est préférable d’utiliser une énumération pour définir les différents états du système :

enum TypeIndexPage {
  case welcome, login, main
}

class ControleurPage : ObservableObject {
   @Published var index = TypeIndexPage.welcome
}

Cela sécurise le code et améliore sa lisibilité. Une erreur sur une énumération est détectée automatiquement par XCode, au lieu de se déclencher plus tard pendant l’utilisation de l’application.

struct ContentView: View {
    @EnvironmentObject var controleurPage : ControleurPage
  
    var body: some View {
      VStack {
        if self.controleurPage.index == .welcome {
          WelcomeView()
        }
        if self.controleurPage.index == .login {
          LoginView()
        }
        if self.controleurPage.index == .main {
          MainView()
        }
      }
    }
}

Très bonne idée !
J’utilise normalement un struct dans un fichier externe pour toutes mes constantes mais un enum est bien plus clair dans ce cas.