Dlaczego moja aplikacja SwiftUI ulega awarii podczas nawigacji do tyłu po umieszczeniu `NavigationLink` wewnątrz` navigationBarItems` w `NavigationView`?

47

Minimalny odtwarzalny przykład (Xcode 11.2 beta, działa w 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()
    }
}

Problem polega na tym, że umieszczam moje NavigationLinkwnętrze navigationBarItemsmodyfikatora zagnieżdżonego w widoku SwiftUI, którego widokiem głównym jest NavigationView. Raport o awarii wskazuje, że próbuję przejść do kontrolera widoku, który nie istnieje, gdy nawiguję do przodu, Childa następnie z powrotem do Parent.

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

Gdybym zamiast tego umieścił to NavigationLinkw treści widoku, jak poniżej, działa to dobrze.

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

Czy to błąd SwiftUI lub oczekiwane zachowanie?

EDYCJA: Otworzyłem problem z Apple w ich asystencie zwrotnym z identyfikatorem FB7423964na wypadek, gdyby ktokolwiek z Apple miał ochotę ważyć :).

EDYCJA: Mój otwarty bilet w asystencie opinii wskazuje, że zgłoszono ponad 10 podobnych problemów. Zaktualizowali rozdzielczość o Resolution: Potential fix identified - For a future OS update. Trzymałem kciuki, że ta niedługo ląduje.

EDYCJA: Naprawiono to w iOS 13.3!

Robert
źródło
Powyższy przykład działa dobrze z Xcode 11.2 beta. Czy coś tu brakuje?
Subramanian Mariappan
@SubramanianMariappan Działa również dobrze dla mnie w wersji 11.2 beta.
Farhan Amjad
1
Co ciekawe, za każdym razem ulega awarii. Próbowałem nawet stworzyć nowy projekt i skopiować dokładnie ten kod zamiast ContentView.swift. Zrobię edycję wpisu, ale awaria zdarza się tylko wtedy, gdy nawigujesz do przodu, a następnie do tyłu.
Robert
Świetne pytanie! Twój przykład tutaj również się zawiesza. Właśnie opublikowałem nową odpowiedź, która działa dla mnie bardzo dobrze. Daj mi znać, czy to również działa dla Ciebie. Dzięki.
Chuck H
1
Dziękujemy za aktualizacje dotyczące biletów na jabłka!
malte

Odpowiedzi:

20

To było dla mnie dość uciążliwe! Zostawiłem to, dopóki większość mojej aplikacji nie została ukończona, i miałem przestrzeń umysłu, aby poradzić sobie z awarią.

Myślę, że wszyscy możemy się zgodzić, że w SwifUI jest kilka niesamowitych rzeczy, ale debugowanie może być trudne.

Moim zdaniem powiedziałbym, że to BŁĄD. Oto moje uzasadnienie:

  • Jeśli zamkniesz wywołanie trybu prezentacji w trybie asynchronicznym z opóźnieniem wynoszącym około pół sekundy, program powinien przestać się zawieszać.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
    
  • To sugeruje mi, że błąd jest nieoczekiwanym zachowaniem głęboko w tym, jak SwiftUI łączy się z całym innym kodem UIKit w celu zarządzania różnymi widokami. W zależności od rzeczywistego kodu może się okazać, że jeśli w widoku występuje niewielka złożoność, awaria faktycznie nie nastąpi. Na przykład, jeśli usuwasz widok z widoku, który ma listę, a ta lista jest pusta, nastąpi awaria bez asynchronicznego opóźnienia. Z drugiej strony, jeśli masz tylko jeden wpis w tym widoku listy, co wymusza iterację pętli w celu wygenerowania widoku nadrzędnego, zobaczysz, że awaria nie nastąpi.

Nie jestem do końca pewien, jak solidne jest moje rozwiązanie polegające na opóźnieniu odwołania. Muszę to przetestować o wiele bardziej. Jeśli masz pomysły na ten temat, daj mi znać! Byłbym bardzo szczęśliwy, mogąc uczyć się od ciebie!

Justin Ngan
źródło
1
Bardzo mądry! Nie myślałem o tym. Mam nadzieję, że wkrótce zostanie naprawiony!
Robert,
1
@Robert Czy to rozwiązało problem? Jest to trudne, ponieważ odkryłem, że niezwiązanym z tym problemem jest użycie narzędzia wyboru w wewnętrznych widokach nawigacji podrzędnej. Podczas gdy styl selektora podzielonego na segmenty działa, domyślnie wydaje się powodować awarię w tym samym punkcie po kliknięciu przycisku Wstecz. Możemy dyskutować dalej, jeśli nadal przynosi ci to smutek. PS. Nienawidzę mojego rozwiązania. To hack, ale nie powinien wymagać aktualizacji kodu, jeśli Apple naprawi problem z timingiem.
Justin Ngan
2
Zgadzam się, że aspekt czasowy, a także fakt, że działał dobrze w 11.1 i działa poza .navigationBarItems()punktami, jest to błąd.
John M.
3
Tak, uważam, że to błąd, i to jest mój obecny wiodący kandydat do nagrody głównej. Ponieważ mam jeszcze 4 dni na nagrodę w chwili pisania tego tekstu, po prostu trzymam się na wypadek, gdyby ktoś otrzymał nowe informacje :).
Robert
1
To była bardzo interesująca wskazówka, dzięki za to! Niestety wciąż niezawodnie zawieszam aplikację w symulatorze przez 100% czasu: / Działa lepiej na urządzeniu, ale wcale nie jest bez awarii. Tak też było bez opóźnień.
Kilian
15

