Closures no Swift: o que são e como utilizá-las
Introdução
Se você está aprendendo a linguagem Swift ou já é dev nessa área, pode ter ouvido falar de closures - um recurso fundamental da linguagem para construir aplicativos incríveis.
Closures são blocos de código (você pode considerá-las como funções sem nome ou anônimas) que podem ser atribuídos a variáveis, passados como argumentos para funções e armazenados em estruturas de dados. Em alguns casos, closures são uma “função” que fica dentro de outra função!
A principal vantagem das closures é fornecer uma maneira concisa e flexível de escrever código; elas podem ser usadas em várias situações: desde o uso de funções como argumento até a implementação de operações assíncronas.
Logo, para entender closures, é essencial que você domine funções e conceitos relacionados como argumentos, tipo de retorno etc.
Neste artigo, vamos aprender na prática:
- Exemplo da vida real: criação de botões;
- O que são closures e como escrever sua sintaxe básica;
- 4 Principais utilidades de closures;
- Conclusão.
Vamos lá?
Exemplo da vida real: criação de botões
Imagine que você precise criar um botão. Um jeito de fazer isso seria criar uma função:
import SwiftUI
struct ContentView: View {
func botaoPressionado() -> Void {
print("Botão pressionado")
}
var body: some View {
VStack {
Text("Clique no botão:")
Button("Mostrar Mensagem", action: botaoPressionado)
}
}
}
Nessa abordagem, perceba como o código fica extenso e trabalhoso. Uma alternativa mais simples e comum seria utilizar as Closures (o que é recomendado pelo Xcode). Vamos ver o que são e como escrevê-las?
O que é closure e como escrever sua sintaxe básica
Em Swift, uma closure é uma “função” (sem nome) que fica dentro de outra função (que geralmente tem nome indicado), como já vimos.
Falando de forma técnica, a closure é:
- Uma entidade que encapsula um bloco de código que pode ser executado em um momento posterior;
- Ela captura e armazena referências para variáveis e constantes do ambiente no qual foi criada;
- Pode ser atribuída a variáveis ou constantes, passada como argumentos para funções e retornadas como valor de uma função.
A sintaxe básica para escrever uma closure em Swift é a seguinte:
{ (parâmetros) -> TipoRetorno in
// Código da closure
}
De início, você pode estranhar essa sintaxe diferente, por isso veremos exemplos para esclarecer as ideias. Vejamos um exemplo de closure que recebe dois números inteiros e retorna sua soma:
let somar: (Int, Int) -> Int = { (a, b) -> Int in
return a + b
}
let resultado = somar(3, 5) // resultado será 8
No exemplo acima, a variável somar
recebe uma closure que recebe dois argumentos inteiros (a
e b
) e retorna sua soma. A sintaxe -> Int
indica o tipo de retorno da closure (no nosso caso é um inteiro). O bloco de código da closure está entre as chaves {}
e contém a implementação da operação de soma.
Primeiro, a closure é criada e armazenada em let somar
; em seguida, é executada em let resultado
.
Agora que vimos como escrever uma closure, vamos voltar ao exemplo prático do início do artigo e descobrir como criar um botão com essa abordagem, bem como outras utilidades.
4 Principais utilidades das closures
Utilidade 1 - Usando closures como argumentos: como criar um botão
No início, vimos a criação de um botão com uma função normal:
import SwiftUI
struct ContentView: View {
func botaoPressionado() -> Void {
print("Botão pressionado")
}
var body: some View {
VStack {
Text("Clique no botão:")
Button("Mostrar Mensagem", action: botaoPressionado)
}
}
}
Como podemos simplificar o código com closure?
Ao criar um botão (Button)
com closure, esses botões podem receber alguma função ou bloco de código. Ao clicar nesse botão, é executado o bloco de código ou função que foram passados:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Clique no botão:")
// Usando uma trailing closure para definir a ação do botão
Button("Mostrar Mensagem") {
print("Botão pressionado")
}
}
}
}
Perceba como a implementação do botão fica diferente, comparada com a abordagem de função. Lembre-se de que uma closure pode ser considerada uma espécie de “função sem nome” que deixa o código mais legível (essa é uma razão para utilizar a closure em vez de uma função comum).
Veja que não utilizamos a sintaxe padrão de uma Closure, que teria a sintaxe ->
. Temos somente as chaves {}
. Por qual motivo? A resposta é que, no botão, utilizamos um trailing closure
, ou seja, outra forma utilizar os closures, agora vamos entender como o que é, como identificar e como usar esta técnica.
Utilidade 2 - Usando closures como argumento: entendendo a trailing closure
Também podemos interpretar as closures como funções anônimas que podem ser passadas como argumento. Para fazer isso, existem duas formas. A primeira (padrão) é criar uma função e passá-la como argumento:
// Definindo uma função que recebe uma closure como argumento
func executarOperacao(_ operacao: () -> Void) {
print("Antes da operação")
operacao() // Executa a closure fornecida como argumento
print("Depois da operação")
}
// Definindo uma closure que será passada como argumento
let minhaClosure = {
print("Executando a operação dentro da closure")
}
// Chamando a função e passando a closure como argumento
executarOperacao(minhaClosure)
A closure é criada e armazenada dentro da função func executarOperacao
, executada dentro dela mesmo.
Você consegue perceber um detalhe nesse código?
Bem, ele ficou extenso, com várias linhas. Uma forma de encurtar o código e torná-lo mais legível seriam as trailing closures (como no caso do botão).
Trailing Closures é uma forma concisa de passar closures como argumentos para funções, especialmente quando a closure é o último argumento da função. Em vez de colocar a closure entre parênteses imediatamente após a função, você pode definir a closure fora dos parênteses, tornando o código mais limpo e legível.
Um exemplo prático do mesmo código anterior, mas utilizando a técnica de trailing, ficaria assim:
func executarOperacao(_ operacao: () -> Void) {
// Código omitido
}
// Realizando o mesmo procedimento do último bloco de código com a Trailling Closures
executarOperacao {
print("Executando a operação dentro da closure")
}
Como você pode ver, definimos apenas a closure com -> Void
, porém as chaves ficam vazias. Ou seja, podemos dizer que a trailing closure é só uma closure que não retorna nada, só executa o código! A seguir, em executarOperacao
, escrevemos a closure de forma sucinta, fora da função. Assim, conseguimos omitir algumas linhas de código e deixá-lo mais conciso!
Vamos ver outra utilidade das closures, que é a captura de valores.
Utilidade 3 - Como fazer a captura de valores
Uma das características poderosas das closures é a capacidade de capturar valores do contexto circundante no qual foram definidas. Isso significa que as closures podem acessar e manipular variáveis e constantes mesmo após o término da função ou escopo no qual foram criadas.
Essa técnica é comumente utilizada para criar funções que mantêm estados internos, lembrando informações sobre o contexto em que foram criadas. As closures fornecem uma maneira poderosa e flexível de gerenciar dados e comportamentos, permitindo que você crie código mais modular e reutilizável.
Considere o seguinte exemplo:
func fazerIncremento(incremento: Int) -> () -> Int {
var total = 0
let incrementador: () -> Int = {
total += incremento
return total
}
return incrementador
}
let incrementaEmDois= fazerIncremento(incremento: 2)
print(incrementaEmDois()) // imprime 2
print(incrementaEmDois()) // imprime 4
Neste exemplo, a função fazerIncremento
retorna uma closure que captura e manipula a variável total
do escopo externo. A cada chamada da closure incrementaEmDois
, o valor de total
é incrementado em 2, devido à captura da variável incremento
.
Fez sentido?
Agora, vamos ver a última utilidade das closures, muito comum no dia a dia de trabalho.
Utilidade 4 - Como fazer chamadas assíncronas com async/await e closures
Uma quarta utilidade das closures é fazer chamadas assíncronas.
Pense, por exemplo, em um aplicativo que precisa se conectar com a internet para mostrar informações numa tela. Nesse caso, podem haver os seguintes cenários:
- Sucesso: o app consegue se conectar com a internet e carrega a tela;
- Sucesso com lentidão: por conta de buscar as informações no mesmo processo que a tela, o app trava até receber a resposta;
- Erro 1: o app demora demais para carregar as informações por conta de uma conexão lenta com a internet;
- Erro 2: o app não consegue carregar as informações se a conexão com a internet cair. Neste caso, a tela fica travada - o que é ruim para a experiência do usuário.
Para evitar esses erros, o recurso async/await (chamada assíncrona) foi introduzido no Swift, e simplifica a execução de operações assíncronas. Ao utilizar async, uma função assíncrona pode ser declarada, permitindo que seu fluxo seja pausado (em caso de erro) até que uma operação seja concluída.
As closures, juntamente com o async/await, desempenham um papel crucial ao permitir que o código seja estruturado de maneira legível e eficiente, evitando o “callback hell” (o que pode acontecer em outras linguagens e gerar erros e inconsistências no código que travam o app) e tornando o tratamento de tarefas assíncronas mais intuitivo.
Veja um exemplo de código que faz uma chamada para mostrar dados de previsão do tempo (e que utiliza ambas as técnicas juntas):
func fetchWeather(completion: @escaping (Result<String, Error>) -> Void) {
// Aqui Vem diversas implementações de closures seguidas de códigos asincronos
Task {
do {
let url = URL(string: "https://api.example.com/weather")!
let (data, _) = try await URLSession.shared.data(from: url)
if let weatherString = String(data: data, encoding: .utf8) {
completion(.success(weatherString))
} else {
throw NSError(domain: "ParsingError", code: 0, userInfo: nil)
}
} catch {
completion(.failure(error))
}
}
}
// Aqui é usado as Closures com captura de valores e Trailing
fetchWeather { result in
switch result {
case .success(let weather):
print("Weather: \(weather)")
case .failure(let error):
print("Error: \(error)")
}
}
No exemplo acima, a função fetchWeather
usa async
e await
para chamar a API de previsão do tempo, aguardando o resultado. A URL é criada e uma solicitação de rede é feita com URLSession
, usando await
para esperar o término. O resultado é tratado na closure de conclusão, exibindo dados ou um erro: se a chamada for bem sucedida, o app mostrará as informações na tela; se falhar, faz um print do erro.
É essencial que todo o código esteja dentro um contexto assíncrono, como uma função Task
, e manter o loop de execução ativo (RunLoop.main.run()
) para tarefas assíncronas.
Neste momento, você não precisa se aprofundar nos detalhes como a API, URLSession
e Task
. Caso queira aprender mais sobre conexão com API, requisições e operações assíncronas, você pode estudar com a gente neste curso.
De início, as closures podem parecer complexas, mas, conforme você as pratica, essas técnicas ficam mais intuitivas de entender e utilizar.
Fez sentido?
Conclusão
As closures são uma parte fundamental da linguagem Swift, permitindo a criação de código flexível, conciso e funcional. Elas oferecem a capacidade de definir blocos de código que podem ser usados e reutilizados de maneira versátil em diferentes partes do código.
Com a habilidade de capturar valores do contexto circundante, as closures proporcionam uma maneira elegante de lidar com operações assíncronas, tarefas de background e muito mais. Através do uso eficaz de closures, as pessoas desenvolvedores Swift podem criar código mais legível, modular e reutilizável, aproveitando ao máximo a riqueza da linguagem e suas características funcionais.
Quer aprender a criar aplicativos incríveis com Swift? Venha estudar com a gente na formação de SwiftUI da Alura!
Até a próxima!