Por que meu aplicativo SwiftUI falha ao navegar para trás depois de colocar um `NavigationLink` dentro de um` navigationBarItems` em um `NavigationView`?

47

Exemplo reprodutível mínimo (Xcode 11.2 beta, isso funciona no 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()
    }
}

O problema parece estar em colocar meu NavigationLinkinterior de um navigationBarItemsmodificador aninhado dentro de uma visualização SwiftUI cuja visualização raiz é a NavigationView. O relatório de falha indica que estou tentando acessar um controlador de exibição que não existe quando navego para frente Childe depois para Parent.

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

Se eu colocasse isso NavigationLinkno corpo da visualização como a abaixo, funciona muito bem.

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

Isso é um bug do SwiftUI ou comportamento esperado?

EDIT: Eu abri um problema com a Apple no assistente de feedback com o ID FB7423964, caso alguém da Apple se preocupe em pesar :).

EDIT: meu ticket aberto no assistente de feedback indica que há mais de 10 problemas relatados semelhantes. Eles atualizaram a resolução com Resolution: Potential fix identified - For a future OS update. Dedos cruzados que a correção chega logo.

EDIT: Isso foi corrigido no iOS 13.3!

Robert
fonte
O exemplo que você forneceu acima funciona bem com o Xcode 11.2 beta. Estamos perdendo alguma coisa aqui?
Subramanian Mariappan 16/10/19
@SubramanianMariappan Está funcionando bem para mim também na versão 11.2 beta.
Farhan Amjad
11
Interessante, ele trava para mim o tempo todo. Eu até tentei criar um projeto novo e copiar esse código exato no lugar de ContentView.swift. Farei uma edição na postagem, mas a falha só acontece quando você navega para frente e para trás.
Robert
Ótima pergunta! Seu exemplo aqui também falha para mim o tempo todo. Acabei de publicar uma nova resposta que funciona muito bem para mim. Deixe-me saber se funciona para você também. Obrigado.
mandril H
11
Obrigado pelas atualizações sobre os ingressos da apple!
malte

Respostas:

20

Este foi um ponto de dor para mim! Deixei-o até a maior parte do meu aplicativo ser concluída e eu tive o espaço da mente para lidar com as falhas.

Acho que todos podemos concordar que há algumas coisas incríveis no SwifUI, mas que a depuração pode ser difícil.

Na minha opinião, eu diria que este é um erro. Aqui está o meu raciocínio:

  • Se você quebrar a chamada de dispensa de PresentationMode em um atraso assíncrono de cerca de meio segundo, você deve descobrir que o programa não travará mais.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
  • Isso sugere para mim que o bug é um comportamento inesperado, profundo na maneira como o SwiftUI faz interface com todos os outros códigos do UIKit para gerenciar as várias visualizações. Dependendo do seu código real, você poderá descobrir que, se houver alguma complexidade menor na exibição, a falha não ocorrerá. Por exemplo, se você estiver descartando de uma exibição para uma que tenha uma lista e essa lista esteja vazia, ocorrerá uma falha sem o atraso assíncrono. Por outro lado, se você tiver apenas uma entrada nessa exibição de lista, forçando uma iteração de loop para gerar a exibição pai, verá que a falha não ocorrerá.

Não sei ao certo quão robusta é minha solução de encerrar a chamada de demissão em um atraso. Eu tenho que testar muito mais. Se você tiver idéias sobre isso, entre em contato! Eu ficaria muito feliz em aprender com você!

Justin Ngan
fonte
11
Muito esperto! Eu não tinha pensado nisso. Esperando que seja corrigido em breve!
Robert
11
@Robert Ele resolveu o seu problema? Essa é uma pergunta difícil, pois um problema não relacionado que eu descobri está usando um Seletor nas visualizações de navegação infantil. Enquanto um estilo de selecionador segmentado funciona, o padrão parece causar uma falha no mesmo ponto, ao clicar no botão Voltar. Podemos discutir mais se ainda estiver lhe dando dor. PS. Eu odeio minha solução. É um truque, mas não exige atualização de código se a Apple resolver o problema de tempo.
Justin Ngan
2
Eu concordo que o aspecto do tempo, juntamente com o fato de que funcionou bem no 11.1 e funciona fora dos .navigationBarItems()pontos, por ser um bug.
John M.
3
Sim, acredito que seja um bug e este é meu atual candidato principal ao prêmio de recompensa. Desde que eu tenho 4 dias restantes na recompensa até o momento da redação deste artigo, estou apenas aguardando caso alguém venha com novas informações :).
Robert
11
Esta foi uma dica muito interessante, obrigado por isso! Infelizmente, ainda estou travando o aplicativo no simulador 100% do tempo: / Funciona melhor no dispositivo, mas não deixa de funcionar. Mas esse também foi o caso sem demora.
Kilian
15

