Warum stürzt meine SwiftUI-App ab, wenn Sie rückwärts navigieren, nachdem Sie einen "NavigationLink" in einem "navigationBarItems" in einer "NavigationView" platziert haben?

47

Minimal reproduzierbares Beispiel (Xcode 11.2 Beta, dies funktioniert in 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()
    }
}

Das Problem scheint darin zu liegen, dass ich mich NavigationLinkin einem navigationBarItemsModifikator befinde, der in einer SwiftUI-Ansicht verschachtelt ist, deren Stammansicht a ist NavigationView. Der Absturzbericht zeigt an, dass ich versuche, zu einem Ansichts-Controller zu wechseln, der nicht vorhanden ist, wenn ich vorwärts zu Childund dann zurück zu navigiere Parent.

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

Wenn ich das stattdessen NavigationLinkwie unten im Hauptteil der Ansicht platzieren würde, funktioniert es einwandfrei.

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

Ist dies ein SwiftUI-Fehler oder ein erwartetes Verhalten?

BEARBEITEN: Ich habe ein Problem mit Apple in seinem Feedback-Assistenten mit der ID eröffnet, FB7423964falls jemand von Apple da draußen abwägen möchte :).

BEARBEITEN: Mein offenes Ticket im Feedback-Assistenten zeigt an, dass mehr als 10 ähnliche Probleme gemeldet wurden. Sie haben die Auflösung mit aktualisiert Resolution: Potential fix identified - For a future OS update. Daumen drücken, dass der Fix bald landet.

EDIT: Dies wurde in iOS 13.3 behoben!

Robert
quelle
Das oben angegebene Beispiel funktioniert problemlos mit Xcode 11.2 Beta. Vermissen wir hier etwas?
Subramanian Mariappan
@SubramanianMariappan Es funktioniert auch gut für mich in der Beta 11.2.
Farhan Amjad
1
Interessant, es stürzt jedes Mal für mich ab. Ich habe sogar versucht, ein neues Projekt zu erstellen und genau diesen Code anstelle von zu kopieren ContentView.swift. Ich werde den Beitrag bearbeiten, aber der Absturz tritt nur auf, wenn Sie vorwärts und dann zurück navigieren.
Robert
Gute Frage! Ihr Beispiel hier stürzt auch jedes Mal für mich ab. Ich habe gerade eine neue Antwort gepostet, die für mich sehr gut funktioniert. Lassen Sie mich wissen, ob es auch für Sie funktioniert. Vielen Dank.
Chuck H
1
Vielen Dank für die Updates bezüglich der Apple Tickets!
Malte

Antworten:

20

Das war ein ziemlicher Schmerzpunkt für mich! Ich habe es verlassen, bis der größte Teil meiner App fertig war und ich den Verstand hatte, um mit dem Absturz fertig zu werden.

Ich denke, wir können uns alle einig sein, dass SwifUI einige großartige Dinge enthält, aber dass das Debuggen schwierig sein kann.

Meiner Meinung nach würde ich sagen, dass dies ein Fehler ist. Hier ist meine Begründung:

  • Wenn Sie den Aufruf zum Schließen des Präsentationsmodus mit einer asynchronen Verzögerung von etwa einer halben Sekunde abschließen, sollten Sie feststellen, dass das Programm nicht mehr abstürzt.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
  • Dies deutet darauf hin, dass der Fehler ein unerwartetes Verhalten ist, das tief in der Schnittstelle von SwiftUI mit allen anderen UIKit-Codes zur Verwaltung der verschiedenen Ansichten liegt. Abhängig von Ihrem tatsächlichen Code stellen Sie möglicherweise fest, dass der Absturz tatsächlich nicht auftritt, wenn die Ansicht geringfügig komplexer ist. Wenn Sie beispielsweise von einer Ansicht zu einer Ansicht mit einer Liste wechseln und diese Liste leer ist, kommt es ohne asynchrone Verzögerung zu einem Absturz. Wenn Sie jedoch nur einen Eintrag in dieser Listenansicht haben und eine Schleifeniteration zum Generieren der übergeordneten Ansicht erzwingen, wird der Absturz nicht auftreten.

