Чому мій додаток 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.