web-dev-qa-db-fra.com

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`?

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 placement de mon NavigationLink à l'intérieur d'un modificateur navigationBarItems qui est imbriqué dans une vue SwiftUI dont la vue racine est un NavigationView. Le rapport de plantage indique que j'essaie de passer à un contrôleur de vue qui n'existe pas lorsque je navigue vers Child puis 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 à la place placer ce NavigationLink dans 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 commentaires avec l'ID FB7423964 au cas où quelqu'un de Apple se soucie 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!

47
Robert

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 impressionnantes 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ésentation PresentationMode 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 plantage 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!

18
Justin Ngan

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 un @State var 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()
            } )
        }
    }
}
15
Chuck H

Il s'agit d'un bogue 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")
                    }
            )
        }
    }
}

Hope Apple triez-le car il va sûrement casser des charges d'applications SwiftUI (y compris la mienne).

12
James

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 ceci dans des commentaires "// HACK" donc quand Apple corrige cela, vous pouvez le remplacer.

6
P. Ent

Sur la base des informations que vous avez fournies et en particulier d'un commentaire que @Robert a fait sur l'emplacement du 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 de contournement à 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)
    }
  }
}

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

3
Julio Bailon

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()
    }
}
0
Mojtaba Hosseini

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

0
FRIDDAY

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.

0
Dave Levy

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.

0
Mike W.