¿Por qué mi aplicación SwiftUI se bloquea cuando navego hacia atrás después de colocar un `NavigationLink` dentro de un` navigationBarItems` en un `NavigationView`?

47

Ejemplo reproducible mínimo (Xcode 11.2 beta, esto funciona en 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()
    }
}

El problema parece radicar en colocar mi NavigationLinkinterior de un navigationBarItemsmodificador que está anidado dentro de una vista SwiftUI cuya vista raíz es a NavigationView. El informe de bloqueo indica que estoy intentando abrir un controlador de vista que no existe cuando navego hacia adelante Childy luego hacia atrás Parent.

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

Si tuviera que colocar eso NavigationLinken el cuerpo de la vista como el siguiente, funciona bien.

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

¿Es esto un error de SwiftUI o un comportamiento esperado?

EDITAR: he abierto un problema con Apple en su asistente de comentarios con la ID FB7423964en caso de que a alguien de Apple le importe pesar :).

EDITAR: Mi boleto abierto en el asistente de comentarios indica que hay más de 10 problemas similares reportados. Han actualizado la resolución con Resolution: Potential fix identified - For a future OS update. Los dedos cruzaron que la solución aterriza pronto.

EDITAR: ¡Esto se ha solucionado en iOS 13.3!

Robert
fuente
El ejemplo que proporcionó anteriormente funciona bien con Xcode 11.2 beta. ¿Nos estamos perdiendo algo aquí?
Subramanian Mariappan
@SubramanianMariappan También funciona bien para mí en 11.2 beta.
Farhan Amjad
1
Interesante, se bloquea para mí cada vez. Incluso intenté crear un proyecto nuevo y copiar ese código exacto en lugar de ContentView.swift. Haré una edición en la publicación, pero el bloqueo solo ocurre cuando navegas hacia adelante y luego hacia atrás.
Robert
Gran pregunta! Tu ejemplo aquí se bloquea para mí también cada vez. Acabo de publicar una nueva respuesta que me funciona muy bien. Avísame si también te funciona. Gracias.
Chuck H
1
¡Gracias por las actualizaciones con respecto a las entradas de Apple!
Malta

Respuestas:

20

¡Este fue un punto de dolor para mí! Lo dejé hasta que se completó la mayor parte de mi aplicación y tuve el espacio mental para lidiar con el bloqueo.

Creo que todos podemos estar de acuerdo en que hay algunas cosas bastante impresionantes con SwifUI, pero que la depuración puede ser difícil.

En mi opinión, diría que esto es un ERROR. Aquí está mi justificación:

  • Si ajusta la llamada de rechazo de PresentationMode en un retraso asíncrono de aproximadamente medio segundo, debería encontrar que el programa ya no se bloqueará.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
  • Esto me sugiere que el error es un comportamiento inesperado en el fondo de cómo SwiftUI interactúa con todos los demás códigos UIKit para administrar las diversas vistas. Dependiendo de su código real, puede encontrar que si hay alguna complejidad menor en la vista, el bloqueo en realidad no sucederá. Por ejemplo, si está descartando de una vista a una que tiene una lista, y esa lista está vacía, obtendrá un bloqueo sin el retraso asincrónico. Por otro lado, si tiene solo una entrada en esa vista de lista, forzando una iteración de bucle para generar la vista principal, verá que no se producirá el bloqueo.

No estoy tan seguro de cuán robusta es mi solución de terminar la llamada de rechazo en un retraso. Tengo que probarlo mucho más. Si tiene ideas sobre esto, ¡hágamelo saber! ¡Estaré muy feliz de aprender de ti!

Justin Ngan
fuente
1
¡Muy inteligente! No había pensado en eso. ¡Esperando que se arregle pronto!
Robert
1
@Robert ¿Solucionó tu problema? Esta es una cuestión difícil ya que un problema no relacionado que he encontrado es usar un selector dentro de las vistas de navegación secundarias. Si bien funciona un estilo de selector segmentado, el valor predeterminado parece causar un bloqueo en el mismo punto, al hacer clic en el botón Atrás. Podemos discutir más a fondo si todavía te da pena. PD. Odio mi solución Es un truco, pero no debería requerir la actualización del código si Apple soluciona el problema de sincronización.
Justin Ngan
2
Estoy de acuerdo en que el aspecto del tiempo, junto con el hecho de que funcionó bien en 11.1 y funciona fuera de los .navigationBarItems()puntos para que esto sea un error.
John M.
3
Sí, creo que es un error y este es mi candidato principal actual para el premio de recompensa. Dado que me quedan 4 días de recompensa al momento de escribir esto, solo estoy esperando en caso de que alguien venga con nueva información :).
Robert
1
Este fue un consejo muy interesante, ¡gracias por eso! Desafortunadamente, sigo bloqueando la aplicación de manera confiable en el simulador el 100% del tiempo: / Funciona mejor en el dispositivo, pero no sin fallar en absoluto. Pero ese fue también el caso sin demora.
Kilian
15

