Interação SwiftUI com Redux

imagem

Olá a todos. Neste artigo, falaremos sobre a estrutura SwiftUI em conjunto com o Redux, este pacote nos permite criar aplicativos de maneira rápida e fácil. O SwiftUI é usado para criar uma interface de usuário de estilo declarativo, diferente do UIKit . O Redux, por sua vez, serve para controlar o estado do aplicativo.

Estado é um conceito fundamental no SwiftUI e Redux. No nosso caso, isso não é apenas uma palavra da moda, mas também uma entidade que os conecta e permite que trabalhem muito bem juntos. Neste artigo, tentarei mostrar que a tese acima é verdadeira, então vamos começar!

Antes de começarmos a escrever código, vamos primeiro entender o que é Redux e em que consiste.

Redux é uma biblioteca de código aberto para gerenciar o estado de um aplicativo. Geralmente usado em conjunto com React ou Angular para desenvolver o lado do cliente. Contém várias ferramentas para simplificar significativamente a transferência de dados de armazenamento através do contexto. Criadores: Daniil Abramov e Andrew Clark.

Para mim, o Redux não é apenas uma biblioteca, já é algo mais, eu atribuo isso às decisões de arquitetura nas quais o aplicativo se baseia. Principalmente devido ao seu fluxo de dados unidirecional.

Fluxo multidirecional ou unidirecional


Para explicar o que quero dizer com fluxo de dados, darei o exemplo a seguir. Um aplicativo criado usando o VIPER suporta um fluxo de dados multidirecional entre módulos: o

imagem

Redux, por sua vez, é um fluxo de dados unidirecional e é mais fácil de explicar com base em seus componentes constituintes.

imagem

Vamos falar um pouco em detalhes sobre cada componente do Redux.

Estado é a única fonte de verdade que contém todas as informações necessárias para nossa aplicação.

Ação é a intenção de mudar de estado. No nosso caso, essa é uma enumeração que contém novas informações que queremos adicionar ou alterar no estado atual.

RedutorÉ uma função que toma Ação e Estado atual como parâmetros e retorna um novo Estado. Esta é a única maneira de criá-lo. Também é importante notar que esse recurso deve estar limpo.

Store é um objeto que contém State e fornece todas as ferramentas necessárias para atualizá-lo.

Eu acredito que a teoria é suficiente, vamos seguir praticando.

Implementação de Redux


Uma das maneiras mais fáceis de conhecer uma ferramenta é começar a usá-la, como disse meu professor de programação, se você quiser aprender uma linguagem de programação, escreva uma aplicação nela. Então, vamos criar um pequeno aplicativo, seja um diário de treinamento simples, ele terá apenas quatro opções, a primeira é exibir uma lista de exercícios, a segunda é adicionar um treino completo, a terceira é excluir e a quarta é classificar os exercícios. Aplicação bastante simples, mas ao mesmo tempo nos permitirá conhecer o Redux e o SwiftUI.

Crie um projeto limpo no Xcode, dê o nome de WorkoutsDiary e, mais importante, selecione SwiftUI para a interface do usuário, pois usaremos o SwiftUI para criar nossa interface do usuário.

Depois de criar o projeto. Crie uma estrutura de treino que será responsável pelo treino que concluímos.

import Foundation

struct Workout: Identifiable {
    let id: UUID = .init()
    let name: String
    let distance: String
    let date: Date
    let complexity: Complexity
}

Como você pode ver, não há nada louco nessa estrutura, o campo id é necessário para estar em conformidade com o protocolo Identificável e o campo complexidade é apenas uma enumeração com a seguinte definição:

enum Complexity: Int {
    case low
    case medium
    case high
}

Agora que temos tudo o que precisamos para começar a implementar o Redux, vamos começar criando um Estado.

struct AppState {
    var workouts: [Workout]
    var sortType: SortType?
}

State é uma estrutura simples que contém dois campos: exercícios e sortType . O primeiro é uma lista de exercícios e o segundo é um campo opcional que determina como a lista é classificada.

SortType é uma enumeração definida da seguinte maneira:

enum SortType {
    case distance
    case complexity
}

