Чому мій додаток SwiftUI виходить з ладу під час переміщення назад, після розміщення "NavigationLink" всередині "NaviBarItems" у "NavigationView"?


47

Мінімальний приклад відтворення (Xcode 11.2 beta, це працює в 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()
    }
}

Здається, проблема полягає в тому, щоб помістити мою NavigationLinkвнутрішню частину navigationBarItemsмодифікатора, який вкладений всередині перегляду SwiftUI, корінь якого - це NavigationView. Звіт про збій вказує на те, що я намагаюся перейти на контролер перегляду, який не існує, коли я переходжу вперед, Childа потім назад до Parent.

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

Якби я замість цього розмістив NavigationLinkу тілі виду, як показано нижче, це працює чудово.

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

Це помилка SwiftUI чи очікувана поведінка?

EDIT: Я відкрив проблему з Apple, їх помічником із зворотним зв'язком з ідентифікатором, FB7423964на випадок, якщо хтось із Apple захоче зважити :).

EDIT: Мій відкритий квиток у помічнику зворотного зв’язку вказує на 10+ подібних повідомлених проблем. Вони оновили резолюцію на Resolution: Potential fix identified - For a future OS update. Пальці схрестили, що виправлення приземляється незабаром.

EDIT: Це було виправлено в iOS 13.3!


Наведений вище приклад добре працює з бета-версією Xcode 11.2. Ми чогось тут пропускаємо?
Subramanian

@SubramanianMariappan Це добре працює і для мене на 11.2 бета-версії.
Фархан Амджад

1
Цікаво, що він ламається для мене щоразу. Я навіть спробував створити новий проект і скопіювати саме той код замість ContentView.swift. Я буду редагувати публікацію, але аварія трапляється лише тоді, коли ви перейдете вперед, а потім назад.
Роберт

Чудове запитання! Ваш приклад тут трапляється і для мене щоразу. Я щойно опублікував нову відповідь, яка дуже добре працює для мене. Дайте мені знати, чи працює він і для вас. Дякую.
Чак Н

1
Дякуємо за оновлення щодо яблучних квитків!
мальте

Відповіді:


20

Це було досить больовим моментом для мене! Я залишив його до тих пір, поки більшість моєї програми не було завершено, і я мав простір для розуму, щоб боротися з збоями.

Я думаю, що ми всі можемо погодитися, що з SwifUI є деякі надзвичайно чудові речі, але налагодження може бути важким.

На мою думку, я б сказав, що це БУГ. Ось моє обгрунтування:

  • Якщо завершити виклик presenMode відхилити в асинхронній затримці приблизно на півсекунди, ви повинні виявити, що програма більше не завершиться збоєм.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
  • Це підказує мені, що помилка є несподіваною поведінкою в глибині того, як SwiftUI взаємодіє з усіма іншими кодами UIKit для управління різними поглядами. Залежно від вашого фактичного коду, ви можете виявити, що якщо є деяка незначна складність у представленні, збій насправді не відбудеться. Наприклад, якщо ви відхиляєтесь від перегляду до списку, який має список, і цей список порожній, ви отримаєте збій без асинхронної затримки. З іншого боку, якщо у вас є навіть лише один запис у цьому списку списку, змушуючи ітерацію циклу генерувати батьківський вигляд, ви побачите, що збій не відбудеться.

Я не настільки впевнений, наскільки надійним є рішення про припинення виклику про відхилення. Я мушу перевірити це набагато більше. Якщо у вас є ідеї щодо цього, будь ласка, повідомте мене! Я був би дуже радий дізнатися у вас!


1
Дуже розумний! Я не думав про це. Сподіваючись, що це швидко виправиться!
Роберт

1
@Robert Чи це вирішило вашу проблему? Це складна проблема, оскільки я виявив, що між моїми непов'язаними проблемами використовується функція Picker у внутрішніх поданнях навігації. Хоча сегментаційний стиль вибору працює, як видається, за замовчуванням викликає збій у тій же точці при натисканні на кнопку назад. Ми можемо обговорити далі, якщо це все-таки доставить вам горе. PS. Я ненавиджу своє рішення. Це злом, але він не повинен вимагати оновлення коду, якщо Apple виправить проблему з термінами.
Джастін Нган

2
Я погоджуюся, що аспект хронометражу, а також той факт, що він працював нормально в 11.1 та працює поза межами .navigationBarItems()пункту, і це помилка.
Джон М.

