Interaction de SwiftUI avec Redux

image

Bonjour à tous. Dans cet article, nous parlerons du cadre SwiftUI en collaboration avec Redux, ce bundle nous permet de créer rapidement et facilement des applications. SwiftUI est utilisé pour créer une interface utilisateur de style déclaratif, contrairement à UIKit . Redux, à son tour, sert à contrôler l'état de l'application.

L'État est un concept fondamental dans SwiftUI et Redux. Dans notre cas, ce n'est pas seulement un mot à la mode, mais aussi une entité qui les relie et leur permet de très bien travailler ensemble. Dans cet article, je vais essayer de montrer que la thèse ci-dessus est vraie, alors commençons!

Avant de plonger dans l'écriture de code, comprenons d'abord ce qu'est Redux et en quoi il consiste.

Redux est une bibliothèque open source pour gérer l'état d'une application. Le plus souvent utilisé en conjonction avec React ou Angular pour développer le côté client. Contient un certain nombre d'outils pour simplifier considérablement le transfert des données de stockage à travers le contexte. Créateurs: Daniil Abramov et Andrew Clark.

Pour moi, Redux n'est pas seulement une bibliothèque, c'est déjà quelque chose de plus, je l'attribue aux décisions architecturales sur lesquelles se base l'application. Principalement en raison de son flux de données unidirectionnel.

Flux multidirectionnel ou unidirectionnel


Pour expliquer ce que j'entends par flux de données, je vais donner l'exemple suivant. Une application créée à l'aide de VIPER prend en charge un flux de données multidirectionnel entre les modules:

image

Redux, à son tour, est un flux de données unidirectionnel et est plus facile à expliquer en fonction de ses composants.

image

Parlons un peu en détail de chaque composant Redux.

L'État est la seule source de vérité qui contient toutes les informations nécessaires à notre application.

L'action est l'intention de changer d'état. Dans notre cas, il s'agit d'une énumération qui contient de nouvelles informations que nous voulons ajouter ou modifier dans l'état actuel.

RéducteurEst une fonction qui prend Action et l'état actuel comme paramètres et renvoie un nouvel état. C'est le seul moyen de le créer. Il convient également de noter que cette fonctionnalité doit être propre.

Store est un objet qui contient State et fournit tous les outils nécessaires pour le mettre à jour.

Je crois que la théorie est suffisante, passons à la pratique.

Implémentation de Redux


L'une des façons les plus simples d'apprendre à connaître un outil est de commencer à l'utiliser, comme l'a dit mon professeur de programmation, si vous voulez apprendre un langage de programmation, écrivez une application dessus. Créons donc une petite application, que ce soit un simple journal d'entraînement, il n'aura que quatre options, la première consiste à afficher une liste des entraînements, la seconde consiste à ajouter une séance d'entraînement terminée, la troisième à supprimer et la quatrième à trier les séances d'entraînement. Application assez simple, mais en même temps nous permettra de nous familiariser avec Redux et SwiftUI.

Créez un projet propre dans Xcode, donnez-lui le nom WorkoutsDiary et, surtout, sélectionnez SwiftUI pour l'interface utilisateur, car nous utiliserons SwiftUI pour créer notre interface utilisateur.

Après avoir créé le projet. Créez une structure d'entraînement qui sera responsable de l'entraînement que nous avons terminé.

import Foundation

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

Comme vous pouvez le voir, cette structure n'a rien de fou, le champ id est requis pour se conformer au protocole identifiable, et le champ de complexité est juste une énumération avec la définition suivante:

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

Maintenant que nous avons tout ce dont nous avons besoin pour commencer à implémenter Redux, commençons par créer un état.

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

State est une structure simple qui contient deux champs: workouts et sortType . Le premier est une liste d'entraînements et le second est un champ facultatif qui détermine comment la liste est triée.

SortType est une énumération définie comme suit:

enum SortType {
    case distance
    case complexity
}

Par souci de simplicité, nous allons trier par distance et difficulté en ordre décroissant, c'est-à-dire que plus la complexité de notre entraînement est élevée, plus elle sera affichée dans notre liste. Il convient de noter que sortType est un type facultatif et qu'il peut être nul, ce qui signifie que la liste n'est pas triée pour le moment.

Nous poursuivrons la mise en œuvre de nos composants. Créons une action

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