Por simplicidade, classificaremos por distância e dificuldade em ordem decrescente, ou seja, quanto maior a complexidade do treinamento, maior será exibida em nossa lista. Vale ressaltar que sortType é um tipo opcional e pode ser nulo, o que significa que a lista não está classificada no momento.

Continuaremos a implementação de nossos componentes. Vamos criar uma ação

enum Action {
    case addWorkout(_ workout: Workout)
    case removeWorkout(at: IndexSet)
    case sort(by: SortType)
}

Como podemos ver, Ação é uma enumeração com três casos que nos dão a capacidade de manipular nosso Estado .

  • addWorkout (_ treino: treino) simplesmente adiciona um treino que é passado como parâmetro.
  • removeWorkout (em: IndexSet) remove o item no índice especificado.
  • sort (por: SortType) classifica a lista de treinamento pelo tipo de classificação especificado.

Vamos criar um dos componentes mais complexos, o Reducer :

func reducer(state: AppState, action: Action) -> AppState {
    var state = state
    
    switch action {
    case .addWorkout(let workout):
        state.workouts.append(workout)
        
    case .removeWorkout(let indexSet):
        state.workouts.remove(atOffsets: indexSet)
    
    case .sort(let type):
        switch type {
        case .distance:
            state.workouts.sort { $0.distance > $1.distance }
            state.sortType = .distance
        case .complexity:
            state.workouts.sort { $0.complexity.rawValue > $1.complexity.rawValue }
            state.sortType = .complexity
        }
    }
    return state
}


A função que escrevemos é bastante simples e funciona da seguinte maneira:

  1. Copia o estado atual para trabalhar com ele.
  2. Com base na ação , atualizamos nosso estado copiado .
  3. Retornar o estado atualizado

Vale a pena notar que a função acima é uma função pura, e é isso que queríamos alcançar! Uma função deve atender a duas condições para ser considerada "pura":

  • Cada vez, a função retorna o mesmo resultado quando é chamada com o mesmo conjunto de dados.
  • Não tem efeitos colaterais.

O último elemento Redux ausente é a Store , então vamos implementá-lo para nosso aplicativo.

final class Store: ObservableObject {
     @Published private(set) var state: AppState
     
     init(state: AppState = .init(workouts: [Workout]())) {
         self.state = state
     }
     
     public func dispatch(action: Action) {
         state = reducer(state: state, action: action)
     }
 }

Nas implementações do objeto Store , usamos todas as vantagens do protocolo ObservableObject , o que nos permite excluir a gravação de uma grande quantidade de código de modelo ou o uso de estruturas de terceiros. A propriedade State é somente leitura e usa o wrapper da propriedade @Published , o que significa que sempre que é alterado, o SwiftUI recebe notificações. O método init assume um estado inicial como um parâmetro com um determinado valor padrão na forma de uma matriz vazia de elementos do Workout. A função de despacho é a única maneira de atualizar o estado: ela substitui o estado atual pelo novo criado pela função redutora , com base emAção , que é passada como um parâmetro.

Agora que implementamos todos os componentes do Redux, podemos começar a criar uma interface de usuário para nosso aplicativo.

Implementação de aplicativo


A interface do usuário do nosso aplicativo será bastante simples. E será composto por duas telas pequenas. A primeira e principal tela é uma tela que exibirá uma lista de exercícios. A segunda tela é a tela de adição de treino. Além disso, cada elemento será exibido em uma determinada cor, a cor refletirá a complexidade do treino. Os glóbulos vermelhos indicam a maior dificuldade do treino, a laranja é responsável pela dificuldade média e o verde é o responsável pelo treino mais fácil.

Implementaremos a interface usando uma nova estrutura da Apple chamada SwiftUI. O SwiftUI substitui nosso familiar UIKit. O SwiftUI é fundamentalmente diferente do UIKit, principalmente por ser uma abordagem declarativa para escrever elementos da interface do usuário com código. Neste artigo, não vou me aprofundar em todos os meandros do SwiftUI e presumo que você já tenha experiência com o SwiftUI. Se você não tem conhecimento do SwiftUI, aconselho a prestar atenção à documentação da Apple, ou seja, veja os vários tutoriais completos com adição passo a passo e exibição interativa do resultado em exibição. Há também links para exemplos de projetos. Esses tutoriais permitirão que você mergulhe rapidamente no mundo declarativo do SwiftUI.

