Développement d'applications sur SwiftUI. Partie 1 : Flux de données et Redux

Développement d'applications sur SwiftUI. Partie 1 : Flux de données et Redux

Après avoir assisté à la session sur l'état de l'Union à la WWDC 2019, j'ai décidé de me plonger en profondeur dans SwiftUI. J'ai passé beaucoup de temps à travailler avec et j'ai maintenant commencé à développer une véritable application qui peut être utile à un large éventail d'utilisateurs.

Je l'ai appelé MovieSwiftUI - il s'agit d'une application permettant de rechercher des films nouveaux et anciens, ainsi que de les collecter dans une collection en utilisant API TMDB. J'ai toujours aimé le cinéma et j'ai même créé une entreprise travaillant dans ce domaine, même s'il y a longtemps. L'entreprise pouvait difficilement être qualifiée de cool, mais l'application l'était !

Nous rappelons: pour tous les lecteurs de "Habr" - une remise de 10 000 roubles lors de l'inscription à n'importe quel cours Skillbox en utilisant le code promotionnel "Habr".

Skillbox vous recommande : Cours éducatif en ligne "Développeur Java Métier".

Alors, que peut faire MovieSwiftUI ?

  • Interagit avec l'API - presque toutes les applications modernes le font.
  • Charge les données asynchrones sur les requêtes et analyse JSON dans le modèle Swift à l'aide codable.
  • Affiche les images chargées sur demande et les met en cache.
  • Cette application pour iOS, iPadOS et macOS offre la meilleure UX aux utilisateurs de ces systèmes d'exploitation.
  • L'utilisateur peut générer des données et créer ses propres listes de films. L'application enregistre et restaure les données utilisateur.
  • Les vues, les composants et les modèles sont clairement séparés à l'aide du modèle Redux. Le flux de données ici est unidirectionnel. Il peut être entièrement mis en cache, restauré et écrasé.
  • L'application utilise les composants de base de SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. Il fournit également des vues personnalisées, des gestes et une UI/UX.

Développement d'applications sur SwiftUI. Partie 1 : Flux de données et Redux
En fait, l'animation est fluide, le GIF s'est avéré un peu saccadé

Travailler sur l'application m'a apporté beaucoup d'expérience et dans l'ensemble, ce fut une expérience positive. J'ai pu écrire une application entièrement fonctionnelle, en septembre je vais l'améliorer et la publier dans l'AppStore, simultanément à la sortie d'iOS 13.

Redux, BindableObject et EnvironmentObject

Développement d'applications sur SwiftUI. Partie 1 : Flux de données et Redux

Je travaille avec Redux depuis environ deux ans maintenant, donc je le connais relativement bien. En particulier, je l'utilise dans le frontend pour Réagir site Web, ainsi que pour le développement d'applications natives iOS (Swift) et Android (Kotlin).

Je n'ai jamais regretté d'avoir choisi Redux comme architecture de flux de données pour créer une application SwiftUI. Les parties les plus difficiles lors de l'utilisation de Redux dans une application UIKit sont de travailler avec le magasin, d'obtenir et de récupérer des données et de les mapper à vos vues/composants. Pour ce faire, j'ai dû créer une sorte de bibliothèque de connecteurs (en utilisant ReSwift et ReKotlin). Fonctionne bien, mais beaucoup de code. Malheureusement, il n’est pas (encore) open source.

Bonnes nouvelles! Les seules choses dont vous devez vous soucier avec SwiftUI - si vous envisagez d'utiliser Redux - sont les magasins, les états et les réducteurs. L'interaction avec le magasin est entièrement prise en charge par SwiftUI grâce à @EnvironmentObject. Ainsi, le magasin commence par un BindableObject.

J'ai créé un simple package Swift, SwiftUIFlux, qui fournit une utilisation de base de Redux. Dans mon cas, cela fait partie de MovieSwiftUI. moi aussi a écrit un tutoriel étape par étape, ce qui vous aidera à utiliser ce composant.

Comment ça marche?

final public class Store<State: FluxState>: BindableObject {
    public let willChange = PassthroughSubject<Void, Never>()
        
    private(set) public var state: State
    
    private func _dispatch(action: Action) {
        willChange.send()
        state = reducer(state, action)
    }
}