Esto también me ha frustrado durante bastante tiempo. En los últimos meses, dependiendo de la versión de Xcode, la versión del simulador y el tipo y / o versión del dispositivo real, ha pasado de funcionar a fallar a funcionar nuevamente, aparentemente al azar. Sin embargo, recientemente ha estado fallando constantemente para mí, así que ayer me sumergí profundamente en él. Actualmente estoy usando Xcode versión 11.2.1 (11B500).

Parece que el problema gira en torno a la barra de navegación y la forma en que se le agregaron los botones. Entonces, en lugar de usar un NavigationLink () para el botón en sí, intenté usar un botón estándar () con una acción que establece un @State var que activa un NavigationLink oculto. Aquí hay un reemplazo para Robert's Parent 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 mí, esto funciona de manera muy consistente en todos los simuladores y todos los dispositivos reales.

Aquí están mis puntos de vista de ayuda:

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

Aquí hay un ejemplo del 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
fuente
¡Puedo confirmar que esto funciona (realmente bien para un hack ;-))! Sin embargo, Apple necesita arreglar esto lo antes posible. Xcode 11.2.1, Catalina 10.15.2 (beta), iOS 13.2.2
P. Ent
1
Estoy de acuerdo al 100% En general, con respecto a la navegación en SwiftUI, hay muchas cosas que están rotas o que simplemente faltan. Lo que, por supuesto, nos lleva al verdadero problema. No hay una "fuente de verdad" (es decir, documentación y ejemplos) de Apple, solo piratas informáticos como nosotros. Por cierto, uso mucho la técnica anterior, he creado dos vistas de utilidad que ayudan mucho con la legibilidad. Los agregaré a mi respuesta en caso de que alguien esté interesado.
Chuck H
Gracias por la solución, ¡simplemente funciona!
Stanislav Poslavsky
1
Esto no funciona para mí para más de una navegación. Una vez que ha vuelto a la pantalla anterior, el enlace invisible ya no funciona.
Jon Shier
1
Tengo varios dispositivos reales en 13.3 (compilación 17C54) y todos funcionan según lo deseado. Como hago casi todas mis pruebas en dispositivos reales, no uso el simulador con mucha frecuencia. Pero acabo de probar mi caso de prueba en un simulador 13.3 y la prueba falla allí. Noté que iOS 13.3 en el simulador Xcode es una compilación anterior (17C45) que la actualización pública. Me interesaría saber si alguien observa el comportamiento defectuoso en un dispositivo real.
Chuck H
12

Este es un error importante y no puedo ver una forma adecuada de solucionarlo. Funcionó bien en iOS 13 / 13.1 pero se bloquea 13.2.

En realidad, puede replicarlo de una manera mucho más simple (este código es, literalmente, todo lo que necesita).

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 Apple lo resuelva, ya que seguramente romperá un montón de aplicaciones SwiftUI (incluida la mía).

James
fuente
Jaja ... Eso es bastante asombroso. ¡Ha navegado a una vista de texto que en SwiftUI es una vista! Sí, eso debería volver a su padre, ¿no? Sin embargo, no lo hace. Es interesante que el comportamiento de su ejemplo rompa la interfaz de usuario, pero en realidad no causa un bloqueo fatal.
Justin Ngan el
Sí, la componibilidad de SwiftUI (y React Native / Flutter, etc.) es increíble. Te da mucho control / flexibilidad (cuando funciona al menos).
James
1
Confirme que esto falla en Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
Ya no se bloquea en 13.3, sin embargo, la navegación solo parece funcionar la primera vez que lo activa 🤦‍♂️
James
6

Como solución alternativa, según la respuesta de Chuck H anterior, he encapsulado el NavigationLink como un 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()
}
}

Luego puede usarlo dentro de un NavigationView (que es crucial) y activarlo desde un Botón en una barra de navegación:

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

Envuelva esto en comentarios "// HACK" para que cuando Apple corrija esto pueda reemplazarlo.