Sfrustrowało mnie to także od dłuższego czasu. W ciągu ostatnich kilku miesięcy, w zależności od wersji Xcode, wersji symulatora oraz rzeczywistego typu i / lub wersji urządzenia, zmieniło się ono z pracy w niedziałającą, ponownie na pozór losowo. Jednak ostatnio mi się to nie udaje, więc wczoraj głęboko się w to zanurzyłem. Obecnie używam Xcode w wersji 11.2.1 (11B500).

Wygląda na to, że problem dotyczy paska nawigacji i sposobu dodania do niego przycisków. Zamiast używać NavigationLink () dla samego przycisku, próbowałem użyć standardowego Button () z akcją, która ustawia zmienną @State, która aktywuje ukryty NavigationLink. Oto zamiennik widoku rodzica Roberta:

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

Dla mnie działa to bardzo konsekwentnie we wszystkich symulatorach i wszystkich rzeczywistych urządzeniach.

Oto moje widoki pomocników:

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

Oto przykład użycia:

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.
źródło
Mogę potwierdzić, że to działa (naprawdę dobrze na hack ;-))! Apple musi to naprawić jak najszybciej. Xcode 11.2.1, Catalina 10.15.2 (beta), iOS 13.2.2
P. Ent
1
Zgadzam się w 100%. Ogólnie rzecz biorąc, jeśli chodzi o nawigację w SwiftUI, wiele jest uszkodzonych lub po prostu brakuje. Co oczywiście prowadzi nas do prawdziwego problemu. Nie ma „źródła prawdy” (tj. Dokumentacji i przykładów) od Apple, tylko hacki takie jak my. BTW, tak często używam powyższej techniki, że stworzyłem dwa widoki narzędzi, które bardzo pomagają w czytelności. Dodam je do mojej odpowiedzi na wypadek, gdyby ktoś był zainteresowany.
Chuck H
Dzięki za obejście, to po prostu działa!
Stanislav Poslavsky
1
To nie działa dla mnie dla więcej niż jednej nawigacji. Po powrocie do poprzedniego ekranu niewidoczny link nie działa.
Jon Shier
1
Mam kilka rzeczywistych urządzeń w 13.3 (kompilacja 17C54) i wszystkie działają zgodnie z życzeniem. Ponieważ prawie wszystkie testy przeprowadzam na prawdziwych urządzeniach, nie używam często symulatora. Ale właśnie wypróbowałem mój przypadek testowy na symulatorze 13.3 i tam test się nie powiedzie. Zauważyłem, że iOS 13.3 na symulatorze Xcode jest wcześniejszą wersją (17C45) niż publiczna aktualizacja. Byłbym zainteresowany dowiedzieć się, czy ktokolwiek zauważy nieprawidłowe działanie na prawdziwym urządzeniu.
Chuck H
12

Jest to poważny błąd i nie widzę odpowiedniego sposobu na obejście go. Działa dobrze w iOS 13 / 13.1, ale awarie 13.2.

Możesz go replikować w znacznie prostszy sposób (ten kod jest dosłownie wszystkim, czego potrzebujesz).

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

Mam nadzieję, że Apple to rozwiąże, ponieważ z pewnością zniszczy mnóstwo aplikacji SwiftUI (w tym moich).

James
źródło
Haha ... To całkiem niesamowite. Nawigowałeś do widoku tekstowego, który w SwiftUI jest widokiem! Tak, to powinno wrócić do rodzica, prawda? Ale tak nie jest. Interesujące jest to, że zachowanie z twojego przykładu psuje interfejs użytkownika, ale tak naprawdę nie powoduje śmiertelnej awarii.
Justin Ngan
Tak, składalność SwiftUI (i React Native / Flutter itp.) Jest niesamowita. Daje ci tyle kontroli / elastyczności (gdy przynajmniej działa).
James
1
Potwierdź to zawieszenie na Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
Nie ulega już awarii w 13.3, jednak nawigacja wydaje się działać tylko przy pierwszym uruchomieniu trigger
James
6

Aby obejść ten problem, w oparciu o powyższą odpowiedź Chucka H, ​​enkapsulowałem NavigationLink jako ukryty element:

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

Następnie możesz użyć go w widoku nawigacji (co jest kluczowe) i uruchomić go z przycisku na pasku nawigacyjnym:

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

Zawiń to w komentarze „// HACK”, więc gdy Apple to naprawi, możesz go zastąpić.