Chaque fois que vous déclenchez une action, vous activez la boîte de vitesses. Il évaluera les actions en fonction de l'état actuel de l'application. Il renverra ensuite un nouvel état modifié conformément au type d'action et aux données.

Eh bien, puisque store est un BindableObject, il avertira SwiftUI lorsque sa valeur changera à l'aide de la propriété willChange fournie par PassthroughSubject. En effet, le BindableObject doit fournir un PublisherType, mais l'implémentation du protocole est responsable de sa gestion. Dans l’ensemble, il s’agit d’un outil très puissant d’Apple. En conséquence, lors du prochain cycle de rendu, SwiftUI aidera à restituer le corps des vues en fonction du changement d'état.

En fait, c'est tout le cœur et la magie de SwiftUI. Désormais, dans toute vue abonnée à un état, la vue sera rendue en fonction des données reçues de l'état et de ce qui a changé.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
 
    var window: UIWindow?
 
 
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            
            let controller = UIHostingController(rootView: HomeView().environmentObject(store))
            window.rootViewController = controller
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}
 
struct CustomListCoverRow : View {
    @EnvironmentObject var store: Store<AppState>
    
    let movieId: Int
    var movie: Movie! {
        return store.state.moviesState.movies[movieId]
    }
    
    var body: some View {
        HStack(alignment: .center, spacing: 0) {
            Image(movie.poster)
        }.listRowInsets(EdgeInsets())
    }
}

Le Store est injecté en tant qu'EnvironmentObject au démarrage de l'application et est ensuite accessible dans n'importe quelle vue à l'aide de @EnvironmentObject. Il n'y a aucune dégradation des performances car les propriétés dérivées sont rapidement récupérées ou calculées à partir de l'état de l'application.

Le code ci-dessus change l'image si l'affiche du film change.

Et cela se fait en fait avec une seule ligne, à l'aide de laquelle les opinions sont reliées à l'État. Si vous avez travaillé avec ReSwift sur iOS ou même connect avec React, vous comprendrez la magie de SwiftUI.

Vous pouvez maintenant essayer d'activer l'action et de publier le nouvel état. Voici un exemple plus complexe.

struct CustomListDetail : View {
    @EnvironmentObject var store: Store<AppState>
 
    let listId: Int
    
    var list: CustomList {
        store.state.moviesState.customLists[listId]!
    }
    
    var movies: [Int] {
        list.movies.sortedMoviesIds(by: .byReleaseDate, state: store.state)
    }
    
    var body: some View {
        List {
            ForEach(movies) { movie in
                NavigationLink(destination: MovieDetail(movieId: movie).environmentObject(self.store)) {
                    MovieRow(movieId: movie, displayListImage: false)
                }
            }.onDelete { (index) in
               self.store.dispatch(action: MoviesActions.RemoveMovieFromCustomList(list: self.listId, movie: self.movies[index.first!]))
            }
        }
    }
}

Dans le code ci-dessus, j'utilise l'action .onDelete de SwiftUI pour chaque IP. Cela permet à la ligne de la liste d'afficher le balayage iOS normal à supprimer. Ainsi, lorsque l'utilisateur touche le bouton Supprimer, il déclenche l'action correspondante et supprime le film de la liste.

Eh bien, puisque la propriété list est dérivée de l'état BindableObject et est injectée en tant qu'EnvironmentObject, SwiftUI met à jour la liste car ForEach est associé à la propriété calculée des films.

Voici une partie du réducteur MoviesState :

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    
    // other actions.
    
    case let action as MoviesActions.AddMovieToCustomList:
        state.customLists[action.list]?.movies.append(action.movie)
        
    case let action as MoviesActions.RemoveMovieFromCustomList:
        state.customLists[action.list]?.movies.removeAll{ $0 == action.movie }
        
    default:
        break
    }
    return state
}

Le réducteur est exécuté lorsque vous envoyez une action et renvoyez un nouvel état, comme indiqué ci-dessus.

Je n'entrerai pas encore dans les détails - comment SwiftUI sait réellement quoi afficher. Pour comprendre cela plus en profondeur, cela vaut la peine voir la session WWDC sur le flux de données dans SwiftUI. Il explique également en détail pourquoi et quand utiliser Région, @Binding, ObjectBinding et EnvironmentObject.

Skillbox vous recommande :

Source: habr.com

Ajouter un commentaire