Isso também me frustrou por algum tempo. Nos últimos meses, dependendo da versão do Xcode, da versão do simulador e do tipo e / ou versão real do dispositivo, ele passou de um trabalho para outro, aparentemente aleatoriamente. No entanto, recentemente, ele falhou consistentemente para mim, então ontem eu mergulhei profundamente nele. Atualmente, estou usando o Xcode versão 11.2.1 (11B500).

Parece que o problema gira em torno da barra de navegação e da maneira como os botões foram adicionados a ele. Portanto, em vez de usar um NavigationLink () para o próprio botão, tentei usar um Button () padrão com uma ação que define uma var @State que ativa um NavigationLink oculto. Aqui está um substituto para o Parent's Robert View:

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

Para mim, isso funciona de maneira muito consistente em todos os simuladores e dispositivos reais.

Aqui estão as minhas visualizações auxiliares:

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

Aqui está um exemplo do uso:

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
fonte
Posso confirmar que isso funciona (muito bem para um hack ;-))! A Apple precisa corrigir isso o mais rápido possível. Xcode 11.2.1, Catalina 10.15.2 (beta), iOS 13.2.2
P. Ent
11
Eu concordo 100%. Em geral, no que diz respeito à navegação no SwiftUI, há muita coisa que está quebrada ou simplesmente ausente. O que, obviamente, nos leva ao verdadeiro problema. Não há "fonte de verdade" (ou seja, documentação e exemplos) da Apple, apenas hacks como nós. BTW, eu uso muito a técnica acima, criei duas visualizações de utilitários que ajudam muito na legibilidade. Vou adicioná-los à minha resposta caso alguém esteja interessado.
mandril H
Obrigado pela solução alternativa, ele simplesmente funciona!
Stanislav Poslavsky
11
Isso não funciona para mim em mais de uma navegação. Depois que você voltar para a tela anterior, o link invisível não funcionará mais.
Jon Shier 13/12/19
11
Eu tenho vários dispositivos reais em 13.3 (compilação 17C54) e todos eles funcionam como desejado. Como faço quase todos os meus testes em dispositivos reais, não uso o simulador com muita frequência. Mas eu apenas tentei meu caso de teste em um simulador 13.3 e o teste falha lá. Notei que o iOS 13.3 no simulador Xcode é uma compilação anterior (17C45) que a atualização pública. Eu estaria interessado em saber se alguém observa o comportamento falha em um dispositivo real.
mandril H
12

Este é um erro grave e não consigo ver uma maneira adequada de contornar isso. Funcionou bem no iOS 13 / 13.1, mas 13.2 falha.

Você pode replicá-lo de uma maneira muito mais simples (esse código é literalmente tudo o que você precisa).

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

Espero que a Apple resolva o problema, pois certamente quebrará muitos aplicativos SwiftUI (incluindo os meus).

James
fonte
Haha ... Isso é incrível. Você navegou para uma visualização de texto que, no SwiftUI, é uma visualização! Sim, isso deve voltar para o pai, não é? No entanto, isso não acontece. É interessante que o comportamento do seu exemplo interrompa a interface do usuário, mas na verdade não cause uma falha fatal.
Justin Ngan
Sim, a composibilidade do SwiftUI (e React Native / Flutter etc) são incríveis. Dá muito controle / flexibilidade (quando funciona pelo menos).
James James
11
Confirme esta falha na Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
Ele não trava mais no 13.3, no entanto, a navegação parece funcionar apenas na primeira vez em que você o aciona 🤦‍♂️
James
6

Como solução alternativa, com base na resposta de Chuck H acima, encapsulei o NavigationLink como um elemento oculto:

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

Em seguida, você pode usá-lo em um NavigationView (que é crucial) e acioná-lo a partir de um botão em uma barra de navegação:

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

