Pourquoi mon application SwiftUI plante-t-elle lors de la navigation vers l'arrière après avoir placé un `NavigationLink` à l'intérieur d'un` navigationBarItems` dans un `NavigationView`?

47

Exemple reproductible minimal (Xcode 11.2 beta, cela fonctionne dans Xcode 11.1):

struct Parent: View {
    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarItems(
                    trailing: NavigationLink(destination: Child(), label: { Text("Next") })
                )
        }
    }
}

struct Child: View {
    @Environment(\.presentationMode) var presentation
    var body: some View {
        Text("Hello, World!")
            .navigationBarItems(
                leading: Button(
                    action: {
                        self.presentation.wrappedValue.dismiss()
                    },
                    label: { Text("Back") }
                )
            )
    }
}

struct ContentView: View {
    var body: some View {
        Parent()
    }
}

Le problème semble résider dans le fait de placer mon NavigationLinkintérieur d'un navigationBarItemsmodificateur imbriqué dans une vue SwiftUI dont la vue racine est a NavigationView. Le rapport d'erreur indique que j'essaie d'accéder à un contrôleur de vue qui n'existe pas lorsque je navigue vers Childpuis vers Parent.

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Tried to pop to a view controller that doesn't exist.'
*** First throw call stack:

Si je devais plutôt placer cela NavigationLinkdans le corps de la vue comme ci-dessous, cela fonctionne très bien.

struct Parent: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: Child(), label: { Text("Next") })
        }
    }
}

S'agit-il d'un bogue SwiftUI ou d'un comportement attendu?

EDIT: J'ai ouvert un problème avec Apple dans leur assistant de rétroaction avec l'ID FB7423964au cas où quelqu'un d'Apple se soucierait de peser :).

EDIT: Mon ticket ouvert dans l'assistant de commentaires indique qu'il y a plus de 10 problèmes similaires signalés. Ils ont mis à jour la résolution avec Resolution: Potential fix identified - For a future OS update. Les doigts croisés que le correctif atterrit bientôt.

EDIT: Cela a été corrigé dans iOS 13.3!

Robert
la source
L'exemple que vous avez fourni ci-dessus fonctionne très bien avec Xcode 11.2 beta. Manquons-nous quelque chose ici?
Subramanian Mariappan
@SubramanianMariappan Cela fonctionne aussi très bien pour moi sur la version bêta 11.2.
Farhan Amjad
1
Intéressant, il se bloque à chaque fois pour moi. J'ai même essayé de créer un nouveau projet et de copier ce code exact à la place de ContentView.swift. Je vais modifier le message, mais le plantage ne se produit que lorsque vous naviguez vers l'avant puis vers l'arrière.
Robert
Grande question! Votre exemple se bloque pour moi à chaque fois aussi. Je viens de poster une nouvelle réponse qui fonctionne très bien pour moi. Faites-moi savoir si cela fonctionne aussi pour vous. Merci.
Chuck H
1
Merci pour les mises à jour concernant les billets Apple!
malte

Réponses:

20

C'était assez douloureux pour moi! Je l'ai laissé jusqu'à ce que la plupart de mon application soit terminée et que j'ai eu l'espace mental pour faire face au crash.

Je pense que nous pouvons tous convenir qu'il y a des choses assez géniales avec SwifUI mais que le débogage peut être difficile.

À mon avis, je dirais que c'est un BUG. Voici ma justification:

  • Si vous encapsulez l'appel de présentationModeMode dans un délai asynchrone d'environ une demi-seconde, vous devriez constater que le programme ne se bloquera plus.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
  • Cela me suggère que le bogue est un comportement inattendu en profondeur dans la façon dont SwiftUI s'interface avec tous les autres codes UIKit pour gérer les différentes vues. En fonction de votre code réel, vous constaterez peut-être qu'en cas de complexité mineure dans la vue, le crash ne se produira pas. Par exemple, si vous passez d'une vue à une qui a une liste et que cette liste est vide, vous obtiendrez un plantage sans le délai asynchrone. D'un autre côté, si vous n'avez qu'une seule entrée dans cette vue de liste, forçant une itération de boucle à générer la vue parent, vous verrez que le crash ne se produira pas.

Je ne suis pas sûr de la robustesse de ma solution d'envelopper l'appel de rejet dans un délai. Je dois le tester beaucoup plus. Si vous avez des idées à ce sujet, faites-le moi savoir! Je serais très heureux d'apprendre de vous!