Ich bin mir nicht sicher, wie robust meine Lösung ist, den Entlassungsaufruf verzögert zu verpacken. Ich muss es noch viel mehr testen. Wenn Sie Ideen dazu haben, lassen Sie es mich bitte wissen! Ich würde mich sehr freuen, von Ihnen zu lernen!

Justin Ngan
quelle
1
Sehr schlau! Daran hatte ich nicht gedacht. Ich hoffe, es wird bald behoben!
Robert
1
@ Robert Hat es dein Problem behoben? Dies ist eine schwierige Frage, da ein nicht verwandtes Problem, das ich gefunden habe, die Verwendung eines Pickers in untergeordneten Navigationsansichten ist. Während ein segmentierter Auswahlstil funktioniert, scheint die Standardeinstellung an derselben Stelle einen Absturz zu verursachen, wenn Sie auf die Schaltfläche "Zurück" klicken. Wir können weiter diskutieren, ob es dir immer noch Kummer macht. PS. Ich hasse meine Lösung. Es ist ein Hack, aber es sollte kein Code-Update erforderlich sein, wenn Apple das Timing-Problem behebt.
Justin Ngan
2
Ich bin damit einverstanden, dass der Timing-Aspekt zusammen mit der Tatsache, dass es in 11.1 gut funktioniert hat und außerhalb der .navigationBarItems()Punkte funktioniert, dass dies ein Fehler ist.
John M.
3
Ja, ich glaube, es ist ein Fehler und dies ist mein derzeit führender Kandidat für den Bounty Award. Da ich zum Zeitpunkt dieses Schreibens noch 4 Tage Zeit habe, halte ich mich nur zurück, falls jemand neue Informationen mitbringt :).
Robert
1
Dies war ein sehr interessanter Tipp, danke dafür! Leider stürze ich die App im Simulator immer noch zu 100% zuverlässig ab: / Sie funktioniert auf dem Gerät besser, ist aber nicht ohne Absturz. Das war aber auch ohne Verzögerung der Fall.
Kilian
15

Das hat mich auch schon seit einiger Zeit frustriert. In den letzten Monaten hat sich die Xcode-Version, die Simulatorversion und der tatsächliche Gerätetyp und / oder die Version scheinbar zufällig von der Arbeit zur Nicht-Arbeit entwickelt. In letzter Zeit ist es für mich jedoch immer wieder gescheitert, und gestern habe ich mich eingehend damit befasst. Ich verwende derzeit Xcode Version 11.2.1 (11B500).

Es sieht so aus, als ob sich das Problem um die Navigationsleiste und die Art und Weise dreht, wie die Schaltflächen hinzugefügt wurden. Anstatt einen NavigationLink () für die Schaltfläche selbst zu verwenden, habe ich versucht, einen Standard-Button () mit einer Aktion zu verwenden, mit der eine @ State-Variable festgelegt wird, die einen versteckten NavigationLink aktiviert. Hier ist ein Ersatz für Roberts Elternansicht:

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") }
             )
        }
    }
}

Für mich funktioniert dies sehr konsistent über alle Simulatoren und alle realen Geräte hinweg.

Hier sind meine Helferansichten:

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 } )
    }
}

Hier ist ein Beispiel für die Verwendung:

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.
quelle
Ich kann bestätigen, dass dies funktioniert (wirklich gut für einen Hack ;-))! Apple muss dies jedoch so schnell wie möglich beheben. Xcode 11.2.1, Catalina 10.15.2 (Beta), iOS 13.2.2
P. Ent
1
Ich stimme zu 100% zu%. Im Allgemeinen gibt es in Bezug auf die Navigation in SwiftUI eine Menge, die entweder kaputt ist oder einfach fehlt. Was uns natürlich zum eigentlichen Problem führt. Es gibt keine "Quelle der Wahrheit" (dh Dokumentation und Beispiele) von Apple, nur Hacks wie wir. Übrigens verwende ich die obige Technik so oft, dass ich zwei Dienstprogrammansichten erstellt habe, die die Lesbarkeit erheblich verbessern. Ich werde sie meiner Antwort hinzufügen, falls jemand interessiert ist.
Chuck H
Vielen Dank für die Problemumgehung, es funktioniert einfach!
Stanislav Poslavsky
1
Dies funktioniert bei mir nicht für mehr als eine Navigation. Sobald Sie zum vorherigen Bildschirm zurückgekehrt sind, funktioniert der unsichtbare Link nicht mehr.
Jon Shier
1
Ich habe mehrere echte Geräte bei 13.3 (Build 17C54) und alle funktionieren wie gewünscht. Da ich fast alle Tests auf realen Geräten durchführe, benutze ich den Simulator nicht sehr oft. Aber ich habe gerade meinen Testfall auf einem 13.3-Simulator ausprobiert und der Test schlägt dort fehl. Ich habe festgestellt, dass iOS 13.3 im Xcode-Simulator ein früherer Build (17C45) ist als das öffentliche Update. Es würde mich interessieren, ob jemand das fehlerhafte Verhalten auf einem realen Gerät beobachtet.
Chuck H
12