3
Так, я вважаю, що це помилка, і це мій теперішній провідний кандидат на нагороду баунті. Оскільки у мене на рахунку ще залишилось 4 дні, я просто тримаю на випадок, якщо хтось прийде разом з новою інформацією :).
Роберт

1
Це була дуже цікава порада, дякую за це! На жаль, я все ще надійно вибиваю додаток у симуляторі 100% часу: / Це працює краще на пристрої, але не без збоїв. Але це теж було без зволікань.
Кіліан

15

Це також засмучує мене досить довгий час. За останні кілька місяців, залежно від версії Xcode, версії тренажера та реального типу пристрою та / або версії, він перейшов від роботи до відмови знову працювати, здавалося б, навмання. Однак останнім часом він постійно провалюється для мене, тому вчора я глибоко занурився в нього. Зараз я використовую Xcode версії 11.2.1 (11B500).

Схоже, проблема обертається навколо Nav Bar і способу додавання до нього кнопок. Тому замість використання NavigationLink () для самої кнопки я спробував використовувати стандартну кнопку () з дією, яка встановлює var @State, який активує прихований NavigationLink. Ось заміна для батьківського виду Роберта:

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

Для мене це працює дуже послідовно на всіх тренажерах та всіх реальних пристроях.

Ось мої помічники:

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

Ось приклад використання:

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

Я можу підтвердити, що це працює (дуже добре для злому ;-))! Яблуку потрібно виправити це якнайшвидше Xcode 11.2.1, Каталіна 10.15.2 (бета), iOS 13.2.2
P.

1
Я згоден на 100%. Загалом, що стосується навігації в SwiftUI, то багато що або зламано, або просто не вистачає. Що, звичайно, призводить нас до реальної проблеми. Немає жодного "джерела істини" (тобто документації та прикладів) від Apple, лише хакі, як у нас. До речі, я так багато використовую вищевказану техніку, я створив два представлення утиліти, які багато допомагають у читанні. Я додам їх до своєї відповіді на випадок, коли хтось зацікавиться.
Чак Н

Дякую за вирішення, воно просто працює!
Станіслав Пославський

1
Це не працює для мене більше ніж однією навігацією. Після повернення на попередній екран невидиме посилання більше не працює.
Джон Шиер

1
У мене є кілька реальних пристроїв на рівні 13.3 (збірка 17C54), і всі вони працюють за бажанням. Оскільки я майже все тестую на реальних пристроях, симулятор я не дуже часто використовую. Але я просто спробував свій тестовий випадок на симуляторі 13.3, і тест там не вийшов. Я помітив, що iOS 13.3 на симуляторі Xcode - це більш рання збірка (17C45), ніж публічне оновлення. Мені було б цікаво дізнатися, чи хтось спостерігає за невдалою поведінкою на реальному пристрої.
Чак Н

12

Це основна помилка, і я не можу побачити належного способу обійти її. Працював чудово в iOS 13 / 13.1, але збій 13,2.

Насправді ви можете його повторити набагато простішим способом (цей код буквально все, що вам потрібно).

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

Сподіваємось, Apple розібрається з цим, оскільки це, безумовно, зламає навантаження програм SwiftUI (включаючи мою).


Ха-ха ... Це досить приголомшливо. Ви перейшли до тексту Text, який у SwiftUI - це перегляд! Так, це повинно повернутися до батьків, чи не так? Однак це не так. Цікаво, що поведінка з вашого прикладу порушує інтерфейс користувача, але насправді не призводить до фатальної аварії.
Джастін Нган

Так, композиційні можливості SwiftUI (і React Native / Flutter тощо) є неймовірними. Дає вам стільки контролю / гнучкості (коли це працює принаймні).
Джеймс

