Почему в моем приложении SwiftUI происходит сбой при перемещении назад после помещения `NavigationLink` внутри` navigationBarItems` в `NavigationView`?
РЕДАКТИРОВАТЬ: это было исправлено в iOS 13.3!
Минимальный воспроизводимый пример (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 или ожидаемое поведение?
РЕДАКТИРОВАТЬ: Я открыл проблему с Apple в их помощнике по обратной связи с идентификатором FB7423964
на тот случай, если кто-то из Apple захочет взвесить:).
РЕДАКТИРОВАТЬ: мой открытый тикет в помощнике обратной связи указывает на наличие более 10 похожих проблем. Они обновили разрешение наResolution: Potential fix identified - For a future OS update
. Скрестив пальцы, исправление скоро появится.
9 ответов
Для меня это было довольно болезненной точкой! Я оставил его до тех пор, пока большая часть моего приложения не будет завершена, и у меня не появится разум, чтобы справиться со сбоями.
Я думаю, мы все можем согласиться с тем, что в SwifUI есть несколько замечательных вещей, но отладка может быть сложной.
На мой взгляд, я бы сказал, что это ОШИБКА. Вот мое обоснование:
Если вы завершите вызов завершения презентации в режиме презентации с асинхронной задержкой примерно на полсекунды, вы обнаружите, что программа больше не будет давать сбой.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.presentationMode.wrappedValue.dismiss() }
Это наводит на мысль, что ошибка является неожиданным поведением на глубине в том, как SwiftUI взаимодействует со всем другим кодом UIKit для управления различными представлениями. В зависимости от вашего фактического кода вы можете обнаружить, что если в представлении есть небольшая сложность, сбой на самом деле не произойдет. Например, если вы отклоняетесь от представления к тому, у которого есть список, и этот список пуст, вы получите сбой без асинхронной задержки. С другой стороны, если у вас есть хотя бы одна запись в этом представлении списка, заставляющая итерацию цикла генерировать родительское представление, вы увидите, что сбоя не произойдет.
Я не уверен, насколько надежным является мое решение обернуть вызов отклонения в задержку. Я должен испытать это гораздо больше. Если у вас есть идеи по этому поводу, дайте мне знать! Буду очень рада учиться у вас!
Это тоже довольно долго меня расстраивало. За последние несколько месяцев, в зависимости от версии Xcode, версии симулятора и реального типа и / или версии устройства, он перешел от работы к отказу и снова работал, по-видимому, случайным образом. Однако в последнее время он постоянно терпит неудачу, поэтому вчера я глубоко погрузился в это. В настоящее время я использую Xcode версии 11.2.1 (11B500).
Похоже, проблема связана с навигационной панелью и тем, как на нее были добавлены кнопки. Поэтому вместо использования NavigationLink() для самой кнопки я попытался использовать стандартную Button() с действием, которое устанавливает переменную @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()
} )
}
}
}
Это серьезная ошибка, и я не вижу правильного способа ее обойти. Работает нормально в 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 (включая мое).
В качестве обходного пути, основанного на ответе Чака Х, приведенном выше, я инкапсулировал 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 исправит это, вы могли заменить его.
Основываясь на информации, которую вы предоставили, и особенно на комментарии @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.
Xcode 11.2.1 Swift 5
ПОНЯЛ! Мне потребовалось несколько дней, чтобы понять это...
В моем случае при использовании SwiftUI я получаю сбой только в том случае, если нижняя часть моего списка выходит за пределы экрана, а затем я пытаюсь "переместить" любые элементы списка. В конечном итоге я обнаружил, что если у меня слишком много "вещей" под List(), он вылетает на ходу. Например, под моим списком () у меня были кнопки Text(), Spacer(), Button(), Spacer() (). Если бы я закомментировал какой-либо ОДИН из этих объектов, то внезапно я не смог бы воссоздать сбой. Я не уверен, каковы ограничения, но если вы получаете этот сбой, попробуйте удалить объекты под вашим списком, чтобы узнать, поможет ли это.
Хотя я не вижу сбоев, в вашем коде есть некоторые проблемы:
устанавливая ведущий элемент, вы фактически убиваете поведение переходов навигации по умолчанию. (попробуйте провести пальцем от ведущей стороны, чтобы проверить, работает ли он).
Так что кнопки там не нужно. Просто оставьте все как есть, и у вас будет бесплатная кнопка возврата.
И не забывайте, согласно 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()
}
}
FWIW - Приведенные выше решения, предлагающие скрытый взлом NavigationLink, по-прежнему являются лучшим решением в iOS 13.3b3. Я также зарегистрировал FB7386339 для потомков и был закрыт аналогично другим вышеупомянутым FB: "Выявлено возможное исправление - для будущего обновления ОС".
Скрещенные пальцы.