Dies ist ein schwerwiegender Fehler, und ich kann keinen geeigneten Weg finden, um ihn zu umgehen. Funktionierte gut in iOS 13 / 13.1, aber 13.2 stürzt ab.

Sie können es tatsächlich viel einfacher replizieren (dieser Code ist buchstäblich alles, was Sie brauchen).

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

Hoffe, Apple schafft es, da es sicherlich viele SwiftUI-Apps (einschließlich meiner) kaputt machen wird.

James
quelle
Haha ... Das ist ziemlich großartig. Sie haben zu einer Textansicht navigiert, die in SwiftUI eine Ansicht ist! Ja, das sollte zurück zu seinem Elternteil navigieren, nicht wahr? Das tut es jedoch nicht. Es ist interessant, dass das Verhalten in Ihrem Beispiel die Benutzeroberfläche beschädigt, aber keinen tödlichen Absturz verursacht.
Justin Ngan
Ja, die Kompositionsfähigkeit von SwiftUI (und React Native / Flutter usw.) ist unglaublich. Gibt Ihnen so viel Kontrolle / Flexibilität (wenn es zumindest funktioniert).
James
1
Bestätigen Sie, dass dies auf Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2) abstürzt
P. Ent
In 13.3 stürzt es nicht mehr ab, die Navigation scheint jedoch nur beim ersten Auslösen zu funktionieren James♂ trigger
James
6

Um dieses Problem zu umgehen, habe ich basierend auf der obigen Antwort von Chuck H den NavigationLink als verstecktes Element gekapselt:

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()
}
}

Dann können Sie es in einer Navigationsansicht verwenden (was entscheidend ist) und es über eine Schaltfläche in einer Navigationsleiste auslösen:

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

Schließen Sie dies in "// HACK" -Kommentare ein, damit Sie es ersetzen können, wenn Apple dies behebt.

P. Ent
quelle
Dies scheint nur bei der ersten Verwendung in iOS 13.3 zu funktionieren.
James
3

Basierend auf den Informationen, die ihr bereitgestellt habt, und speziell einem Kommentar, den @Robert dazu gemacht hat, wo sich die Navigationsansicht befindet, habe ich einen Weg gefunden, das Problem zumindest in meinem speziellen Szenario zu umgehen.

In meinem Fall hatte ich eine TabView, die in einer Navigationsansicht wie folgt eingeschlossen war:

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)
        }
    }
  }
}

Dieser Code stürzt ab, da alle Benutzer in iOS 13.2 Bericht erstatten und in iOS 13.1 funktionieren. Nach einigen Recherchen habe ich eine Problemumgehung für diese Situation gefunden.

Grundsätzlich verschiebe ich die Navigationsansicht wie folgt auf jeden Bildschirm auf jeder Registerkarte:

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)
    }
  }
}

Irgendwie widerspricht die SwiftUI-Prämisse der Einfachheit, aber es funktioniert unter iOS 13.2.

Julio Bailon
quelle
Dies funktioniert, aber das Problem besteht darin, tabViews in NewView zu entfernen.
Freitag,
1
@FRIDDAY Dieses Beispiel funktioniert in 13.1, stürzt jedoch in 13.2 ab. Es ist ein bekannter Fehler und meine Absicht war es, jemandem im selben Szenario bei einer Problemumgehung zu helfen
Julio Bailon
1

