Controlador de cebolla. Rompemos las pantallas en partes

El diseño atómico y el diseño del sistema son populares en el diseño: esto es cuando todo consiste en componentes, desde controles hasta pantallas. No es difícil para un programador escribir controles separados, pero ¿qué hacer con pantallas enteras?


Echemos un vistazo al ejemplo de Año Nuevo:


  • peguemos todo junto;
  • dividido en controladores: seleccione navegación, plantilla y contenido;
  • reutilizar código para otras pantallas.


Todo en un montón


La pantalla de este año nuevo habla sobre los horarios especiales de apertura de las pizzerías. Es bastante simple, por lo que no será un delito convertirlo en un controlador:



Pero La próxima vez, cuando necesitemos una pantalla similar, tendremos que repetirla nuevamente y luego hacer los mismos cambios en todas las pantallas. Bueno, no sucede sin ediciones.


Por lo tanto, es más razonable dividirlo en partes y usarlo para otras pantallas. Destaqué tres:


  • navegación
  • una plantilla con un área para contenido y un lugar para acciones en la parte inferior de la pantalla,
  • Contenido único en el centro.

Seleccione cada parte en su propio UIViewController .


Navegación de contenedores


Los ejemplos más llamativos de contenedores de navegación son UINavigationController y UITabBarController . Cada uno ocupa una franja en la pantalla bajo sus propios controles, y deja el espacio restante para otro UIViewController .


En nuestro caso, habrá un contenedor para todas las pantallas modales con un solo botón de cierre.


Cual es el punto?

Si queremos mover el botón hacia la derecha, solo necesitaremos cambiarlo en un controlador.


O, si decidimos mostrar todas las ventanas modales con una animación especial, y cerrar de forma interactiva con un deslizamiento, como en las tarjetas de historia de AppStore. Entonces, UIViewControllerTransitioningDelegate deberá configurarse solo para este controlador.



Puede usar una container view para separar los controladores: creará una UIView en el padre e insertará la UIView controlador hijo en él.



Estire la container view hasta el borde de la pantalla. Safe area se aplicará automáticamente al controlador secundario:



Patrón de pantalla


El contenido es obvio en la pantalla: imagen, título, texto. El botón parece ser parte de él, pero el contenido es dinámico en diferentes iPhones, y el botón es fijo. Son visibles dos sistemas con diferentes tareas: uno muestra el contenido y el otro lo integra y lo alinea. Deben dividirse en dos controladores.



El primero es responsable del diseño de la pantalla: el contenido debe estar centrado y el botón clavado en la parte inferior de la pantalla. El segundo dibujará el contenido.



Sin una plantilla, todos los controladores son similares, pero los elementos bailan.

Los botones en la última pantalla son diferentes, depende del contenido. La delegación ayudará a resolver el problema: la plantilla del controlador solicitará controles del contenido y los mostrará en su UIStackView .


 // OnboardingViewController.swift protocol OnboardingViewControllerDatasource { var supportingViews: [UIView] { get } } // NewYearContentViewController.swift extension NewYearContentViewController: OnboardingViewControllerDatasource { var supportingViews: [UIView] { return [view().doneButton] } } 

¿Por qué ver ()?

UIViewController leer acerca de cómo especializarse UIView con UIViewController en mi último artículo Controlador, ¡ UIView con calma! Sacamos el código en UIView.


Los botones se pueden conectar al controlador a través de objetos relacionados. Su IBOutlet e IBAction se almacenan en el controlador de contenido, solo los elementos no se agregan a la jerarquía.



Puede obtener elementos del contenido y agregarlos a la plantilla en la etapa de preparación de UIStoryboardSegue :


 // OnboardingViewController.swift override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let buttonsDatasource = segue.destination as? OnboardingViewControllerDatasource { view().supportingViews = buttonsDatasource.supportingViews } } 

En el setter, agregamos controles a UIStackView :


 // OnboardingView.swift var supportingViews: [UIView] = [] { didSet { for view in supportingViews { stackView.addArrangedSubview(view) } } } 

Como resultado, nuestro controlador se dividió en tres partes: navegación, plantilla y contenido. En la imagen, toda la container view muestra en gris:



Tamaño del controlador dinámico


El controlador de contenido tiene su propio tamaño máximo, está limitado por constraints internas.


