15
votes

Naviguer par programme vers une nouvelle vue dans SwiftUI

Exemple descriptif:

écran de connexion, l'utilisateur appuie sur le bouton «Connexion», la demande est effectuée, l'interface utilisateur affiche l'indicateur d'attente, puis après une réponse réussie, je voudrais naviguer automatiquement l'utilisateur vers l'écran suivant.

Comment puis-je réaliser une telle transition automatique dans SwiftUI?


0 commentaires

6 Réponses :


-1
votes

Maintenant, vous devez simplement créer une instance de la nouvelle vue vers laquelle vous souhaitez naviguer et la placer dans NavigationButton:

NavigationButton(destination: NextView(), isDetail: true, onTrigger: { () -> Bool in
    return self.done
}) {
    Text("Login")
}

Si vous retournez true onTrigger, cela signifie que vous vous êtes connecté avec succès.


1 commentaires

NavigationButton n'existe pas dans SwiftUI 2 . Mieux vaut supprimer une réponse.



18
votes

Vous pouvez remplacer la vue suivante par votre vue de connexion après une connexion réussie. Par exemple:

import Combine

class UserAuth: ObservableObject {

  let didChange = PassthroughSubject<UserAuth,Never>()

  // required to conform to protocol 'ObservableObject' 
  let willChange = PassthroughSubject<UserAuth,Never>()

  func login() {
    // login request... on success:
    self.isLoggedin = true
  }

  var isLoggedin = false {
    didSet {
      didChange.send(self)
    }

    // willSet {
    //       willChange.send(self)
    // }
  }
}

Vous devez gérer votre processus de connexion dans votre modèle de données et utiliser des liaisons telles que @EnvironmentObject pour transmettre isLoggedin à votre vue.

Remarque: dans Xcode version 11.0 beta 4 , pour se conformer au protocole 'BindableObject', la propriété willChange doit être ajoutée

struct LoginView: View {
    var body: some View {
        ...
    }
}

struct NextView: View {
    var body: some View {
        ...
    }
}

// Your starting view
struct ContentView: View {

    @EnvironmentObject var userAuth: UserAuth 

    var body: some View {
        if !userAuth.isLoggedin {
            LoginView()
        } else {
            NextView()
        }

    }
}


8 commentaires

Si je comprends bien votre concept, c'est comme avoir deux hiérarchies distinctes, une pour les cas authentifiés et une pour les nonautorisés montés à la racine même de l'arbre des vues


@zgorawski C'est vrai. Je n'ai pas encore trouvé de moyen de pousser manuellement une nouvelle vue avec SwiftUI. De cette manière, l'utilisateur ne peut pas revenir à l'écran de connexion sauf si vous définissez isLoggedin sur false.


c'est la solution que j'ai également faite dans Flutter. Malheureusement, il n'est élégant que dans un exemple de connexion ou dans un cas similaire «état global». Mais si vous souhaitez par exemple. confirmez une édition de données sur la vue des détails, affichez le spinner, puis affichez l'erreur ou rejetez la vue des détails - ce ne sera pas si simple.


@MoRezaFarahani J'utilise la version beta 7 et UserAuth est maintenant un ObservableObject. Dans mon ContentView, j'obtiens l'erreur "Function déclare un type de retour opaque, mais n'a pas d'instructions de retour dans son corps à partir de laquelle déduire un type sous-jacent" dans la ligne "var body: some View"


@MoRezaFarahani merci pour cela. Ma seule question est la suivante: est-il recommandé de créer la machine à états entière pour l'application dans un ContentView comme ci-dessus. Par exemple: struct BaseView: View { @ObservedObject var auth = Auth() var body: some View { if auth.empty { IntroView() } else if auth.unregistered { LoginView() } else { AppView() } // add other states in the application here } }


en utilisant cette méthode, je peux faire la transition. Mais y a-t-il de toute façon que je puisse fondre dans une autre vue en utilisant l'animation? J'ai essayé d'ajouter .opacity(!appSetting.isLoggedin ? 1 : 0) .animation(Animation.easeOut(duration: 0.5)) , qui fonctionne sur d'autres vues mais pas ici, et également envelopper la fermeture de mon bouton qui change le drapeau environmentObject isLoggedIn avec withAnimation { ... } . Les deux méthodes ne fonctionnent pas et mes opinions ont changé brusquement


Je suis nouveau sur SwiftUI, mais j'ai suivi cela exactement et j'obtiens cette erreur: Erreur fatale: Aucun objet ObservableObject de type UserAuth trouvé. Un View.environmentObject (_ :) pour UserAuth peut être manquant comme ancêtre de cette vue .: fichier SwiftUI, ligne 0. Cette erreur n'apparaît qu'au moment de l'exécution.