Xcode 11.2.1 Swift 5

VERSTANDEN! Ich habe ein paar Tage gebraucht, um das herauszufinden ...

In meinem Fall, wenn ich SwiftUI verwende, kommt es nur dann zu einem Absturz, wenn sich das Ende meiner Liste über den Bildschirm hinaus erstreckt und ich versuche, alle Listenelemente zu "verschieben". Am Ende habe ich herausgefunden, dass wenn ich zu viel "Zeug" unter der Liste () habe, es unterwegs abstürzt. Zum Beispiel hatte ich unter meiner Liste () einen Text (), einen Spacer (), einen Button (), einen Spacer () Button (). Wenn ich EINES dieser Objekte auskommentierte, konnte ich den Absturz plötzlich nicht mehr neu erstellen. Ich bin nicht sicher, was die Einschränkungen sind, aber wenn Sie diesen Absturz bekommen, versuchen Sie, Objekte unter Ihrer Liste zu entfernen, um zu sehen, ob es hilft.

Dave Levy
quelle
0

Obwohl ich keine Abstürze sehen kann, weist Ihr Code einige Probleme auf:

Durch Festlegen des führenden Elements wird das Standardverhalten der Navigationsübergänge tatsächlich beendet. (Wischen Sie von der Vorderseite, um zu sehen, ob es funktioniert.)

Es muss also kein Knopf vorhanden sein. Lass es einfach so wie es ist und du hast einen kostenlosen Zurück-Button.

Und vergessen Sie laut HIG nicht , dass der Titel des Zurück-Buttons zeigen sollte, wohin es geht, nicht was es ist! Versuchen Sie also, einen Titel für die erste Seite festzulegen, um eine der beliebigen Zurück-Schaltflächen anzuzeigen.

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
quelle
1
Hey, danke für die Antwort. Ich stimme zwar zu, dass es wünschenswert ist, das Standardverhalten der Zurück-Schaltfläche beizubehalten, es führt jedoch dennoch zu einem Absturz.
Robert
Welche Version verwenden Sie? Ich habe es vor dem Senden getestet. Vielleicht haben Sie ein anderes Problem. Können Sie bitte ein Beispielprojekt zur Verfügung stellen?
Mojtaba Hosseini
1
Xcode 11.2 Beta wie die Frage sagt. Das Beispiel, das ich in der Frage angegeben habe, ist alles, was Sie brauchen, um den Absturz zu reproduzieren.
Robert
Ich benutze die gleiche Version und den gleichen Code, aber keine Abstürze 🤔
Mojtaba Hosseini
1
Bestätigen Sie, dass dies auf Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2) abstürzt
P. Ent
0

FWIW - Die oben genannten Lösungen, die einen versteckten NavigationLink-Hack vorschlagen, sind immer noch die beste Problemumgehung in iOS 13.3b3. Ich habe auch einen FB7386339 für die Nachwelt eingereicht und wurde ähnlich wie bei anderen oben genannten FBs geschlossen: "Möglicher Fix identifiziert - Für ein zukünftiges Betriebssystem-Update".

Daumen drücken.

Mike W.
quelle
Bitte vermeiden Sie das Hinzufügen von Kommentaren als Antworten.
Karthick Ramesh
0

Es ist in iOS 13.3 gelöst. Aktualisieren Sie einfach Ihr Betriebssystem und Ihren xCode.

FREITAG
quelle
1
Xcode 11.3 (11C29) auf 10.15.2 führt für mich zu einem anderen Verhalten: Die Rückwärtsnavigation funktioniert, aber danach hat der NavigationLink keine Funktion mehr. Ein Klick macht nichts.
Malte
@malte Es ist besser, dafür eine neue Frage zu eröffnen. Bevor ich Ihren Code überprüfe, geben Sie Ihren NavigationLink- .buttonStyle(PlainButtonStyle())Modifikator an und versuchen Sie es erneut. Lassen Sie mich wissen, wenn Sie eine Frage gestellt haben.
Freitag,
1
Du hast recht. Es stellt sich heraus, dass es bereits eine neue Frage gibt: stackoverflow.com/questions/59279176/…
malte