Incorporado na versão Swift 5.1, os tipos de retorno opacos são uma funcionalidade da linguagem que geralmente está associada ao SwiftUI e ao tipo some View usado para construir visualizações com ele. Mas assim como as outras características do Swift que impulsionam o DSL do SwiftUI, os tipos de retorno opacos são uma funcionalidade geral que pode ser usada em muitos contextos diferentes.
Esta semana, vamos dar uma olhada mais de perto nos tipos de retorno opacos — como eles podem ser usados tanto com quanto sem o SwiftUI e como se compara a técnicas similares de programação genérica, como a substituição de tipo.

Swift by Sundell é trazido a você pelo Genius Scan SDK — Adicione um poderoso scanner de documentos a qualquer aplicativo móvel e transforme as escaneações em PDFs de alta qualidade com uma linha de código. Tente hoje.
Os tipos de retorno opacos nos permitem fazer basicamente duas coisas. Primeiro, eles nos permitem aproveitar a capacidade do compilador Swift de inferir tipos para evitar ter que declarar exatamente qual tipo uma determinada função ou propriedade computada retornará; em segundo lugar, eles escondem esses tipos inferidos dos chamadores dessas APIs.
Para ver o que isso significa na prática, vamos dizer que construímos a seguinte visualização SwiftUI, que renderiza dois Text views verticalmente usando um VStack:
struct TextView: View {
var title: String
var subtitle: String
var body: some View {
VStack(alignment: .leading) {
Text(title).bold()
Text(subtitle).foregroundColor(.secondary)
}
}
}A cima, estamos usando um tipo de retorno opaco, some View, para o body da nossa visualização, que é uma convenção comum ao construir visualizações SwiftUI.
O motivo disso é que o SwiftUI faz uso intensivo do sistema de tipos Swift para realizar tarefas como a diferenciação e garantir a segurança total de tipo em toda a hierarquia de visualizações — o que significa que acabamos com tipos retornados bastante complexos, mesmo para visualizações simples, já que nossa própria hierarquia de visualização é essencialmente codificada no próprio sistema de tipos.
Por exemplo, o body da visualização acima resulta no seguinte tipo:
VStack<TupleView<(Text, Text)>>Se você está atualmente trabalhando em um projeto baseado no SwiftUI, tente chamar type(of:) na body de uma das suas visualizações e tenho certeza que verá um tipo ainda mais complexo, especialmente se essa visualização tiver modificadores aplicados a ela.
Ao fato de não termos que especificar explicitamente os tipos exatos de nossas visualizações SwiftUI é realmente uma coisa boa, já que caso contrário teríamos que modificar o tipo de retorno body de cada visualização toda vez que alterássemos sua hierarquia, o que tornaria o SwiftUI muito mais difícil de usar.
No entanto, usar um tipo de retorno opaco requer que todas as ramificações do código dentro de uma determinada função ou propriedade retornem exatamente o mesmo tipo — caso contrário, o compilador não seria capaz de inferir esse tipo para nós. Isso pode levar a algumas situações complicadas sempre que estamos lidando com algum tipo de lógica condicional, por exemplo para determinar se mostrar um spinner de carregamento ou o conteúdo real de uma visualização SwiftUI — como nesta ProductView:
struct ProductView: View {
@ObservedObject var viewModel: ProductViewModel
var body: some View {
switch viewModel.state {
case .isLoading:
Wrap(UIActivityIndicatorView()) {
$0.startAnimating()
}
case .finishedLoading:
TextView(
title: viewModel.productName,
subtitle: viewModel.formattedPrice
)
}
}
}A cima, estamos usando a view Wrap de “Inline wrapping of UIKit or AppKit views within SwiftUI” para poder trazer facilmente o UIActivityIndicatorView do UIKit para dentro do SwiftUI.
Tentando compilar o código acima nos dará o seguinte erro:
Função declara um tipo de retorno opaco, mas as
instruções de retorno em seu corpo não têm tipos subjacentes correspondentes.Uma maneira de corrigir esse problema seria usar type erasure para dar ao nossos dois casos do switch o mesmo tipo de retorno — AnyView, neste caso — que é um wrapper incorporado que nos permite apagar o tipo subjacente de uma view SwiftUI, assim:
struct ProductView: View {
@ObservedObject var viewModel: ProductViewModel
var body: some View {
switch viewModel.state {
case .isLoading:
AnyView(Wrap(UIActivityIndicatorView()) {
$0.startAnimating()
})
case .finishedLoading:
AnyView(TextView(
title: viewModel.productName,
subtitle: viewModel.formattedPrice
))
}
}
}Nosso código agora compila, mas ter que fazer o tipo de envoltório acima sempre que temos algum tipo de condição dentro de uma das nossas views pode se tornar um pouco tedioso, então vamos explorar algumas outras rotas também.
No Swift 5.3, duas mudanças importantes foram feitas no funcionalidade de construtores de funções que o SwiftUI usa para permitir que várias expressões separadas sejam combinadas em um único tipo de retorno. Primeiro, agora podemos usar declarações switch dentro de uma função, propriedade ou fechamento com suporte a construtores de funções — e segundo, cada view body agora herda o atributo @ViewBuilder da própria declaração do protocolo View.
O que isso significa neste contexto é que uma vez que estejamos prontos para atualizar para Swift 5.3 e Xcode 12, poderemos refatorar a acima mencionada ProductView removendo ambos os usos de AnyView, e nossas palavras-chave return — o que nos dá a seguinte implementação:
struct ProductView: View {
@ObservedObject var viewModel: ProductViewModel
var body: some View {
switch viewModel.state {
case .isLoading:
ProgressView()
case .finishedLoading:
TextView(
title: viewModel.productName,
subtitle: viewModel.formattedPrice
)
}
}
}A cima, substituímos nossa embalagem inline de UIActivityIndicatorView com o ProgressView que agora vem integrado em várias plataformas da Apple.
Isto é muito legal, mas ainda mais interessante é que podemos replicar quase exatamente o mesmo comportamento no Swift 5.2 também. Ao adicionar manualmente o atributo @ViewBuilder à propriedade body de nossa view, podemos usar uma declaração combinada if/else para implementar a mesma lógica condicional, embora de forma um pouco menos futurista em comparação com o uso de uma instrução switch exaustiva:
struct ProductView: View {
@ObservedObject var viewModel: ProductViewModel
@ViewBuilder var body: some View {
if viewModel.state == .isLoading {
Wrap(UIActivityIndicatorView()) {
$0.startAnimating()
}
} else {
TextView(
title: viewModel.productName,
subtitle: viewModel.formattedPrice
)
}
}
}Dentro do contexto de SwiftUI, os tipos de retorno opacos são usados para nos permitir retornar qualquer expressão View de nossas implementações de body, sem ter que especificar nenhum tipo explícito — desde que cada ramo de código retorne o mesmo tipo, algo que muitas vezes pode ser alcançado usando ViewBuilder.
Agora vamos nos afastar do reino de SwiftUI e explorar como os tipos de retorno opacos poderiam ser usados em outros contextos também. Um caso de uso inicial que podemos pensar é usar um tipo de retorno opaco para realizar a substituição automática de tipos — por exemplo, para retornar some Publisher ao construir uma pipeline de dados alimentada pelo Combine, em vez de ter que especificar exatamente qual tipo de publicador que uma determinada expressão retorna, como neste caso:
struct ModelLoader<Model: Decodable> {
var urlSession = URLSession.shared
var decoder = JSONDecoder()
var url: URL
func load() -> some Publisher {
urlSession.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Model.self, decoder: decoder)
}
}Embora o caso de uso acima seja, na superfície, extremamente similar ao modo como os tipos de retorno opacos são usados dentro do contexto de SwiftUI — acabaremos com um problema substancial neste caso.
Uma das principais diferenças entre tipos opacos e a substituição de tipo convencional é que os tipos opacos não preservam nenhuma informação sobre seus tipos subjacentes, o que significa que o publisher retornado pelo método acima load não terá consciência do tipo genérico Model que está sendo carregado.
Portanto, se quisermos preservar esse tipo de informação de tipo, como neste caso, é melhor usar a substituição de tipo — o que pode ser feito usando AnyPublisher e o operador eraseToAnyPublisher quando usamos Combine:
struct ModelLoader<Model: Decodable> {
var urlSession = URLSession.shared
var decoder = JSONDecoder()
var url: URL
func load() -> AnyPublisher<Model, Error> {
urlSession.dataTaskPublisher(for: url)
.map(<.data>)
.decode(type: Model.self, decoder: decoder)
.eraseToAnyPublisher()
}
}No entanto, há situações em que podemos desejar descartar o tipo de informação genérica acima e nessas circunstâncias, os tipos de retorno opacos podem ser extremamente úteis.
Vamos olhar para um exemplo final, no qual definimos uma protocolo Task usado para modelar uma série de tarefas assíncronas que podem ter sucesso ou falha sem retornar nenhum valor específico:
protocol Task {
typealias Handler = (Result<Void, Error>) -> Void
func perform(then handler: @escaping Handler)
}Com a abordagem acima, podemos usar nosso protocolo Task diretamente e referenciá-lo como qualquer outro tipo, por exemplo retornando uma instância que o conforma de um método, assim:
struct DataUploader {
var fileManager = FileManager.default
var urlSession = URLSession.shared
func taskForUploading(_ data: Data, to url: URL) -> Task {
let file = File(data: data, manager: fileManager)
return FileUploadingTask(
file: file,
url: url,
session: urlSession
)
}
}No entanto, se quisermos adicionar algum requisito de tipo associado ou Self ao nosso protocolo Task, começaremos a encontrar problemas. Por exemplo, podemos querer exigir que todas as tarefas também conformem com o protocolo embutido Identifiable, para podermos rastrear cada tarefa com base em seu ID:
protocol Tarefa: Identificável {
typealias Manipulador = (Resultado<Void, Erro>) -> Void
func executar(logoApos manipulador: @escaping Manipulador)
}Quando fazemos essa alteração, começaremos a receber o seguinte tipo de erro do compilador sempre que nos referirmos diretamente ao protocolo Tarefa, por exemplo na implementação acima da classe DataUploader:
Protocol 'Tarefa' pode ser usado apenas como uma restrição genérica
porque tem requisitos de tipo associado ou Self.Nessa situação, usar um tipo de retorno opaco pode ser uma ótima opção, pois adicionando a palavra-chave some na frente de Tarefa, poderemos continuar usando a mesma implementação que tínhamos antes:
struct DataUploader {
...
func tarefaParaUpload(_ dados: Data, para url: URL) -> some Tarefa {
...
}
}Uma alternativa ao acima seria usar o tipo concreto que estamos realmente retornando como o tipo de retorno do nosso método (TarefaDeUploadDeArquivo no caso acima), mas isso nem sempre é prático ou algo que queremos expor como parte da nossa API pública.
Como exemplo adicional, digamos que quiséssemos adicionar uma API conveniente que nos permitisse facilmente encadear uma tarefa a outra. Para fazer isso, poderíamos criar um tipo privado TarefaEncadeada, que toma dois exemplos de instâncias que seguem o protocolo Tarefa e os chama ambos em sequência quando executados — assim:
private struct TarefaEncadeada<Primeira: Tarefa, Segunda: Tarefa>: Tarefa {
let id = UUID()
var primeira: Primeira
var segunda: Segunda
func executar(logoApos manipulador: @escaping Manipulador) {
primeira.executar({ [segunda] resultado in
switch resultado {
case .sucesso:
segunda.executar(logoApos: manipulador)
case .falha:
manipulador(resultado)
}
})
}
}Note que ainda temos que usar tipos genéricos para nossas propriedades primeira e segunda, pois a palavra-chave some não pode ser aplicada a propriedades cujo tipo concreto o compilador não possa inferir.
Para então criar uma API pública para nosso novo tipo TarefaEncadeada, poderíamos usar um design muito parecido com SwiftUI e estender o protocolo Tarefa em si com um método que retorna uma instância de TarefaEncadeada escondida por trás de um retorno opaco some Tarefa:
extension Tarefa {
func encadeado<T: Tarefa>(para proximaTarefa: T) -> some Tarefa {
TarefaEncadeada(primeira: self, segunda: proximaTarefa)
}
}Com o acima em vigor, podemos agora combinar duas tarefas separadas em uma só, tudo sem precisar saber nada sobre os tipos subjacentes que estão realmente realizando nosso trabalho:
let tarefaDeEnvio = uploader.taskForUploading(dados, to: url)
let tarefaDeConfirmação = ConfirmationUITask()
let tarefaEncadeada = tarefaDeEnvio.chained(to: tarefaDeConfirmação)
chainedTask.perform { resultado in
switch resultado {
case .success:
case .failure(let erro):
}
}Também é possível usar tipos de retorno opacos para esconder informações de tipo genérico por trás de uma API pública muito mais simples, o que pode ser uma técnica ótima a se manter em mente, especialmente ao construir bibliotecas reutilizáveis em Swift.

Os tipos de retorno opacos podem ser descritos como uma característica nichada da linguagem fora do contexto do SwiftUI, mas aprender como eles funcionam ainda pode ser muito valioso — tanto para poder entender melhor os problemas que podemos encontrar ao usá-los em visualizações do SwiftUI quanto por serem realmente úteis ao trabalhar com protocolos genéricos.
Espero que este artigo tenha dado a você um ponto de partida para fazer isso e mostrado alguns exemplos concretos de como os tipos de retorno opacos podem ser usados em diferentes contextos. Se você tiver perguntas, comentários ou feedback, sinta-se à vontade para entrar em contato através de Twitter ou email.
Obrigado por ler! 🚀