Também é importante notar que o SwiftUI ainda não está pronto para projetos de produção, é muito jovem e mais de um ano se passará antes que possa ser usado dessa maneira. Além disso, não esqueça que ele suporta apenas as versões do iOS 13.0+. Mas também vale a pena notar que o SwiftUI funcionará em todas as plataformas da Apple, o que é uma grande vantagem sobre o UIKit!

Vamos começar a implementação na tela principal do nosso aplicativo. Vá para o arquivo ContentView.swift, altere o código atual para isso.

struct ContentView: View {
    @EnvironmentObject var store: Store
    @State private var isAddingMode: Bool = false
    
    var body: some View {
        NavigationView {
            WorkoutListView()
                .navigationBarTitle("Workouts diary", displayMode: .inline)
                .navigationBarItems(
                    leading: AddButton(isAddingMode: self.$isAddingMode),
                    trailing: TrailingView()
                )
        }
        .sheet(isPresented: $isAddingMode) {
            AddWorkoutView(isAddingMode: self.$isAddingMode)
                .environmentObject(self.store)
        }
    }
}

A Visualização de Conteúdo é uma visualização padrão no SwiftUI. A parte mais importante - do meu ponto de vista - é a linha de código que contém a variável store. Criaremos @EnvironmentObject. Isso nos permitirá usar os dados da loja sempre que necessário e, além disso, atualizará automaticamente nossas visualizações se os dados forem alterados. Isso é algo como Singleton para nossa loja.

@EnvironmentObject var store: Store

Também vale a pena observar a seguinte linha de código:

@State private var isAddingMode: Bool = false

EstadoÉ um invólucro que podemos usar para indicar o estado de uma Visualização. O SwiftUI o armazenará em uma memória interna especial fora da estrutura do View. Somente uma Visualização vinculada pode acessá-la. Uma vez que o valor da propriedadeEstado alterações, o SwiftUI recria a tela para levar em conta as alterações de estado.

imagem

Em seguida, iremos ao arquivo SceneDelegate.swift e adicionaremos o código ao método:

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        let contentView = ContentView().environmentObject(Store())
        
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Da mesma maneira, qualquer @EnvironmentObject pode ser passado para qualquer representação filho em todo o aplicativo, e tudo isso é possível graças ao Environment. A variável IsAddingMode está marcadaEstadoe indica se a visualização secundária é exibida ou não. A variável store é herdada automaticamente pelo WorkoutListView , e não precisamos transmiti-la explicitamente, mas precisamos fazer isso para AddWorkoutView , porque é apresentada na forma de uma planilha que não é filha do ContentView .

Agora crie um WorkoutListView que herdará da View. Crie um novo arquivo rápido chamado WorkoutListView .

struct WorkoutListView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        List {
            ForEach(store.state.workouts) {
                WorkoutView(workout: $0)
            }
            .onDelete {
                self.store.dispatch(action: .removeWorkout(at: $0))
            }
            .listRowInsets(EdgeInsets())
        }
    }
}

View, que usa o elemento List do contêiner para exibir uma lista de exercícios. A função onDelete é usada para excluir um treino e usa a ação removeWorkout , que é executada usando a função de despacho fornecida pela loja . Para exibir o treino na lista, o WorkoutView é usado.

Crie outro arquivo WorkoutView.swift, que será responsável por exibir nosso item na lista.

struct WorkoutView: View {
    let workout: Workout
    
    private var backgroundColor: Color {
        switch workout.complexity {
        case .low:
            return .green
        case .medium:
            return .orange
        case .high:
            return .red
        }
    }
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(workout.name)
                Text("Distance:" + workout.distance + "km")
                    .font(.subheadline)
            }
            Spacer()
            VStack(alignment: .leading) {
                Text(simpleFormat(workout.date))
            }
        }
        .padding()
        .background(backgroundColor)
    }
}

private extension WorkoutView {
    func simpleFormat(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd MMM yyyy"
        dateFormatter.locale = .init(identifier: "en_GB")
        return dateFormatter.string(from: date)
    }
}

Essa visualização usa o objeto de treinamento como parâmetro e é configurada com base em suas propriedades.

