Skip to content

A simple approach to learning the beginnings of Redux in Swift.

License

Notifications You must be signed in to change notification settings

EvandroViva/ReduxPaperSwift

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ReduxPaperSwift

Uma simples abordagem para aprender os primórdios de Redux em Swift.

Introdução

Redux é a implementação de um padrão arquitetural de software que prioriza o fluxo de dados unidirecional. Foi criada a partir da arquitetura Flux (desenvolvida pelo Facebook), vem crescendo bastante no desenvolvimento de aplicações e promete grandes vantagens na sua utilização. Ela é uma alternativa a outros padrões arquiteturais como, por exemplo: MVC, MVVM, Viper e CleanSwift.

Vantagens

Uma das grandes promessas do Redux é criar restrições que incentivam um desenvolvimento de software mais organizado e mais fácil de testar, assim, por esses motivos, acaba diminuindo a complexidade na fase de desenvolvimento além de oferecer facilidades na manutenção do estado da aplicação e depuração avançada.

Esse artigo descreve uma abordagem simples para começar a entender este novo padrão.

Requisitos para a implementação

  • Nível básico na construção de aplicações em iOS (Swift + Xcode).
  • Conhecimento do padrão de projeto Observer.
  • Saber utilizar o sistema de dependencias CocoaPods.

Componentes

  • State: Representa o estado da aplicação. Deve existir apenas um, podendo este ser dividido em sub-estados.
  • Actions: São objetos simples que descrevem o que o sistema pode fazer. Esses objetos podem carregar informações ou não, dependendo do caso. Eles são despachados pela camada View como intenções de alterar o estado da aplicação.
  • Reducers: É aqui que desenvolvemos a lógica principal da aplicação. Reducers devem ser funções puras, sem efeitos colaterais e devem ser síncronos. São os únicos objetos que podem criar um novo estado para a aplicação. Eles recebem uma ação e o estado atual, e retornam um novo estado.

imagem do fluxo

Vejam que o fluxo unidirecional acontece quando a View despacha uma Action. Essa Action é passada para o Reducer correspondente, então este Reducer gera um novo State de acordo com a Action passada, e o State é passado de volta para a View para que esta seja alterada.

  • Store: É um dos componentes mais importantes dessa implementação. É ela que agrega todos os componentes citados acima e faz o fluxo funcionar. A View despacha uma nova Action para a Store. A Store então, passa essa Action para o Reducer junto com o State atual e então recebe de volta o novo State do Reducer. A View é avisada sempre que um novo State é criado, isso é possível pela implementação do padrão de projeto Observer que permite que a View vire "assinante" da Store, para ser notificada.

Vamos começar

Minha abordagem para começarmos a aprender Redux é construir uma aplicação de exemplo - um jogo de "Pedra, Papel e Tesoura" - utilizando uma biblioteca chamada ReSwift que implementa os conceitos dessa arquitetura em Swift.

Começamos então fazendo um esboço de como deve ser a aplicação. Para simplificar, a aplicação deverá funcionar em um único ViewController, contendo 3 botões na parte inferior (Pedra, Papel e Tesoura), 1 campo de mensagem na parte superior e 2 placeholders para mostrar quando um jogador já realizou sua jogada e no final para revelar a arma dos jogadores.

imagem da view

Para começar o desenvolvimento propus um caso de uso em que o Jogador1 escolhe Tesousa e o Jogador2 escolhe Pedra, resultando na vitória do Jogador2. Esse fluxo aconteceria da seguinte forma:

imagem da view

Desenvolvimento

Criamos um novo projeto no Xcode do tipo "Single view application" e habilitamos "Include Unit Tests" para podermos fazer um teste usando os conceitos de Redux.

Instale o pod "ReSwift", utilizando CocoaPods.

Em seguida vamos criar o primeiro componente, o State. Observando as imagens acima, conseguimos perceber claramente as partes do app que irão se alterar durante a execução, cada parte desta consiste no estado da aplicação. Criei então um arquivo State.swift e dentro dele coloquei as estruturas que formam o estado, juntamente com possíveis estruturas de modelo que formam o conceito da aplicação. É importante salientar que as estruturas devem ser imutáveis para que o Redux funcione, só assim garantimos que o State seja alterado apenas pelos Reducers, por isso utilizei Structs e Enums ao invés de Classes:

import ReSwift

// MARK:- STATE

struct AppState: StateType {
    
    var message: Message
    var turn: Turn
    var player1Play: Play
    var player2Play: Play
    var result: Result?
    
    init() {
        
        self.message = .player1choose
        self.turn = Turn(player: .one)
        self.player1Play = Play(chosen: false, weapon: nil)
        self.player2Play = Play(chosen: false, weapon: nil)
    }
}


// MARK:- MODEL & OPTIONS

enum Message: String {
    
    case player1choose = "PLAYER 1 - Choose your weapon:"
    case player2choose = "PLAYER 2 - Choose your weapon:"
    case player1wins = "PLAYER 1 WINS!"
    case player2wins = "PLAYER 2 WINS!"
    case draw = "DRAW!"
}

struct Turn {
    
    var player: Player
}

enum Player {
    
    case one
    case two
}

struct Play {
    
    var chosen: Bool
    var weapon: Weapon?
}

enum Weapon: String {
    