Coloque isso nos comentários "// HACK"; assim, quando a Apple corrigir isso, você poderá substituí-lo.

P. Ent
fonte
Isso parece funcionar apenas no primeiro uso no iOS 13.3.
James
3

Com base nas informações que vocês forneceram e, especialmente, em um comentário que o @Robert fez sobre a localização do NavigationView, encontrei uma maneira de solucionar o problema, pelo menos no meu cenário específico.

No meu caso, eu tinha um TabView incluído em um NavigationView como este:

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

Esse código falha quando todos relatam no iOS 13.2 e funcionam no iOS 13.1. Após algumas pesquisas, descobri uma solução alternativa para essa situação.

Basicamente, estou movendo o NavigationView para cada tela separadamente em cada guia como esta:

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

De alguma forma, vai contra a premissa de simplicidade da SwiftUI, mas funciona no iOS 13.2.

Julio Bailon
fonte
isso funciona, mas o problema está removendo tabViews no NewView.
Sexta
11
@FRIDDAY este exemplo funciona na 13.1, mas falha na 13.2. É um bug conhecido e minha intenção era tentar ajudar alguém no mesmo cenário com uma solução alternativa
Julio Bailon
1

Xcode 11.2.1 Swift 5

ENTENDI! Levei alguns dias para descobrir isso ...

No meu caso, ao usar o SwiftUI, estou recebendo uma falha apenas se a parte inferior da minha lista se estender além da tela e, em seguida, tentar "mover" qualquer item da lista. O que acabei descobrindo é que, se houver muitas "coisas" embaixo do List (), ele trava em movimento. Por exemplo, abaixo da minha Lista () eu tinha um botão Texto (), Espaçador (), Botão (), Espaçador () (). Se eu comentei algum desses objetos, de repente não consegui recriar a falha. Não sei ao certo quais são as limitações, mas se você estiver recebendo esse erro, tente remover objetos abaixo da sua lista para ver se isso ajuda.

Dave Levy
fonte
0

Embora eu não consiga ver nenhuma falha, seu código tem alguns problemas:

Ao definir o item principal, você realmente elimina o comportamento padrão das transições de navegação. (tente deslizar do lado principal para ver se funciona).

Portanto, não há necessidade de um botão lá. Apenas deixe como está e você terá um botão Voltar gratuito.

E não se esqueça, de acordo com a HIG , o título do botão Voltar deve mostrar para onde vai, não o que é! Portanto, tente definir um título para a primeira página para mostrá-lo no botão Voltar que aparece.

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
fonte
11
Ei, obrigado pela resposta. Embora eu concorde que é desejável deixar o comportamento padrão do botão voltar, ele ainda produz uma falha.
Robert
Qual versão você está usando? Eu testei antes de enviar. Talvez você tenha outro problema. Você pode fornecer um projeto de amostra, por favor?
Mojtaba Hosseini
11
Xcode 11.2 beta como a pergunta diz. O exemplo que forneci na pergunta é tudo o que você precisa para reproduzir a falha.
Robert
Estou usando mesma versão e mesmo código, mas sem falhas 🤔
Mojtaba Hosseini
11
Confirme esta falha na Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
0

FWIW - As soluções acima sugerindo um Hack NavigationLink oculto ainda são a melhor solução alternativa no iOS 13.3b3. Também arquivei um FB7386339 pelo bem da posteridade e fui fechado da mesma forma que outros FBs acima mencionados: "Correção potencial identificada - para uma futura atualização do sistema operacional".

Dedos cruzados.

Mike W.
fonte
Evite adicionar comentários como respostas.
Karthick Ramesh
0

Foi resolvido no iOS 13.3. Basta atualizar seu sistema operacional e o xCode.

Sexta-feira
fonte
11
O Xcode 11.3 (11C29) em 10.15.2 resulta em um comportamento diferente para mim: a navegação para trás está funcionando, mas depois o NavigationLink não tem mais nenhuma função. Clicar nele não faz nada.
malte
@malte É melhor abrir uma nova pergunta para isso. Antes de verificar o seu código, forneça o .buttonStyle(PlainButtonStyle())modificador NavigationLink e tente novamente. deixe-me saber se você fez uma pergunta.
SEXTA
11
Você está certo. Acontece que já existe uma nova pergunta: stackoverflow.com/questions/59279176/…
malte 28/12/19