Adaptando su solución comercial existente para SwiftUI. Parte 4. Navegación y configuración

¡Buen día a todos! Contigo yo, Anna Zharkova, un desarrollador líder de dispositivos móviles de Usetech.
Ahora hablemos de otro punto interesante en SwiftUI, la navegación.

Si se perdió los artículos anteriores de la serie, puede leerlos aquí:

parte 1
parte 2
parte 3

Con el cambio en la descripción de la parte visual y la transición a la sintaxis declarativa, el control de navegación en la aplicación SwiftUI también ha cambiado. Se niega directamente el uso de UIViewContoller; el UINavigationController no se usa directamente. Se reemplaza por NavigationView.

@available(iOS 13.0, OSX 10.15, tvOS 13.0, *)
@available(watchOS, unavailable)
public struct NavigationView<Content> : View where Content : View {

    public init(@ViewBuilder content: () -> Content)

    //....
}

Esencialmente un contenedor sobre UINavigationController y su funcionalidad.


El mecanismo de transición principal es NavigationLink (un análogo de segue), que se establece inmediatamente en el código de visualización del cuerpo.

public struct NavigationLink<Label, Destination> : View where Label : View, 
                                                                                           Destination : View {
.
    public init(destination: Destination, @ViewBuilder label: () -> Label)

    public init(destination: Destination, isActive: Binding<Bool>, 
                   @ViewBuilder label: () -> Label)

    public init<V>(destination: Destination, tag: V, selection: Binding<V?>,
                          @ViewBuilder label: () -> Label) where V : Hashable
//....
}

Al crear un NavigationLink, indica la Vista a la que se realiza la transición, así como la Vista que envuelve el NavigationLink, es decir, al interactuar con él, se activa el NavigationLink. Más información sobre las posibles formas de inicializar NavigationLink en la documentación de Apple.

Sin embargo, debe tenerse en cuenta que no hay acceso directo a la pila de View debido a la encapsulación, la navegación se programa solo hacia adelante, es posible regresar solo 1 nivel hacia atrás y luego a través del código encapsulado para el botón "Atrás"



También en SwiftUI no hay navegación de software dinámica. Si la transición no está vinculada a un evento desencadenante, por ejemplo, presionar un botón, pero sigue como resultado de algún tipo de lógica, entonces simplemente no se puede hacer. La transición a la siguiente Vista está necesariamente vinculada al mecanismo NavigationLink, que se establece de forma declarativa inmediatamente al describir la Vista que los contiene. Todas.

Si nuestra pantalla debe contener una transición a muchas pantallas diferentes, entonces el código se vuelve engorroso:

  NavigationView{
            NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {
                Text("Profile")
            }
            NavigationLink(destination: Settings(), isActive: self.$isSettings) {
                           Text("Settings")
                       }
            NavigationLink(destination: Favorite(), isActive: self.$isFavorite) {
                Text("Favorite")
            }
            NavigationLink(destination: Login(), isActive: self.$isLogin) {
                Text("Login")
            }
            NavigationLink(destination: Search(), isActive: self.$isSearch) {
                Text("Search")
            }
}

Podemos gestionar enlaces de varias maneras:
- Control de actividad NavigationLink a través de la propiedad @Binding

 NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {
                Text("Profile")
            }

- control sobre la creación de enlaces a través de una condición (variables de estado)

       if self.isProfile {
            NavigationLink(destination: ProfileView()) {
                Text("Profile")
            }
}

El primer método nos agrega el trabajo de monitorear el estado de las variables de control.
Si planeamos navegar más de 1 nivel por delante, entonces esta es una tarea muy difícil.

En el caso de la pantalla de la lista de elementos similares, todo parece compacto:

 NavigationView{
        List(model.data) { item in
            NavigationLink(destination: NewsItemView(item:item)) {
            NewsItemRow(data: item)
            }
        }

El problema más grave de NavigationLink, en mi opinión, es que todos los enlaces de View especificados no son vagos. Se crean no en el momento en que se activa el enlace, sino en el momento de la creación. Si tenemos una lista de muchos elementos o transiciones a diferentes contenidos pesados ​​de View, esto no afectará el rendimiento de nuestra aplicación de la mejor manera. Si todavía tenemos ViewModel adjunto a estas vistas con lógica en la implementación de la cual la vista del ciclo de vida no se tiene en cuenta o no se tiene en cuenta correctamente, entonces la situación se vuelve muy difícil.

Por ejemplo, tenemos una lista de noticias con elementos del mismo tipo. Nunca hemos pasado a una sola pantalla de una sola noticia, y los modelos ya están en nuestra memoria:



¿Qué podemos hacer en este caso para facilitarnos la vida?

Primero, recuerde que View no existe en el vacío, sino que se representa en UIHostingController.

open class UIHostingController<Content> : UIViewController where Content : View {

    public init(rootView: Content)

    public var rootView: Content
//...
}

Y este es el UIViewController. Entonces podemos hacer lo siguiente. Transferiremos toda la responsabilidad de la transición a la siguiente Vista dentro del nuevo UIHostingController al controlador de la Vista actual. Creemos módulos de navegación y configuración que llamaremos desde nuestra Vista.

El navegador que trabaja con UIViewController se verá así:

class Navigator {
    private init(){}
    
    static let shared = Navigator()
    
    private weak var view: UIViewController?
    
    internal weak var nc: UINavigationController?
        
   func setup(view: UIViewController) {
        self.view = view
    }
     
  internal func open<Content:View>(screen: Content.Type, _ data: Any? = nil) {
     if let vc = ModuleConfig.shared.config(screen: screen)?
        .createScreen(data) {
        self.nc?.pushViewController(vc, animated: true)
        }
   }

Por el mismo principio, crearemos una fábrica de configuradores, que nos dará la implementación del configurador de un módulo específico:

protocol IConfugator: class {
    func createScreen(_ data: Any?)->UIViewController
}

class ModuleConfig{
    private init(){}
    static let shared = ModuleConfig()
    
    func config<Content:View>(screen: Content.Type)->IConfugator? {
        if screen == NewsListView.self {
            return NewsListConfigurator.shared
        }
      // -
        return nil
    }
}

El navegador, por tipo de pantalla, solicita el configurador de un módulo en particular, le transfiere toda la información necesaria.

class NewsListConfigurator: IConfugator {
    static let shared = NewsListConfigurator()
    
    func createScreen(_ data: Any?) -> UIViewController {
         var view = NewsListView()
        let presenter = NewsListPresenter()
         let interactor = NewsListInteractor()
        
        interactor.output = presenter
        presenter.output = view
        view.output = interactor
        
        let vc = UIHostingController<ContainerView<NewsListView>>
                    (rootView: ContainerView(content: view))
        return vc
    }
}

El configurador regala el UIViewController, que es el navegador y empuja el UINavigationController a la pila compartida.



Reemplace NavigationLink en el código con una llamada a Navigator. Como desencadenante, tendremos un evento de hacer clic en un elemento de la lista:

  List(model.data) { item in
            NewsItemRow(data: item)
                .onTapGesture {
                Navigator.shared.open(screen: NewsItemView.self, item)
            }
        }

Nada nos impide invocar Navigator en cualquier método de Vista de la misma manera. No solo dentro del cuerpo.

Además del hecho de que el código se ha vuelto notablemente más limpio, también descargamos la memoria. Después de todo, con este enfoque, la Vista se creará solo cuando se llame.



Ahora nuestra aplicación SwiftUI es más fácil de expandir y modificar. El código es limpio y hermoso.
Puede encontrar el código de ejemplo aquí .

La próxima vez hablaremos sobre la implementación más profunda de Combine.

All Articles