    case rock = "Rock"
    case paper = "Paper"
    case scissors = "Scissors"
}

enum Result {
    
    case draw
    case player1wins
    case player2wins
}

Agora vamos criar uma Action, que será a descrição de uma ação que tem intenção de alterar o State. Neste caso temos apenas uma, ChooseWeaponAction, que é disparada quando cada jogador escolhe uma arma:

import ReSwift

// MARK:- ACTIONS

struct ChooseWeaponAction: Action {
    
    var weapon: Weapon
}

Por último vamos construir o Reducer, aqui nós filtramos a Action criada, pegamos o State atual da aplicação e geramos um novo State baseado na lógica que desenvolveremos com as informações contidas na Action:

import ReSwift

// MARK:- REDUCERS

func appReducer(action: Action, state: AppState?) -> AppState {
    
    // creates a new state if one does not already exist
    var state = state ?? AppState()
    
    switch action {
    case let chooseWeaponAction as ChooseWeaponAction:
        
        let turn = state.turn
        switch turn.player {
        case .one:
            
            // create a play
            let play = Play(chosen: true, weapon: chooseWeaponAction.weapon)
            state.player1Play = play
            
            // pass the turn to the next player
            state.turn = Turn(player: .two)
            
            // change the message
            state.message = .player2choose
            
        case .two:
            
            // create a play
            let play = Play(chosen: true, weapon: chooseWeaponAction.weapon)
            state.player2Play = play
            
            // calculate who won
            let player1weapon = state.player1Play.weapon ?? .rock
            let player2weapon = state.player2Play.weapon ?? .rock
            
            switch player1weapon {
            case .rock:
                switch player2weapon {
                case .rock:
                    state.result = .draw
                    state.message = .draw
                case .paper:
                    state.result = .player2wins
                    state.message = .player2wins
                case .scissors:
                    state.result = .player1wins
                    state.message = .player1wins
                }
            case .paper:
                switch player2weapon {
                case .rock:
                    state.result = .player1wins
                    state.message = .player1wins
                case .paper:
                    state.result = .draw
                    state.message = .draw
                case .scissors:
                    state.result = .player2wins
                    state.message = .player2wins
                }
            case .scissors:
                switch player2weapon {
                case .rock:
                    state.result = .player2wins
                    state.message = .player2wins
                case .paper:
                    state.result = .player1wins
                    state.message = .player1wins
                case .scissors:
                    state.result = .draw
                    state.message = .draw
                }
            }    
        }
        
    default:
        break
    }
    
    // return the new state
    return state
}

Teste

Pronto, simples assim, nós implementamos um padrão Redux com fluxo unidirecional. Para mostrar a facilidade de testar esse tipo de arquitetura, construi esta classe de XCTest que testa lógicas da aplicação sem mesmo termos construído a UI (View).

import XCTest
import ReSwift
@testable import ReduxPaperSwift

class ReduxPaperSwiftTests: XCTestCase {
    
    // testing whether a rule works.
    func test1() {
        
        let store = Store<AppState>(reducer: appReducer, state: nil)
        
        // Player 1 choose
        store.dispatch(ChooseWeaponAction(weapon: .rock))
        
        // Player 2 choose
        store.dispatch(ChooseWeaponAction(weapon: .scissors))
        
        // Check result
        XCTAssertEqual(store.state.result, .player1wins)
    }
    
    // testing whether another rule works.
    func test2() {
        
        let store = Store<AppState>(reducer: appReducer, state: nil)
        
        // Player 1 choose
        store.dispatch(ChooseWeaponAction(weapon: .rock))
        
        // Player 2 choose
        store.dispatch(ChooseWeaponAction(weapon: .paper))
        
        // Check result
        XCTAssertEqual(store.state.result, .player2wins)
    }
    
}

Finalizando

Para finalizar, criei um ViewController com as características mostradas do desenho de esboço, e fiz esse ViewController se tornar um "assinante" da Store, podendo assim executar uma mudança nas views sempre que o State mudar. Isso acontece com a implementação do protocolo StoreSubscriber:

import UIKit
import ReSwift

class ViewController: UIViewController, StoreSubscriber {

    @IBOutlet weak var message: UILabel!
    @IBOutlet weak var placeholder1: UILabel!
    @IBOutlet weak var placeholder2: UILabel!
    
    @IBAction func rockButton(_ sender: Any) {
        store.dispatch(ChooseWeaponAction(weapon: .rock))
    }
    
    @IBAction func paperButton(_ sender: Any) {
        store.dispatch(ChooseWeaponAction(weapon: .paper))
    }
    
    @IBAction func scissorsButton(_ sender: Any) {
        store.dispatch(ChooseWeaponAction(weapon: .scissors))
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        store.subscribe(self)
    }
    
    func newState(state: AppState) {
        
        message.text = state.message.rawValue
        
        if state.player2Play.chosen {
            placeholder1.text = state.player1Play.weapon?.rawValue
            placeholder2.text = state.player2Play.weapon?.rawValue
        } else {
            placeholder1.text = state.player1Play.chosen ? "chosen" : ""
        }
    }
}

Créditos

Para se aprofundar no assunto

About

A simple approach to learning the beginnings of Redux in Swift.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 64.9%
  • Shell 31.8%
  • Objective-C 2.6%
  • Ruby 0.7%