Container view agrega constores basados ​​en la Autoresizing mask y entran en conflicto con las dimensiones internas del contenido. El problema se resuelve en el código: en el controlador de contenido, debe indicar que no está afectado por los constores de la Autoresizing mask :


 // NewYearContentViewController.swift override func loadView() { super.loadView() view.translatesAutoresizingMaskIntoConstraints = false } 


Hay dos pasos más para Interface Builder:


Paso 1. Especifique el Intrinsic size para la vista UIView . Los valores reales aparecerán después del lanzamiento, pero por ahora pondremos los adecuados.



Paso 2. Para el controlador de contenido, especifique Simulated Size . Puede que no coincida con el tamaño pasado.


Hubo errores de diseño, ¿qué debo hacer?

Se producen errores cuando AutoLayout no puede descubrir cómo descomponer los elementos en el tamaño actual.


Muy a menudo, el problema desaparece después de cambiar las prioridades de la constante. UIView dejarlos para que uno de los UIView pueda expandirse / contraerse más que los demás.


Nos dividimos en partes y escribimos código


Dividimos el controlador en varias partes, pero hasta ahora no podemos reutilizarlos, la interfaz de UIStoryboard difícil de extraer en partes. Si necesitamos transferir algunos datos al contenido, tendremos que acceder a ellos a través de toda la jerarquía. Debería ser al revés: primero tome el contenido, configúrelo y luego envuélvalo en los contenedores necesarios. Como una bombilla


Tres tareas aparecen en nuestro camino:


  1. Separe cada controlador en su propio UIStoryboard .
  2. Rechace la container view , agregue controladores a los contenedores en el código.
  3. Ata todo de nuevo.

Compartir UIStoryboard


UIStoryboard crear dos UIStoryboard adicionales y copiar y pegar el controlador de navegación y el controlador de plantilla en ellos. Embed segue se interrumpirá, pero la container view del container view con restricciones configuradas se transferirá. Las restricciones deben guardarse y la container view del container view debe reemplazarse con una vista de UIView normal.


La forma más fácil es cambiar el tipo de vista de Contenedor en el código de UIStoryboard.
  • abra UIStoryboard como un código (menú contextual del archivo → Abrir como ... → Código fuente);
  • cambie el tipo de containerView para view . Es necesario cambiar las etiquetas de apertura y cierre .


    Del mismo modo, puede cambiar, por ejemplo, UIView a UIScrollView , si es necesario. Y viceversa.




Configuramos el controlador a la propiedad de is initial view controller , y llamaremos al UIStoryboard como controlador.


Cargamos el controlador desde UIStoryboard.

Si el nombre del controlador coincide con el nombre de UIStoryboard , la descarga se puede envolver en un método que encontrará el archivo deseado:


 protocol Storyboardable { } extension Storyboardable where Self: UIViewController { static func instantiateInitialFromStoryboard() -> Self { let controller = storyboard().instantiateInitialViewController() return controller! as! Self } static func storyboard(fileName: String? = nil) -> UIStoryboard { let storyboard = UIStoryboard(name: fileName ?? storyboardIdentifier, bundle: nil) return storyboard } static var storyboardIdentifier: String { return String(describing: self) } static var storyboardName: String { return storyboardIdentifier } } 

Si el controlador se describe en .xib , el constructor estándar se cargará sin tales bailes. Por desgracia, .xib puede contener solo un controlador, a menudo esto no es suficiente: en un buen caso, una pantalla consta de varias. Por lo tanto, usamos UIStoryborad , es fácil dividir la pantalla en partes.


Agregar un controlador en el código


Para que el controlador funcione correctamente, necesitamos todos los métodos de su ciclo de vida: will/did-appear/disappear .


Para la visualización correcta, debe llamar a 5 pasos:


  willMove(toParent parent: UIViewController?) addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?) 

Apple sugiere reducir el código a 4 pasos, porque addChild() llamará willMove(toParent) . En resumen:


  addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?) 

Para simplificar, puede envolverlo todo en extension . Para nuestro caso, necesitamos una versión con insertSubview() .


 extension UIViewController { func insertFullframeChildController(_ childController: UIViewController, toView: UIView? = nil, index: Int) { let containerView: UIView = toView ?? view addChild(childController) containerView.insertSubview(childController.view, at: index) containerView.pinToBounds(childController.view) childController.didMove(toParent: self) } } 