Comme nous pouvons le voir, l' action est une énumération avec trois cas qui nous donnent la capacité de manipuler notre État .

  • addWorkout (_ workout: Workout) ajoute simplement un entraînement qui est passé en paramètre.
  • removeWorkout (at: IndexSet) supprime l'élément à l'index spécifié.
  • sort (by: SortType) trie la liste de formation selon le type de tri spécifié.

Créons l'un des composants les plus complexes, voici 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
}


La fonction que nous avons écrite est assez simple et fonctionne comme suit:

  1. Copie l' état actuel pour travailler avec lui.
  2. Sur la base de l' action , nous mettons à jour notre état copié .
  3. Renvoyer l' état mis à jour

Il convient de noter que la fonction ci-dessus est une fonction pure, et c'est ce que nous voulions réaliser! Une fonction doit remplir deux conditions pour être considérée comme «pure»:

  • À chaque fois, la fonction renvoie le même résultat lorsqu'elle est appelée avec le même ensemble de données.
  • Il n'y a aucun effet secondaire.

Le dernier élément Redux manquant est le Store , nous allons donc l'implémenter pour notre application.

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)
     }
 }

Dans les implémentations de l'objet Store , nous utilisons tous les avantages du protocole ObservableObject , ce qui nous permet d'exclure l'écriture de beaucoup de code de modèle ou l'utilisation de frameworks tiers. La propriété State est en lecture seule et utilise l'encapsuleur de la propriété @Published , ce qui signifie que chaque fois qu'elle est modifiée, SwiftUI recevra des notifications. La méthode init prend un état initial en tant que paramètre avec une valeur par défaut donnée sous la forme d'un tableau vide d'éléments d'entraînement. La fonction de répartition est le seul moyen de mettre à jour l'état: elle remplace l'état actuel par le nouveau créé par la fonction de réduction , basé surAction , qui est passée en paramètre.

Maintenant que nous avons implémenté tous les composants de Redux, nous pouvons commencer à créer une interface utilisateur pour notre application.

Implémentation d'application


L'interface utilisateur de notre application sera assez simple. Et il sera composé de deux petits écrans. Le premier et principal écran est un écran qui affichera une liste des séances d'entraînement. Le deuxième écran est l'écran d'ajout d'entraînement. De plus, chaque élément sera affiché dans une certaine couleur, la couleur reflétera la complexité de l'entraînement. Les globules rouges indiquent la difficulté la plus élevée de l'entraînement, l'orange est responsable de la difficulté moyenne et le vert est responsable de l'entraînement le plus facile.

Nous allons implémenter l'interface en utilisant un nouveau framework d'Apple appelé SwiftUI. SwiftUI vient remplacer notre UIKit familier. SwiftUI est fondamentalement différent de UIKit, principalement en ce qu'il s'agit d'une approche déclarative pour écrire des éléments d'interface utilisateur avec du code. Dans cet article, je ne vais pas me plonger dans toutes les subtilités de SwiftUI et je suppose que vous avez déjà de l'expérience avec SwiftUI. Si vous ne connaissez pas SwiftUI, je vous conseille de faire attention à la documentation d'Apple, à savoir, regardez leurs plusieurs tutoriels complets avec ajout étape par étape et affichage interactif du résultat affiché. Il existe également des liens vers des exemples de projets. Ces tutoriels vous permettront de plonger rapidement dans le monde déclaratif de SwiftUI.

Il convient également de noter que SwiftUI n'est pas encore prêt pour les projets de production, il est trop jeune et plus d'un an s'écoulera avant de pouvoir être utilisé de cette manière. N'oubliez pas non plus qu'il ne prend en charge que les versions iOS 13.0+. Mais il convient également de noter que SwiftUI fonctionnera sur toutes les plates-formes Apple, ce qui est un gros avantage sur UIKit!

Commençons l'implémentation à partir de l'écran principal de notre application. Accédez au fichier ContentView.swift changez le code actuel en ceci.

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)
        }
    }
}

La vue de contenu est une vue standard dans SwiftUI. La partie la plus importante - de mon point de vue - est la ligne de code qui contient la variable store. Nous allons créer @EnvironmentObject. Cela nous permettra d'utiliser les données de la boutique où cela est nécessaire, et en outre, cela mettra automatiquement à jour nos vues si les données sont modifiées. C'est quelque chose comme Singleton pour notre magasin.

@EnvironmentObject var store: Store

