Controlador de cebola. Dividimos telas em partes

O design atômico e o design do sistema são populares no design: é quando tudo consiste em componentes, de controles a telas. Não é difícil para um programador escrever controles separados, mas o que fazer com telas inteiras?


Vamos dar uma olhada no exemplo do ano novo:


  • vamos juntar tudo;
  • dividido em controladores: selecione navegação, modelo e conteúdo;
  • reutilize o código para outras telas.


Tudo em um monte


A tela deste ano novo fala sobre o horário de funcionamento especial das pizzarias. Como é bastante simples, não será crime torná-lo um controlador:



Mas Da próxima vez, quando precisarmos de uma tela semelhante, teremos que repeti-la novamente e fazer as mesmas alterações em todas as telas. Bem, isso não acontece sem edições.


Portanto, é mais razoável dividi-lo em partes e usá-lo para outras telas. Eu destaquei três:


  • navegação
  • um modelo com uma área para conteúdo e um local para ações na parte inferior da tela,
  • conteúdo exclusivo no centro.

Selecione cada parte em seu próprio UIViewController .


Navegação em contêiner


Os exemplos mais impressionantes de contêineres de navegação são o UINavigationController e o UITabBarController . Cada um ocupa uma faixa na tela sob seus próprios controles e deixa o espaço restante para outro UIViewController .


No nosso caso, haverá um contêiner para todas as telas modais com apenas um botão Fechar.


Qual é o objetivo?

Se quisermos mover o botão para a direita, precisaremos apenas alterá-lo em um controlador.


Ou, se decidirmos mostrar todas as janelas modais com uma animação especial e fechar de forma interativa com um furto, como nos cartões de histórias da AppStore. Então UIViewControllerTransitioningDelegate precisará ser definido apenas para este controlador.



Você pode usar uma container view para separar controladores: ele criará um UIView no pai e inserirá o UIView controlador filho nele.



Estique a container view até a borda da tela. Safe area será aplicada automaticamente ao controlador filho:



Padrão de tela


O conteúdo é óbvio na tela: imagem, título, texto. O botão parece fazer parte dele, mas o conteúdo é dinâmico em diferentes iPhones e o botão está fixo. Dois sistemas com tarefas diferentes são visíveis: um exibe o conteúdo e o outro o incorpora e alinha. Eles devem ser divididos em dois controladores.



O primeiro é responsável pelo layout da tela: o conteúdo deve ser centralizado e o botão pregado na parte inferior da tela. O segundo irá desenhar o conteúdo.



Sem um modelo, todos os controladores são semelhantes, mas os elementos dançam.

Os botões na última tela são diferentes - depende do conteúdo. A delegação ajudará a resolver o problema: o modelo do controlador solicitará controles do conteúdo e os exibirá em seu UIStackView .


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

Por que visualizar ()?

UIViewController pode ler sobre como especializar o UIView com o UIViewController no meu último artigo Controller, vá com calma! Nós retiramos o código no UIView.


Os botões podem ser conectados ao controlador através de objetos relacionados. Seus IBOutlet e IBAction são armazenados no controlador de conteúdo, apenas os elementos não são adicionados à hierarquia.



Você pode obter elementos do conteúdo e adicioná-los ao modelo na fase de preparação do UIStoryboardSegue :


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

No setter, adicionamos controles ao UIStackView :


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

Como resultado, nosso controlador foi dividido em três partes: navegação, modelo e conteúdo. Na figura, todas as container view mostradas em cinza:



Tamanho dinâmico do controlador


O controlador de conteúdo tem seu próprio tamanho máximo, é limitado por constraints internas.


Container view adiciona constores com base na Autoresizing mask e eles entram em conflito com as dimensões internas do conteúdo. O problema foi resolvido no código: no controlador de conteúdo, você precisa indicar que ele não é afetado pelos constores da Autoresizing mask :


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


Há mais duas etapas para o Interface Builder:


Etapa 1. Especifique o Intrinsic size para o UIView . Os valores reais aparecerão após o lançamento, mas, por enquanto, colocaremos os adequados.



Etapa 2. Para o controlador de conteúdo, especifique Simulated Size . Pode não corresponder ao tamanho anterior.


Houve erros de layout, o que devo fazer?

Os erros ocorrem quando o AutoLayout não consegue descobrir como decompor os elementos no tamanho atual.


Na maioria das vezes, o problema desaparece após a alteração das prioridades da constante. Você precisa colocá-los para que um dos UIView possa expandir / contrair mais do que os outros.


Dividimos em partes e escrevemos em código


Dividimos o controlador em várias partes, mas até agora não podemos reutilizá-las, a interface do UIStoryboard difícil de extrair em partes. Se precisarmos transferir alguns dados para o conteúdo, teremos que bater neles por toda a hierarquia. Deve ser o contrário: primeiro pegue o conteúdo, configure-o e depois embrulhe-o nos recipientes necessários. Como uma lâmpada.