P. Ent
fuente
Esto solo parece funcionar en el primer uso en iOS 13.3.
James
3

Basado en la información que ustedes proporcionaron y especialmente en un comentario que hizo @Robert sobre dónde se ubica el NavigationView, he encontrado una manera de solucionar el problema, al menos en mi escenario específico.

En mi caso, tenía un TabView que estaba encerrado en un 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)
        }
    }
  }
}

Este código falla cuando todos informan en iOS 13.2 y funciona en iOS 13.1. Después de algunas investigaciones, descubrí una solución a esta situación.

Básicamente, estoy moviendo el NavigationView a cada pantalla por separado en cada pestaña de esta manera:

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 alguna manera va en contra de la premisa de simplicidad de SwiftUI, pero funciona en iOS 13.2.

Julio Bailon
fuente
esto funciona pero, el problema es eliminar tabViews en NewView.
VIERNES
1
@FRIDDAY este ejemplo funciona en 13.1 pero se bloquea en 13.2. Es un error conocido y mi intención era tratar de ayudar a alguien en el mismo escenario con una solución alternativa
Julio Bailon
1

Xcode 11.2.1 Swift 5

¡ENTENDIDO! Me tomó un par de días resolver esto ...

En mi caso, cuando uso SwiftUI, tengo un bloqueo solo si la parte inferior de mi lista se extiende más allá de la pantalla y luego trato de "mover" cualquier elemento de la lista. Lo que terminé descubriendo es que si tengo demasiadas "cosas" debajo de la Lista (), entonces se bloquea en el movimiento. Por ejemplo, debajo de mi Lista () tenía un Texto (), Espaciador (), Botón (), Botón Espaciador () (). Si comenté UNO de esos objetos, de repente no pude recrear el bloqueo. No estoy seguro de cuáles son las limitaciones, pero si tiene este bloqueo, intente eliminar los objetos debajo de su lista para ver si ayuda.

Dave Levy
fuente
0

Aunque no puedo ver ningún bloqueo, su código tiene algunos problemas:

al configurar el elemento principal, en realidad elimina el comportamiento predeterminado de las transiciones de navegación. (intente deslizar desde el lado delantero para ver si funciona).

Así que no es necesario tener un botón allí. Simplemente déjelo como está y tendrá un botón de retroceso gratuito.

Y no se olvide de acuerdo con HIG , el título del botón de retroceso debe mostrar a dónde va, ¡ no lo que es! Por lo tanto, intente configurar un título para la primera página para mostrarle el botón de retroceso 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
fuente
1
Hola, gracias por la respuesta. Si bien estoy de acuerdo en que es conveniente dejar el comportamiento predeterminado del botón de retroceso, todavía produce un bloqueo.
Robert
Qué versión estás usando? Lo he probado antes de enviarlo. Quizás tengas otro problema. ¿Puede proporcionar un proyecto de muestra por favor?
Mojtaba Hosseini
1
Xcode 11.2 beta como dice la pregunta. El ejemplo que proporcioné en la pregunta es todo lo que necesita para reproducir el bloqueo.
Robert
Estoy usando la misma versión y el mismo código, pero no se bloquea 🤔
Mojtaba Hosseini
1
Confirme que esto falla en Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2)
P. Ent
0

FWIW: las soluciones anteriores que sugieren un NavigationLink Hack oculto sigue siendo la mejor solución en iOS 13.3b3. También he presentado un FB7386339 por el bien de la posteridad, y se cerró de manera similar a otros FB mencionados anteriormente: "Solución potencial identificada - Para una futura actualización del sistema operativo".

Dedos cruzados.

Mike W.
fuente
Evite agregar comentarios como respuestas.
Karthick Ramesh
0

Está resuelto en iOS 13.3. Simplemente actualice su sistema operativo y xCode.

VIERNES
fuente
1
Xcode 11.3 (11C29) en 10.15.2 resulta en un comportamiento diferente para mí: la navegación hacia atrás está funcionando, pero luego el NavigationLink ya no tiene función. Al hacer clic no hace nada.
malte
@malte Es mejor abrir una nueva pregunta para eso. Antes de verificar su código, proporcione su .buttonStyle(PlainButtonStyle())modificador NavigationLink y vuelva a intentarlo. Avísame si hiciste una pregunta.
VIERNES
1
Tienes razón. Resulta que ya hay una nueva pregunta: stackoverflow.com/questions/59279176/…
malte