UIKit: renderizando componentes no SwiftUI

Brenno de Moura
ioasys-voices
Published in
5 min readSep 27, 2021

--

Ao construir uma view em SwiftUI, nem sempre chegamos ao resultado desejado. Algumas views ainda contam com limitações do iOS, exigindo a aplicação suporte como versão mínima do iOS mais recente. No entanto, o UIKit é um framework maduro que contém uma infinita variedade de views e possibilidades a serem construídas.

O SwiftUI disponibiliza como parte da sua API declarativa o UIViewRepresentable e o UIViewControllerRepresentable. Ambos são protocolos importantes para a compatibilidade entre os dois frameworks da Apple, porém um pouco complicados e confusos quanto a sua implementação e uso adequado. Além disso, nem sempre o resultado é alcançado, apresentando inconsistências de layout e usabilidade.

Dessa forma, o SwiftUI disponibiliza a UIHostingController como a única forma de transformar as views declarativas em views do UIKit. E neste artigo será detalhado a implementação do Host que torna o UIKit acessível ao SwiftUI. A implementação da view Host exige o conhecimento de alguns conceitos do SwiftUI que são importantes para compreender a solução.

Definição

O framework da Apple tem duas características fundamentais que alteram a forma de desenvolvimento. A primeira é que não há suporte nativo nas constraints. Se a UIView encapsulada usa constraints para definir a largura e altura dinamicamente, é muito provável que o layout irá ficar esquisito. A segunda está relacionada a maneira como o SwiftUI reconstrói suas views, sendo comum que a atualização dos dados entre as duas views seja instável. A implementação de uma view declarativa e genérica precisa considerar esses dois fatores.

O Host utiliza o formato declarativo para receber a view encapsulada. Para isso, é necessário utilizar o callback que retorna um objeto do tipo UIView. Esse callback é chamado toda vez que o estado da view declarativa altera, notificado pela função updateUIViewController(_:context:) do framework da Apple.

Quanto ao auto layout, temos apenas quatro opções de uso: (a) vertical e horizontal fixo; (b) vertical dinâmico e horizontal fixo; (c) vertical fixo e horizontal dinâmico; e (d) vertical e horizontal dinâmico. Isso pode ser representado por duas variáveis booleanas para o controle do layout vertical e horizontal.

A implementação do Host pode ser dividida em quatro views: (a) Host; (b) HostView; (c) HostingViewController; e (d) HostingView. As views (a) e (b) são do SwiftUI montando e recarregando o layout conforme os eventos da View. As views (c) e (d) são do UIKit e renderizam a view encapsulada aplicando as constraints.

Hosting View

A HostingView contém três métodos e duas variáveis para controle do auto layout e renderização da view encapsulada. O primeiro método é o init(vertical:horizontal:) que define as variáveis de layout para aplicação das constraints.

O segundo método é o didMoveToSuperview, que deve ser sobrescrito para definir as constraints verticais e horizontais conforme as duas propriedades booleanas da view. Conforme a Figura 1, é detalhado o processo de configurar as constraints verticais (topo e baixo) e as horizontais (direita e esquerda) com base nos valores configurados pelo init.

Figura 1 — Sobrescrita do didMoveToSuperview para controle de layout da view.

Uma outra regra importante a ser definida nesta implementação é a ativação e desativação das constraints conforme os valores booleanos vertical e horizontal se alterem. Essas propriedades são verdadeiras para quando as constraints são fixas, variando de acordo com o tamanho interno da view encapsulada, e falso para quando é dinâmica expandindo conforme a superview.

A Figura 2 mostra o método hostView(_:) implementado na HostingView para adicionar a view encapsulada na hierarquia. Nesse método é feito apenas a troca da view que estava sendo encapsulada e configurado as constraints de borda entre a view e a HostingView.

Figura 2 — Implementação do método hostView(_:)

HostingViewController

A HostingViewController é usada para criar a UIViewController na hierarquia antes de adicionar uma UIView. Esse processo é fundamental porque nenhuma UIView existe sem uma controller, ganhando várias propriedades de controle tanto de layout quanto de ciclo de vida.

Essa view controller precisa configurar a propriedade preferredContentSize quando o método viewDidLayoutSubviews é chamado. Além disso, no loadView é preciso carregar a view fazendo o encapsulamento dela com a HostingView.

A Figura 3 mostra a sobrescrita dos métodos do UIKit, a implementação parcial da função fixedSize(horizontal:vertical:) e a variável currentView. Com base nisso, a view do SwiftUI consegue passar para a HostingView a view a ser encapsulada e as configurações de constraints horizontal e vertical.

Figura 3 — Implementação da HostingViewController com os métodos para configurar a HostingView.

HostView

A HostView implementa o protocolo UIViewControllerRepresentable fazendo a integração do UIKit com o SwiftUI. É preciso fazer a implementação dos dois métodos do protocolo para carregar a HostingViewController e configurar as constraints.

A Figura 4 detalha a implementação dessa view com os métodos makeUIViewController(context:) e updateUIViewController(_:context:). Com isso, ao ser chamado pelo body do SwiftUI da Host, a UIView é recriada e configurada corretamente com os valores de constraints, mantendo o layout íntegro.

Figura 4 — Implementação da HostView.

Host

A Host View é uma view do SwiftUI que encapsula a HostView passando os parâmetros necessários para a configuração correta da view. Além disso, é importante chamar o método do framework declarativo fixedSize(horizontal:vertical:) para que ele leia o preferredContentSize da view controller.

Essa view também precisa de um método para que sejam alterados os valores das constraints vertical e horizontal. Elas, por padrão, são configuradas como verdadeiro, respeitando o tamanho da view encapsulada. Esse método declarativo usado pela aplicação transforma o self mutável e altera o valor das propriedades.

A Figura 5 mostra a implementação da Host com a definição do body e do método fixedSize(horizontal:vertical:). Dessa forma, é possível utilizar a view Host no SwiftUI para encapsular qualquer view do UIKit mantendo o layout íntegro.

Figura 5 — Implementação da Host.

Considerações finais

A implementação discutida neste artigo foi baseada em outras soluções publicadas no Medium e também em fóruns de discussões de problemas envolvendo a integração dos dois frameworks. Essa solução não é perfeita, mas resolve a maioria dos problemas sendo fortemente indicada para os projetos.

Views como UILabel, UIButton e com baixa complexidade de layout correspondem melhor a essa implementação. Já views que agrupam várias views com um layout mais ambíguo para o SwiftUI podem apresentar inconsistências.

No entanto, o objetivo dessa solução é permitir que grande parte do layout seja escrito em SwiftUI e as pequenas partes, incompatíveis com o ele, sejam integradas com o UIKit.

Obrigado pela leitura!

--

--

Brenno de Moura
ioasys-voices

Software engineer with a passion for technology and a focus on declarative programming, experience in challenging projects and multidisciplinary teams