Três tarefas aparecem no nosso caminho:


  1. Separe cada controlador em seu próprio UIStoryboard .
  2. Recusar container view , adicione controladores a contêineres no código.
  3. Amarre tudo de volta.

Compartilhando o UIStoryboard


Você precisa criar dois UIStoryboard adicionais e copiar e colar o controlador de navegação e o controlador de modelo neles. Embed segue será interrompida, mas a container view do container view com restrições configuradas será transferida. As restrições devem ser salvas e a container view do container view deve ser substituída por uma UIView regular.


A maneira mais fácil é alterar o tipo de exibição Container no código UIStoryboard.
  • abrir o UIStoryboard como um código (menu de contexto do arquivo → Abrir como ... → Código fonte);
  • altere o tipo de containerView para view . É necessário alterar as tags de abertura e fechamento .


    Da mesma maneira, você pode alterar, por exemplo, UIView para UIScrollView , se necessário. E vice-versa.




Definimos o controlador como a propriedade is initial view controller e chamaremos o UIStoryboard como o controlador.


Carregamos o controlador do UIStoryboard.

Se o nome do controlador corresponder ao nome do UIStoryboard , o download poderá ser UIStoryboard em um método que, por si só, encontrará o arquivo desejado:


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

Se o controlador for descrito em .xib , o construtor padrão será carregado sem essas danças. Infelizmente, o .xib pode conter apenas um controlador, geralmente isso não é suficiente: em um bom caso, uma tela consiste em vários. Portanto, usamos o UIStoryborad , é fácil dividir a tela em partes.


Adicionar um controlador no código


Para que o controlador funcione corretamente, precisamos de todos os métodos de seu ciclo de vida: will/did-appear/disappear .


Para a exibição correta, você precisa chamar 5 etapas:


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

A Apple sugere reduzir o código para 4 etapas, porque o próprio addChild() chamará willMove(toParent) . Em resumo:


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

Para simplificar, você pode agrupar tudo em extension . Para o nosso caso, precisamos de uma versão com 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 excluir, você precisa das mesmas etapas; somente em vez do controlador pai, você precisa definir nil . Agora removeFromParent() chama didMove(toParent: nil) e o layout não é necessário. A versão abreviada é muito diferente:


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

Layout


Definir restrições


Para definir corretamente o tamanho do controlador, usaremos o AutoLayout . Precisamos pregar todos os lados para todos os 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) ]) } } 

Adicionar um controlador filho no código


Agora tudo pode ser combinado:


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

Devido à frequência de uso, podemos agrupar tudo isso em extension :


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

Um método semelhante também é necessário para o controlador de modelo. prepare(for segue:) costumavam ser configuradas no prepare(for segue:) , mas agora você pode vinculá-lo ao método de incorporação do controlador:


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

A criação de um controlador é assim:


 // 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 uma nova tela ao modelo é simples:


  • remova o que não é relevante para o conteúdo;
  • especificar botões de ação implementando o protocolo OnboardingViewControllerDatasource;
  • escreva um método que vincule um modelo e conteúdo.

Mais sobre contêineres


Barra de status


Geralmente, é necessário que a visibilidade da status bar seja controlada por um controlador com conteúdo, não por um contêiner. Há algumas property para isso:


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

Usando essas property você pode criar uma cadeia de controladores, que serão responsáveis ​​por exibir a status bar .


Área segura


Se os botões do contêiner se sobrepuserem ao conteúdo, você deverá aumentar a zona safeArea . Isso pode ser feito no código: defina additinalSafeAreaInsets para controladores filhos. Você pode chamá-lo em 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 } } 

Se você adicionar 30 pontos na parte superior, o botão interromperá a sobreposição de conteúdo e o safeArea ocupará a área verde:



Margens. Preservar margens de visão geral


Controladores têm margins padrão. Geralmente, eles são iguais a 16 pontos de cada lado da tela e apenas nos tamanhos Plus são 20 pontos.


Com base nas margins você pode criar constantes, o recuo até a borda será diferente para diferentes iPhones:



Quando colocamos uma UIView em outra, as margins são reduzidas pela metade: para 8 pontos. Para evitar isso, você precisa incluir Preserve superview margins . Em seguida, as margins UIView filho serão iguais às margins pai. É adequado para recipientes de tela cheia.


O fim


Os controladores de contêiner são uma ferramenta poderosa. Eles simplificam o código, separam tarefas e podem ser reutilizados. Você pode escrever controladores aninhados de qualquer maneira: no UIStoryboard , no .xib ou simplesmente no código. Mais importante, eles são fáceis de criar e divertidos de usar.


Um exemplo de um artigo no GitHub


Você tem telas nas quais valeria a pena fazer um modelo? Compartilhe nos comentários!

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


All Articles