Para eliminar, necesita los mismos pasos, solo que en lugar del controlador principal, debe establecer nil . Ahora removeFromParent() llama a didMove(toParent: nil) , y el diseño no es necesario. La versión abreviada es muy diferente:


  willMove(toParent: nil) view.removeFromSuperview() removeFromParent() 

Diseño


Establecer restricciones


Para establecer correctamente el tamaño del controlador, usaremos AutoLayout . Necesitamos clavar todos los lados a todos los lados:


 extension UIView { func pinToBounds(_ view: UIView) { view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: topAnchor), view.bottomAnchor.constraint(equalTo: bottomAnchor), view.leadingAnchor.constraint(equalTo: leadingAnchor), view.trailingAnchor.constraint(equalTo: trailingAnchor) ]) } } 

Agregar un controlador secundario en el código


Ahora todo se puede combinar:


 // ModalContainerViewController.swift public func embedController(_ controller: UIViewController) { insertFullframeChildController(controller, index: 0) } 

Debido a la frecuencia de uso, podemos envolver todo esto en extension :


 // ModalContainerViewController.swift extension UIViewController { func wrapInModalContainer() -> ModalContainerViewController { let modalController = ModalContainerViewController.instantiateInitialFromStoryboard() modalController.embedController(self) return modalController } } 

También se necesita un método similar para el controlador de plantilla. prepare(for segue:) solía configurarse en prepare(for segue:) , pero ahora puede vincularlo en el método de prepare(for segue:) del controlador:


 // OnboardingViewController.swift public func embedController(_ controller: UIViewController, actionsDatasource: OnboardingViewControllerDatasource) { insertFullframeChildController(controller, toView: view().contentContainerView, index: 0) view().supportingViews = actionsDatasource.supportingViews } 

Crear un controlador se ve así:


 // MainViewController.swift @IBAction func showModalControllerDidPress(_ sender: UIButton) { let content = NewYearContentViewController.instantiateInitialFromStoryboard() //     let onboarding = OnboardingViewController.instantiateInitialFromStoryboard() onboarding.embedController(contentController, actionsDatasource: contentController) let modalController = onboarding.wrapInModalContainer() present(modalController, animated: true) } 

Conectar una nueva pantalla a la plantilla es simple:


  • eliminar lo que no es relevante para el contenido;
  • especificar botones de acción implementando el protocolo OnboardingViewControllerDatasource;
  • escriba un método que vincule una plantilla y contenido.

Más sobre contenedores


Barra de estado


A menudo es necesario que la visibilidad de la status bar sea ​​controlada por un controlador con contenido, no un contenedor. Hay un par de property para esto:


 // UIView.swift var childForStatusBarStyle: UIViewController? var childForStatusBarHidden: UIViewController? 

Con estas property puede crear una cadena de controladores, este último será responsable de mostrar la status bar .


Zona segura


Si los botones del contenedor se superponen al contenido, entonces debe aumentar la zona safeArea . Esto se puede hacer en código: establezca additinalSafeAreaInsets para controladores secundarios. Puedes llamarlo desde embedController() :


 private func addSafeArea(to controller: UIViewController) { if #available(iOS 11.0, *) { let buttonHeight = CGFloat(30) let topInset = UIEdgeInsets(top: buttonHeight, left: 0, bottom: 0, right: 0) controller.additionalSafeAreaInsets = topInset } } 

Si agrega 30 puntos en la parte superior, el botón dejará de superponer contenido y safeArea ocupará el área verde:



Márgenes Preservar los márgenes de supervisión


Los controladores tienen margins estándar. Por lo general, son iguales a 16 puntos de cada lado de la pantalla y solo en tamaños Plus son 20 puntos.


Según los margins puede crear constantes, la sangría en el borde será diferente para diferentes iPhones:



Cuando ponemos una UIView en otra, los margins se reducen a la mitad: a 8 puntos. Para evitar esto, debe incluir Preserve superview margins . Luego, los margins UIView secundario serán iguales a los margins padre. Es adecuado para contenedores de pantalla completa.


El final


Los controladores de contenedores son una herramienta poderosa. Simplifican el código, separan las tareas y pueden reutilizarse. Puede escribir controladores anidados de cualquier manera: en UIStoryboard , en .xib o simplemente en código. Lo más importante es que son fáciles de crear y divertidos de usar.


Un ejemplo de un artículo sobre GitHub


¿Tienes pantallas desde las cuales valdría la pena hacer una plantilla? ¡Comparte en los comentarios!

Source: https://habr.com/ru/post/es434470/


All Articles