désolé, mais je devrai placer toutes les vues dans un fichier ou je peux utiliser votre méthode pour accéder à une autre vue de fichier?



4
votes

Pour référence future, comme un certain nombre d'utilisateurs ont signalé avoir obtenu l'erreur «La fonction déclare un type de retour opaque», pour implémenter le code ci-dessus à partir de @MoRezaFarahani, il faut la syntaxe suivante:

struct ContentView: View {

    @EnvironmentObject var userAuth: UserAuth 

    var body: some View {
        if !userAuth.isLoggedin {
            return AnyView(LoginView())
        } else {
            return AnyView(NextView())
        }

    }
}

Cela fonctionne avec Xcode 11.4 et Swift 5


1 commentaires

Est-ce que cela s'anime correctement? Je voudrais une animation push entre les deux vues.



1
votes

Pour expliquer ce que d'autres ont élaboré ci-dessus en fonction des modifications apportées à la combinaison à partir de la Swift Version 5.2 de Swift Version 5.2 il pourrait être simplifié en utilisant des éditeurs.

  1. Créez un nom de classe UserAuth comme indiqué ci-dessous, n'oubliez pas d'importer import Combine .
struct ContentView: View {
    @EnvironmentObject var userAuth: UserAuth 
    var body: some View {
        if !userAuth.isLoggedin {
                LoginView()
            } else {
                NextView()
            }
    }
  }
  1. Mettre à jour SceneDelegate.Swift avec

    let contentView = ContentView().environmentObject(UserAuth())

  2. Votre vue d'authentification

       struct NextView: View {
         var body: some View {
         ...
         }
       }
    
  3. Votre tableau de bord après une authentification réussie, si l'authentification userAuth.isLoggedin = true il sera chargé.

     struct LoginView: View {
        @EnvironmentObject  var  userAuth: UserAuth
        var body: some View {
            ...
        if ... {
        self.userAuth.login()
        } else {
        ...
        }
     }
    }
    
    
  4. Enfin, la vue initiale à charger une fois l'application lancée.

class UserAuth: ObservableObject {
        @Published var isLoggedin:Bool = false

        func login() {
            self.isLoggedin = true
        }
    }


0 commentaires

0
votes

Voici une extension sur UINavigationController qui a un simple push / pop avec des vues SwiftUI qui obtient les bonnes animations. Le problème que j'ai eu avec la plupart des navigations personnalisées ci-dessus était que les animations push / pop étaient désactivées. L'utilisation de NavigationLink avec une liaison isActive est la bonne façon de le faire, mais elle n'est ni flexible ni évolutive. L'extension ci-dessous a donc fait l'affaire pour moi:

var appNavigationController = UINavigationController.init(rootView: rootView)
window.rootViewController = appNavigationController
window.makeKeyAndVisible()

// Now you can use appNavigationController like any UINavigationController, but with SwiftUI views i.e. 
appNavigationController.pushView(view: AnyView(MySwiftUILoginView()))

Voici un exemple rapide d'utilisation de ceci pour window.rootViewController .

/**
 * Since SwiftUI doesn't have a scalable programmatic navigation, this could be used as
 * replacement. It just adds push/pop methods that host SwiftUI views in UIHostingController.
 */
extension UINavigationController: UINavigationControllerDelegate {

    convenience init(rootView: AnyView) {
        let hostingView = UIHostingController(rootView: rootView)
        self.init(rootViewController: hostingView)

        // Doing this to hide the nav bar since I am expecting SwiftUI
        // views to be wrapped in NavigationViews in case they need nav.
        self.delegate = self
    }

    public func pushView(view:AnyView) {
        let hostingView = UIHostingController(rootView: view)
        self.pushViewController(hostingView, animated: true)
    }

    public func popView() {
        self.popViewController(animated: true)
    }

    public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        navigationController.navigationBar.isHidden = true
    }
}


0 commentaires

2
votes
struct LoginView: View {
    
    @State var isActive = false
    @State var attemptingLogin = false
    
    var body: some View {
        ZStack {
            NavigationLink(destination: HomePage(), isActive: $isActive) {
                Button(action: {
                    attlempinglogin = true
                    // Your login function will most likely have a closure in 
                    // which you change the state of isActive to true in order 
                    // to trigger a transition
                    loginFunction() { response in
                        if response == .success {
                            self.isActive = true
                        } else {
                            self.attemptingLogin = false
                        }
                    }
                }) {
                    Text("login")
                }
            }
            
            WaitingIndicator()
                .opacity(attemptingLogin ? 1.0 : 0.0)
        }
    }
}
Use Navigation link with the $isActive binding variable

0 commentaires