Почему происходит сбой моего приложения SwiftUI при переходе назад после размещения `NavigationLink` внутри` navigationBarItems` в `NavigationView`?

47

Минимальный воспроизводимый пример (бета-версия Xcode 11.2, работает в 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. Скрестим пальцы, что починка скоро приземлится.

РЕДАКТИРОВАТЬ: Это было исправлено в iOS 13.3!

Роберт
источник
Приведенный выше пример прекрасно работает с бета-версией Xcode 11.2. Мы что-то здесь упускаем?
Subramanian
@SubramanianMariappan На 11.2 бета у меня тоже работает нормально.
Фархан Амджад
1
Интересно, это вылетает для меня каждый раз. Я даже попытался создать новый проект и скопировать этот точный код вместо ContentView.swift. Я внесу изменения в сообщение, но сбой происходит только при переходе вперед, а затем назад.
Роберт
Отличный вопрос! Ваш пример здесь вылетает для меня каждый раз тоже. Я только что опубликовал новый ответ, который очень хорошо работает для меня. Дайте мне знать, если это работает и для вас. Спасибо.
Чак Х
1
Спасибо за обновления, касающиеся яблочных билетов!
солод

Ответы:

20

Это было довольно больно для меня! Я оставил его до тех пор, пока большая часть моего приложения не была завершена, и у меня не было места для разборок.

Я думаю, что мы все можем согласиться с тем, что в SwifUI есть кое-что удивительное, но отладка может быть сложной.

На мой взгляд, я бы сказал, что это ошибка. Вот мое обоснование:

  • Если вы оберните вызов прерывания presentationMode с асинхронной задержкой около полсекунды, вы обнаружите, что программа больше не будет аварийно завершать работу.

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

Я не совсем уверен, насколько надежным является мое решение об отложении вызова отклонения. Я должен проверить это намного больше. Если у вас есть идеи по этому поводу, пожалуйста, дайте мне знать! Я был бы очень рад учиться у вас!

Джастин нган
источник
1
Очень умный! Я не думал об этом. Надеюсь, это будет исправлено в ближайшее время!
Роберт
1
@ Роберт Это решило твою проблему? Это сложный вопрос, поскольку я обнаружил несвязанную проблему с использованием средства выбора в дочерних навигационных представлениях. В то время как стиль сегментированного выбора работает, по умолчанию появляется сбой в той же точке при нажатии кнопки назад. Мы можем обсудить дальше, если это все еще приносит вам горе. PS. Я ненавижу свое решение. Это взлом, но он не требует обновления кода, если Apple исправит проблему с синхронизацией.
Джастин Нган
2
Я согласен с тем, что аспект синхронизации, а также тот факт, что он работал нормально в 11.1 и работает вне пределов, .navigationBarItems()указывает на то, что это ошибка.
Джон М.
3
Да, я считаю, что это ошибка, и это мой текущий кандидат на получение награды. Так как на момент написания этой статьи у меня осталось 4 дня, я просто продержусь, если кто-нибудь придет с новой информацией :).
Роберт,
1
Это был очень интересный совет, спасибо за это! К сожалению, я до сих пор надежно рушаю приложение в симуляторе 100% времени: / Это работает лучше на устройстве, но не без сбоев вообще. Но это также имело место без задержки.
Килиан
15

Это также разочаровало меня в течение довольно долгого времени. За последние несколько месяцев, в зависимости от версии 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()
            } )
        }
    }
}
Чак Н
источник
Я могу подтвердить, что это работает (действительно хорошо для взлома ;-))! Apple должна исправить это как можно скорее. Xcode 11.2.1, Catalina 10.15.2 (бета), iOS 13.2.2
P. Ent
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 (включая мое).

Джеймс
источник
Хаха ... Это очень круто. Вы перешли к текстовому представлению, которое в SwiftUI является представлением! Да, это должно вернуться к его родителю, не так ли? Тем не менее, это не так. Интересно, что поведение из вашего примера нарушает интерфейс, но на самом деле не приводит к фатальному сбою.
Джастин Нган
Да, сочетаемость SwiftUI (и React Native / Flutter и т. Д.) Невероятна. Дает вам так много контроля / гибкости (когда это работает по крайней мере).
Джеймс
1
Подтвердите это сбой на Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
В 13.3 он больше не падает, однако навигация, кажется, работает только при первом запуске 🤦‍♂️
Джеймс
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 я получаю сбой, только если нижняя часть моего списка выходит за пределы экрана, а затем я пытаюсь «переместить» любые элементы списка. В итоге я обнаружил, что если у меня слишком много «материала» под List (), то он падает на ходу. Например, ниже моего List () у меня были Text (), Spacer (), Button (), Spacer () Button (). Если я закомментировал ОДИН из этих объектов, то внезапно не смог воссоздать аварию. Я не уверен, каковы ограничения, но если вы получаете этот сбой, то попробуйте удалить объекты из списка ниже, чтобы посмотреть, поможет ли это.

Дейв Леви
источник
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 Hosseini
1
Подтвердите это сбой на Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
0

FWIW - вышеупомянутые решения, предполагающие скрытый взлом NavigationLink, по-прежнему остаются лучшим обходным путем в iOS 13.3b3. Я также подал FB7386339 для потомков и был закрыт, как и другие вышеупомянутые FB: «Обнаружено потенциальное исправление - для будущего обновления ОС».

Скрещенные пальцы.

Майк У.
источник
Пожалуйста, не добавляйте комментарии в качестве ответов.
Картик Рамеш
0

Это решено в iOS 13.3. Просто обновите свою ОС и xCode.

FRIDDAY
источник
1
Xcode 11.3 (11C29) на 10.15.2 приводит меня к другому поведению: обратная навигация работает, но после этого NavigationLink больше не работает. Нажатие это ничего не делает.
мальте
@malte Лучше открыть новый вопрос для этого. Прежде чем я проверю ваш код, дайте ваш .buttonStyle(PlainButtonStyle())модификатор NavigationLink и попробуйте снова. дайте мне знать, если вы задали вопрос.
пятница,
1
Вы правы. Оказывается, уже есть новый вопрос: stackoverflow.com/questions/59279176/…
malte