Justin Ngan
la source
1
Très intelligent! Je n'y avais pas pensé. En espérant que cela se corrige bientôt!
Robert
1
@Robert A-t-il résolu votre problème? C'est difficile car un problème sans rapport que j'ai trouvé utilise un sélecteur dans les vues de navigation enfant. Pendant qu'un style de sélecteur segmenté fonctionne, la valeur par défaut semble provoquer un plantage au même point, lorsque vous cliquez sur le bouton de retour. Nous pouvons en discuter davantage si cela vous cause encore du chagrin. PS. Je déteste ma solution. C'est un hack mais c'est un qui ne devrait pas nécessiter de mise à jour du code si Apple corrige le problème de synchronisation.
Justin Ngan
2
Je suis d'accord que l'aspect temporel, ainsi que le fait que cela a bien fonctionné en 11.1 et fonctionne en dehors des .navigationBarItems()points pour que ce soit un bug.
John M.
3
Oui, je crois que c'est un bug et c'est mon principal candidat actuel pour le prix de la prime. Depuis qu'il me reste 4 jours sur la prime au moment de la rédaction de ce document, je tiens le coup au cas où quelqu'un viendrait avec de nouvelles informations :).
Robert
1
C'était une astuce très intéressante, merci pour ça! Malheureusement, je plante toujours de manière fiable l'application dans le simulateur 100% du temps: / Cela fonctionne mieux sur l'appareil, mais ce n'est pas sans planter du tout. Mais ce fut également le cas sans délai.
Kilian
15

Cela m'a également frustré pendant un certain temps. Au cours des derniers mois, en fonction de la version Xcode, de la version du simulateur et du type et / ou de la version réelle de l'appareil, il est passé de fonctionner à ne pas fonctionner à nouveau, apparemment au hasard. Cependant, récemment, cela a échoué de façon constante pour moi, alors hier, j'ai plongé profondément. J'utilise actuellement Xcode version 11.2.1 (11B500).

Il semble que le problème tourne autour de la barre de navigation et de la façon dont les boutons y ont été ajoutés. Donc, au lieu d'utiliser un NavigationLink () pour le bouton lui-même, j'ai essayé d'utiliser un Button () standard avec une action qui définit une variable @State qui active un NavigationLink caché. Voici un remplacement pour Robert's Parent View:

struct Parent: View {
    @State private var showingChildView = false
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World")
                NavigationLink(destination: Child(),
                               isActive: self.$showingChildView)
                { EmptyView() }
                    .frame(width: 0, height: 0)
                    .disabled(true)
                    .hidden()            
             }
             .navigationBarItems(
                 trailing: Button(action:{ self.showingChildView = true }) { Text("Next") }
             )
        }
    }
}

Pour moi, cela fonctionne de manière très cohérente sur tous les simulateurs et tous les appareils réels.

Voici mes vues d'aide:

struct HiddenNavigationLink<Destination : View>: View {

    public var destination:  Destination
    public var isActive: Binding<Bool>

    var body: some View {

        NavigationLink(destination: self.destination, isActive: self.isActive)
        { EmptyView() }
            .frame(width: 0, height: 0)
            .disabled(true)
            .hidden()
    }
}

struct ActivateButton<Label> : View where Label : View {

    public var activates: Binding<Bool>
    public var label: Label

    public init(activates: Binding<Bool>, @ViewBuilder label: () -> Label) {
        self.activates = activates
        self.label = label()
    }

    var body: some View {
        Button(action: { self.activates.wrappedValue = true }, label: { self.label } )
    }
}

Voici un exemple d'utilisation:

struct ContentView: View {
    @State private var showingAddView: Bool = false
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello, World!")
                HiddenNavigationLink(destination: AddView(), isActive: self.$showingAddView)
            }
            .navigationBarItems(trailing:
                HStack {
                    ActivateButton(activates: self.$showingAddView) { Image(uiImage: UIImage(systemName: "plus")!) }
                    EditButton()
            } )
        }
    }
}
Chuck H
la source
Je peux confirmer que cela fonctionne (vraiment bien pour un hack ;-))! Apple doit cependant résoudre ce problème dès que possible. Xcode 11.2.1, Catalina 10.15.2 (beta), iOS 13.2.2
P. Ent
1
Je suis d'accord à 100%. En général, en ce qui concerne la navigation dans SwiftUI, il y a beaucoup de choses qui sont soit cassées, soit tout simplement manquantes. Ce qui nous amène bien sûr au vrai problème. Il n'y a pas de "source de vérité" (c'est-à-dire de la documentation et des exemples) d'Apple, seulement des hacks comme nous. BTW, j'utilise tellement la technique ci-dessus, j'ai créé deux vues utilitaires qui aident beaucoup à la lisibilité. Je les ajouterai à ma réponse au cas où quelqu'un serait intéressé.
Chuck H
Merci pour la solution de contournement, cela fonctionne!
Stanislav Poslavsky
1
Cela ne fonctionne pas pour moi pour plus d'une navigation. Une fois que vous êtes revenu à l'écran précédent, le lien invisible ne fonctionne plus.
Jon Shier
1
J'ai plusieurs vrais appareils en 13.3 (build 17C54) et ils fonctionnent tous comme souhaité. Comme je fais presque tous mes tests sur de vrais appareils, je n'utilise pas très souvent le simulateur. Mais je viens d'essayer mon cas de test sur un simulateur 13.3 et le test échoue là-bas. J'ai remarqué que iOS 13.3 sur le simulateur Xcode est une version antérieure (17C45) à la mise à jour publique. Je serais intéressé de savoir si quelqu'un observe le comportement défaillant sur un appareil réel.
Chuck H
12

Il s'agit d'un bug majeur et je ne vois pas de bonne façon de le contourner. A bien fonctionné dans iOS 13 / 13.1 mais 13.2 se bloque.

Vous pouvez réellement le répliquer d'une manière beaucoup plus simple (ce code est littéralement tout ce dont vous avez besoin).

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Hello, World!").navigationBarTitle("To Do App")
                .navigationBarItems(leading: NavigationLink(destination: Text("Hi")) {
                    Text("Nav")
                    }
            )
        }
    }
}

J'espère qu'Apple le triera car il cassera sûrement des tas d'applications SwiftUI (y compris la mienne).

James
la source
Haha ... C'est assez génial. Vous avez accédé à une vue Texte qui, dans SwiftUI, est une vue! Ouais, ça devrait revenir à son parent, non? Pourtant, ce n'est pas le cas. Il est intéressant de noter que le comportement de votre exemple rompt l'interface utilisateur, mais ne provoque pas réellement un crash fatal.
Justin Ngan
Oui, la composabilité de SwiftUI (et React Native / Flutter, etc.) est incroyable. Vous donne tellement de contrôle / flexibilité (quand cela fonctionne au moins).
James
1
Confirmez que cela se bloque sur Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
Il ne plante plus en 13.3, mais la navigation ne semble fonctionner que la première fois que vous la déclenchez
James
6

Comme solution de contournement, basé sur la réponse de Chuck H ci-dessus, j'ai encapsulé le NavigationLink comme un élément caché:

struct HiddenNavigationLink<Content: View>: View {
var destination: Content
@Binding var activateLink: Bool

var body: some View {
    NavigationLink(destination: destination, isActive: self.$activateLink) {
        EmptyView()
    }
    .frame(width: 0, height: 0)
    .disabled(true)
    .hidden()
}
}

Ensuite, vous pouvez l'utiliser dans un NavigationView (ce qui est crucial) et le déclencher à partir d'un bouton dans une barre de navigation:

VStack {
    HiddenNavigationList(destination: SearchView(), activateLink: self.$searchActivated)
    ...
}
.navigationBarItems(trailing: 
    Button("Search") { self.searchActivated = true }
)

Enveloppez-le dans les commentaires "// HACK" donc quand Apple corrige cela, vous pouvez le remplacer.

P. Ent
la source
Cela ne semble fonctionner que lors de la première utilisation dans iOS 13.3.
James
3

Sur la base des informations que vous avez fournies et spécialement d'un commentaire que @Robert a fait à propos de l'emplacement de NavigationView, j'ai trouvé un moyen de contourner le problème au moins dans mon scénario spécifique.

Dans mon cas, j'avais un TabView qui était enfermé dans un NavigationView comme ceci:

struct ContentViewThatCrashes: View {
@State private var selection = 0

var body: some View {
    NavigationView{
        TabView(selection: $selection){
            NavigationLink(destination: NewView()){
                Text("First View")
                    .font(.title)
            }
            .tabItem {
                VStack {
                    Image("first")
                    Text("First")
                }
            }
            .tag(0)
            NavigationLink(destination: NewView()){
                Text("Second View")
                    .font(.title)
            }
            .tabItem {
                VStack {
                    Image("second")
                    Text("Second")
                }
            }
            .tag(1)
        }
    }
  }
}

Ce code se bloque car tout le monde signale dans iOS 13.2 et fonctionne dans iOS 13.1. Après quelques recherches, j'ai trouvé une solution à cette situation.

Fondamentalement, je déplace la NavigationView sur chaque écran séparément sur chaque onglet comme ceci:

struct ContentViewThatWorks: View {
@State private var selection = 0

var body: some View {
    TabView(selection: $selection){
        NavigationView{
            NavigationLink(destination: NewView()){
                Text("First View")
                    .font(.title)
            }
        }
        .tabItem {
            VStack {
                Image("first")
                Text("First")
            }
        }
        .tag(0)
        NavigationView{
            NavigationLink(destination: NewView()){
                Text("Second View")
                    .font(.title)
            }
        }
        .tabItem {
            VStack {
                Image("second")
                Text("Second")
            }
        }
        .tag(1)
    }
  }
}

Cela va à l'encontre de la prémisse de simplicité de SwiftUI, mais cela fonctionne sur iOS 13.2.

Julio Bailon
la source
cela fonctionne mais, le problème supprime tabViews sur le NewView.
VENDREDI
1
@FRIDDAY cet exemple fonctionne en 13.1 mais plante en 13.2. C'est un bug connu et mon intention était d'essayer d'aider quelqu'un dans le même scénario avec une solution de contournement
Julio Bailon
1

Xcode 11.2.1 Swift 5

JE L'AI! Il m'a fallu quelques jours pour comprendre celui-ci ...

Dans mon cas, lorsque j'utilise SwiftUI, j'obtiens un plantage uniquement si le bas de ma liste s'étend au-delà de l'écran, puis j'essaie de «déplacer» les éléments de la liste. Ce que j'ai fini par découvrir, c'est que si j'ai trop de "trucs" sous List (), alors il se bloque en déplacement. Par exemple, sous ma List (), j'avais un bouton Text (), Spacer (), Button (), Spacer () (). Si je commentais l'un de ces objets, je ne pouvais pas recréer le crash. Je ne sais pas quelles sont les limitations, mais si vous obtenez ce plantage, essayez de supprimer des objets sous votre liste pour voir si cela aide.

Dave Levy
la source
0

Bien que je ne puisse voir aucun plantage, votre code a quelques problèmes:

en définissant l'élément de tête, vous tuez réellement le comportement par défaut des transitions de navigation. (essayez de glisser du côté avant pour voir si cela fonctionne).

Donc pas besoin d'avoir un bouton là-bas. Laissez-le tel quel et vous avez un bouton de retour gratuit.

Et n'oubliez pas selon HIG , le titre du bouton de retour devrait montrer où il va, pas ce que c'est! Essayez donc de définir un titre pour la première page pour l'afficher sur l'un des boutons de retour qui y apparaît.

struct Parent: View {
    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarItems(
                    trailing: NavigationLink(destination: Child(), label: { Text("Next") })
                )
                .navigationBarTitle("First Page",displayMode: .inline)
        }
    }
}

struct Child: View {
    @Environment(\.presentationMode) var presentation
    var body: some View {
        Text("Hello, World!")
    }
}

struct ContentView: View {
    var body: some View {
        Parent()
    }
}
Mojtaba Hosseini
la source
1
Hé, merci pour la réponse. Bien que je convienne que laisser le comportement du bouton de retour par défaut est souhaitable, cela produit toujours un plantage.
Robert
Quelle version utilisez-vous? Je l'ai testé avant l'envoi. Vous avez peut-être un autre problème. Pouvez-vous fournir un exemple de projet s'il vous plaît?
Mojtaba Hosseini
1
Xcode 11.2 beta comme le dit la question. L'exemple que j'ai fourni dans la question est tout ce dont vous avez besoin pour reproduire le crash.
Robert
J'utilise la même version et le même code mais pas de plantage 🤔
Mojtaba Hosseini
1
Confirmez que cela se bloque sur Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
0

FWIW - Les solutions ci-dessus suggérant un Hack NavigationLink caché sont toujours la meilleure solution de contournement dans iOS 13.3b3. J'ai également déposé un FB7386339 pour la postérité, et j'ai été fermé de la même manière que les autres FB susmentionnés: "Correction potentielle identifiée - Pour une future mise à jour du système d'exploitation".

Doigts croisés.

Mike W.
la source
Veuillez éviter d'ajouter des commentaires comme réponses.
Karthick Ramesh
0

Il est résolu dans iOS 13.3. Mettez simplement à jour votre système d'exploitation et votre xCode.

VENDREDI
la source
1
Xcode 11.3 (11C29) sur 10.15.2 se traduit par un comportement différent pour moi: la navigation vers l'arrière fonctionne, mais ensuite le NavigationLink n'a plus de fonction. Cliquer dessus ne fait rien.
malte
@malte Il vaut mieux ouvrir une nouvelle question pour ça. Avant de vérifier votre code, donnez votre .buttonStyle(PlainButtonStyle())modificateur NavigationLink et réessayez. faites-moi savoir si vous avez posé une question.
VENDREDI
1
Tu as raison. Il s'avère qu'il y a déjà une nouvelle question: stackoverflow.com/questions/59279176/…
malte