1
Підтвердьте цю аварію на Каталіні (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P.

Він більше не виходить з ладу в 13.3, проте навігація, здається, спрацьовує лише при першому запуску it
Джеймс

6

В якості вирішення рішення, грунтуючись на відповіді Чак Н вище, я інкапсулював NavigationLink як прихований елемент:

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

Потім ви можете використовувати його в NavigationView (що є вирішальним) і запустити його з кнопки на навігаційній панелі:

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

Заверніть це до коментарів "// HACK", тому коли Apple це виправить, ви можете замінити його.


Це, здається, працює при першому використанні в iOS 13.3.
Джеймс

3

Виходячи з інформації, яку ви надали, і спеціально зауваження, яке @Robert зробив про те, де розміщено NavigationView, я знайшов спосіб вирішити цю проблему принаймні в моєму конкретному сценарії.

У моєму випадку у мене був TabView, який був укладений у NavigationView так:

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

Цей код виходить з ладу, оскільки всі повідомляють у iOS 13.2 та працюють у iOS 13.1. Після деяких досліджень я з'ясував вирішення цієї ситуації.

В основному я переміщу NavigationView на кожен екран окремо на кожній вкладці, як це:

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

Так чи інакше суперечить приміщенню SwiftUI простоти, але він працює на iOS 13.2.


це працює, але проблема полягає в тому, щоб видалити tabViews в NewView.
П'ятниця

1
@FRIDDAY цей приклад працює в 13.1, але виходить з ладу в 13.2. Це відома помилка, і мій намір полягав у тому, щоб спробувати допомогти комусь за тим самим сценарієм із вирішенням проблеми
Хуліо Байлон

1

Xcode 11.2.1 Swift 5

ЗРОЗУМІВ! Знадобилося мені пару днів, щоб розібратися в цьому ...

У моєму випадку, коли я користуюся SwiftUI, я отримую збої, лише якщо нижня частина мого списку поширюється за межі екрана, і тоді я намагаюся "перемістити" будь-які елементи списку. Я дізнався, що якщо я маю занадто багато "речей" під списком (), то він виходить з ладу. Наприклад, під моїм списком () у мене була кнопка Text (), Spacer (), Button (), Spacer () (). Якби я прокоментував якийсь ОДИН із цих об'єктів, то раптом я не зміг би відтворити аварію. Я не впевнений, що таке обмеження, але якщо ви отримуєте цю аварію, то спробуйте видалити об'єкти під списком, щоб побачити, чи допомагає вона.


0

Хоча я не бачу жодних збоїв, у вашому коді є деякі проблеми:

встановивши провідний елемент, ви фактично вбиваєте поведінку за замовчуванням навігаційних переходів. (спробуйте пальцем з ведучої сторони, щоб побачити, чи працює).

Тому немає необхідності мати там кнопку. Просто залиште його таким, яким він є, і у вас є вільна кнопка повернення.

І не забувайте, згідно з HIG , заголовок кнопки повернення повинен показувати, куди йде, а не те, що це! Тому спробуйте встановити заголовок для першої сторінки, щоб показати її однією будь-якою кнопкою "назад", яка вискакує на неї.

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

1
Гей, дякую за відповідь. Хоча я згоден з тим, що залишити кнопку задуму повернення за замовчуванням бажано, вона все одно призводить до збоїв.
Роберт

Яку версію ви використовуєте? Я перевірив його перед відправкою. Можливо, у вас є інше питання. Чи можете ви надати зразок проекту, будь ласка?
Мойтаба Хоссейні

1
Бета-версія Xcode 11.2, як сказано в запитанні. Приклад, який я наводив у запитанні, - це все, що вам потрібно для відтворення аварії.
Роберт

Я використовую ту саму версію та той самий код, але без збоїв. 🤔
Mojtaba

1
Підтвердьте цю аварію на Каталіні (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P.

0

FWIW - Рішення вище, що пропонують прихований NavickLink Hack, як і раніше є найкращим вирішенням в iOS 13.3b3. Я також подав FB7386339 заради нащадків, і був закритий аналогічно іншим вищезгаданим ФБ: "Визначений потенціал - для майбутнього оновлення ОС".

Схрещені пальці.


Будь ласка, уникайте коментарів як відповідей.
Картік Рамеш

0

Це вирішено в iOS 13.3. Просто оновіть вашу ОС та xCode.


1
Xcode 11.3 (11C29) 10.15.2 призводить до різної поведінки для мене: навігаційна навігація працює, але після цього NavigationLink більше не функціонує. Клацання не робить нічого.
мальте

@malte Краще відкрити для цього нове питання. Перш ніж перевірити ваш код, дайте .buttonStyle(PlainButtonStyle())модифікатор NavigationLink та спробуйте його ще раз. дайте мені знати, якщо ви задали питання.
П'ятниця

1
Ти правий. Виявляється, є вже новий питання: stackoverflow.com/questions/59279176 / ...
Malte
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.