Il convient également de noter la ligne de code suivante:

@State private var isAddingMode: Bool = false

EtatEst un wrapper que nous pouvons utiliser pour indiquer l'état d'une vue. SwiftUI le stockera dans une mémoire interne spéciale en dehors de la structure View. Seule une vue liée peut y accéder. Une fois la valeur de la propriétéEtat modifications, SwiftUI reconstruit la vue pour tenir compte des changements d'état.

image

Ensuite, nous irons dans le fichier SceneDelegate.swift et ajouterons le code à la méthode:

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()
        }
    }

De la même manière, n'importe quel @EnvironmentObject peut être transmis à n'importe quelle représentation enfant dans l'application entière, et tout cela est possible grâce à l'environnement. La variable IsAddingMode est marquéeEtatet indique si la vue secondaire est affichée ou non. La variable de magasin est automatiquement héritée par WorkoutListView , et nous n'avons pas besoin de la transmettre explicitement, mais nous devons le faire pour AddWorkoutView , car elle est présentée sous la forme d'une feuille qui n'est pas un enfant de ContentView .

Créez maintenant un WorkoutListView qui héritera de View. Créez un nouveau fichier rapide appelé 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, qui utilise l'élément conteneur List pour afficher une liste des entraînements. La fonction onDelete est utilisée pour supprimer un entraînement et utilise l' action removeWorkout , qui est effectuée à l'aide de la fonction de répartition fournie par le magasin . Pour afficher l'entraînement dans la liste, WorkoutView est utilisé.

Créez un autre fichier WorkoutView.swift qui sera chargé d'afficher notre article dans la liste.

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)
    }
}

Cette vue prend l'objet d'apprentissage comme paramètre et est configurée en fonction de ses propriétés.

Pour ajouter un nouvel élément à la liste, vous devez changer le paramètre isAddingMode sur true pour afficher AddWorkoutView . Cette responsabilité incombe à AddButton .

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

AddButton vaut également la peine d'être placé dans un fichier séparé.

Cette vue est un simple bouton qui a été extrait de ContentView principal pour une meilleure structure et une meilleure séparation du code.

Créez une vue pour ajouter un nouvel entraînement. Créez un nouveau fichier 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)
            )
        }
    }
}

Il s'agit d'un contrôleur assez volumineux qui, comme les autres contrôleurs, contient la variable store. Il contient également les variables nomTexte, distanceTexte, complexitéField et isAddingMode . Les trois premières variables sont nécessaires pour lier TextField, Picker, DatePicker , qui peut être vu sur cet écran. La barre de navigation comporte deux éléments. Le premier bouton est un bouton qui ferme l'écran sans ajouter de nouvel entraînement, et le dernier ajoute un nouvel entraînement à la liste, ce qui est réalisé en envoyant l'action addWorkout. Cette action ferme également l'écran d'ajout d'un nouvel entraînement.

image

Le dernier mais non le moindre est 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()
        }
    }
}

Cette vue se compose de deux boutons qui sont chargés de trier la liste d'entraînement et d'activer ou de désactiver le mode d'édition de notre liste d'entraînement. Les actions de tri sont appelées à l'aide de la fonction de répartition, que nous pouvons appeler via la propriété store.

image

Résultat


L'application est prête et devrait fonctionner exactement comme prévu. Essayons de le compiler et de l'exécuter.

résultats


Redux et SwiftUI fonctionnent très bien ensemble. Le code écrit à l'aide de ces outils est facile à comprendre et peut être bien organisé. Un autre bon aspect de cette solution est son excellente testabilité du code. Cependant, cette solution n'est pas sans inconvénients, l'un d'eux est une grande quantité de mémoire utilisée par l'application lorsque l'état de l'application est très complexe, et les performances de l'application peuvent ne pas être idéales dans certains scénarios spécifiques, car toutes les vues dans SwiftUI sont mises à jour lors de la création d'un nouvel état. Ces lacunes peuvent avoir un impact important sur la qualité de l'application et l'interaction avec l'utilisateur, mais si nous nous en souvenons et préparons l'état de manière raisonnable, l'impact négatif peut être facilement minimisé, voire évité.

Vous pouvez télécharger ou afficher le code source du projet.ici .

J'espère que cet article vous a plu et que vous avez appris quelque chose de nouveau par vous-même, à bientôt. De plus, ce sera encore plus intéressant.

All Articles