P. Ent
źródło
Wydaje się, że działa to tylko przy pierwszym użyciu w iOS 13.3.
James
3

Na podstawie informacji, które przekazaliście, a zwłaszcza komentarza @Robert na temat miejsca, w którym znajduje się NavigationView, znalazłem sposób na obejście tego problemu przynajmniej w moim konkretnym scenariuszu.

W moim przypadku miałem TabView, który był zamknięty w NavigationView w następujący sposób:

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

Ten kod ulega awarii, ponieważ wszyscy zgłaszają się w systemie iOS 13.2 i działa w systemie iOS 13.1. Po kilku badaniach wymyśliłem obejście tej sytuacji.

Zasadniczo przenoszę NavigationView do każdego ekranu osobno na każdej karcie w następujący sposób:

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

Jakoś jest sprzeczne z założeniem SwiftUI dotyczącym prostoty, ale działa na iOS 13.2.

Julio Bailon
źródło
to działa, ale problemem jest usunięcie tabViews na NewView.
PIĄTEK,
1
@FRIDDAY ten przykład działa w 13.1, ale ulega awarii w 13.2. Jest to znany błąd, a moim zamiarem była próba pomocy komuś w tym samym scenariuszu z obejściem problemu
Julio Bailon
1

Xcode 11.2.1 Swift 5

ROZUMIEM! Rozpracowanie tego zajęło mi kilka dni ...

W moim przypadku podczas korzystania ze SwiftUI dochodzi do awarii tylko wtedy, gdy spód mojej listy wystaje poza ekran, a następnie próbuję „przenieść” dowolne elementy listy. Ostatecznie dowiedziałem się, że jeśli mam za dużo „rzeczy” pod List (), to ulega awarii podczas przenoszenia. Na przykład poniżej mojej listy () miałem przycisk Tekst (), Odstęp (), Przycisk (), Odstęp ()) (). Gdybym skomentował JEDEN z tych obiektów, nagle nie mogłem odtworzyć awarii. Nie jestem pewien, jakie są ograniczenia, ale jeśli masz awarię, spróbuj usunąć obiekty poniżej listy, aby zobaczyć, czy to pomoże.

Dave Levy
źródło
0

Chociaż nie widzę żadnych awarii, Twój kod ma pewne problemy:

ustawiając wiodący element, faktycznie zabijasz domyślne zachowanie przejść nawigacyjnych. (spróbuj przesunąć palcem od strony wiodącej, aby zobaczyć, czy to działa).

Więc nie trzeba mieć przycisku. Po prostu zostaw to tak, jak jest, a masz bezpłatny przycisk Wstecz.

I nie zapominaj, zgodnie z HIG , tytuł przycisku wstecz powinien pokazywać, dokąd idzie, a nie co to jest! Postaraj się więc ustawić tytuł dla pierwszej strony, aby wyświetlać ją każdemu przyciskowi wstecz, który się na nią pojawi.

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
źródło
1
Hej, dzięki za odpowiedź. Chociaż zgadzam się, że pozostawienie domyślnego zachowania przycisku Wstecz jest pożądane, nadal powoduje awarię.
Robert,
Jakiej wersji używasz? Przetestowałem to przed wysłaniem. Może masz inny problem. Czy możesz podać przykładowy projekt?
Mojtaba Hosseini,
1
Xcode 11.2 beta, jak mówi pytanie. Podany przeze mnie przykład to wszystko, czego potrzebujesz, aby odtworzyć awarię.
Robert,
Używam tej samej wersji i tego samego kodu, ale bez awarii 🤔
Mojtaba Hosseini
1
Potwierdź to zawieszenie na Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
0

FWIW - powyższe rozwiązania sugerujące ukryty hack NavigationLink to wciąż najlepsze obejście w iOS 13.3b3. Złożyłem również FB7386339 ze względu na potomstwo i został zamknięty podobnie jak inne wyżej wspomniane FB: „Zidentyfikowano potencjalną poprawkę - do przyszłej aktualizacji systemu operacyjnego”.

Skrzyżowane palce.

Mike W.
źródło
Unikaj dodawania komentarzy jako odpowiedzi.
Karthick Ramesh
0

Jest rozwiązany w iOS 13.3. Po prostu zaktualizuj system operacyjny i kod xCode.

PIĄTEK
źródło
1
Xcode 11.3 (11C29) w wersji 10.15.2 powoduje dla mnie inne zachowanie: Nawigacja wsteczna działa, ale później NavigationLink nie ma już żadnej funkcji. Kliknięcie tego nic nie robi.
malte
@malte Lepiej jest otworzyć na to nowe pytanie. Zanim sprawdzę kod, podaj .buttonStyle(PlainButtonStyle())modyfikator NavigationLink i spróbuj ponownie. daj mi znać, jeśli zadałeś pytanie.
PIĄTEK
1
Masz rację. Okazuje się, że jest już nowe pytanie: stackoverflow.com/questions/59279176/…
malte