Para adicionar um novo item à lista, você deve alterar o parâmetro isAddingMode para true para exibir AddWorkoutView . Essa responsabilidade é do AddButton .

struct AddButton: View {
    @Binding var isAddingMode: Bool
    
    var body: some View {
        Button(action: { self.isAddingMode = true }) {
            Image(systemName: "plus")
        }
    }
}

AddButton também vale a pena colocar em um arquivo separado.

Essa visualização é um botão simples que foi extraído do ContentView principal para melhor estrutura e separação de código.

Crie uma exibição para adicionar um novo treino. Crie um novo arquivo AddWorkoutView.swift :

struct AddWorkoutView: View {
    @EnvironmentObject private var store: Store
    
    @State private var nameText: String = ""
    @State private var distanceText: String = ""
    @State private var complexityField: Complexity = .medium
    @State private var dateField: Date = Date()
    @Binding var isAddingMode: Bool
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: $nameText)
                TextField("Distance", text: $distanceText)
                Picker(selection: $complexityField, label: Text("Complexity")) {
                    Text("Low").tag(Complexity.low)
                    Text("Medium").tag(Complexity.medium)
                    Text("High").tag(Complexity.high)
                }
                DatePicker(selection: $dateField, displayedComponents: .date) {
                    Text("Date")
                }
            }
            .navigationBarTitle("Workout Details", displayMode: .inline)
            .navigationBarItems(
                leading: Button(action: { self.isAddingMode = false }) {
                    Text("Cancel")
                },
                trailing: Button(action: {
                    let workout = Workout(
                        name: self.nameText,
                        distance: self.distanceText,
                        date: self.dateField,
                        complexity: self.complexityField
                    )
                    self.store.dispatch(action: .addWorkout(workout))
                    self.isAddingMode = false
                }) {
                    Text("Save")
                }
                .disabled(nameText.isEmpty)
            )
        }
    }
}

Este é um controlador bastante grande que, como outros controladores, contém a variável de armazenamento. Ele também contém as variáveis nameText, distanceText, complexField e isAddingMode . As três primeiras variáveis ​​são necessárias para vincular TextField, Picker, DatePicker , que podem ser vistas nesta tela. A barra de navegação possui dois elementos. O primeiro botão é um botão que fecha a tela sem adicionar um novo treino, e o último adiciona um novo treino à lista, o que é alcançado enviando a ação addWorkout. Essa ação também fecha a tela Adicionar novo treino.

imagem

Por último, mas não menos importante, é o TrailingView .

struct TrailingView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        HStack(alignment: .center, spacing: 30) {
            Button(action: {
                switch self.store.state.sortType {
                case .distance:
                    self.store.dispatch(action: .sort(by: .distance))
                default:
                    self.store.dispatch(action: .sort(by: .complexity))
                }
            }) {
                Image(systemName: "arrow.up.arrow.down")
            }
            EditButton()
        }
    }
}

Essa visualização consiste em dois botões responsáveis ​​pela classificação da lista de exercícios e pela ativação ou desativação do modo de edição da nossa lista de exercícios. As ações de classificação são chamadas usando a função de despacho, que podemos chamar através da propriedade store.

imagem

Resultado


O aplicativo está pronto e deve funcionar exatamente como o esperado. Vamos tentar compilar e executá-lo.

achados


Redux e SwiftUI funcionam muito bem juntos. O código escrito usando essas ferramentas é fácil de entender e pode ser bem organizado. Outro bom aspecto desta solução é sua excelente testabilidade de código. No entanto, esta solução não apresenta desvantagens, uma delas é uma grande quantidade de memória usada pelo aplicativo quando o estado do aplicativo é muito complexo e o desempenho do aplicativo pode não ser o ideal em alguns cenários específicos, pois todas as Exibições no SwiftUI são atualizadas ao criar um novo Estado. Essas deficiências podem ter um grande impacto na qualidade do aplicativo e na interação do usuário, mas se as lembrarmos e prepararmos o estado de maneira razoável, o impacto negativo poderá ser facilmente minimizado ou mesmo evitado.

Você pode baixar ou visualizar o código fonte do projeto.aqui .

Espero que você tenha gostado deste artigo e tenha aprendido algo novo por si mesmo. Até breve. Além disso, será ainda mais interessante.

All Articles