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



Après avoir participé à la session sur l'état de l'Union à la WWDC 2019, j'ai décidé d'étudier SwiftUI en détail. J'ai passé beaucoup de temps à travailler avec lui et maintenant j'ai commencé à développer une vraie application qui peut être utile à un large éventail d'utilisateurs.

Je l'ai appelé MovieSwiftUI - c'est une application pour trouver des films nouveaux et anciens, ainsi que pour les collecter dans une collection en utilisant l' API TMDB . J'ai toujours aimé les films et j'ai même créé une entreprise travaillant dans ce domaine, mais depuis longtemps. Il était difficile d'appeler l'entreprise cool, mais l'application - oui!

Nous vous rappelons: pour tous les lecteurs de «Habr» - une remise de 10 000 roubles lors de l'inscription à un cours Skillbox en utilisant le code promo «Habr».

Skillbox recommande: Le cours éducatif en ligne "Profession Java-developer" .

Alors, que fait MovieSwiftUI?

  • Interagit avec l'API - c'est ce que font presque toutes les applications modernes.
  • Charge les données de demande asynchrones et analyse JSON dans le modèle Swift à l'aide de Codable .
  • Affiche les images téléchargées à la demande et les met en cache.
  • Cette application pour iOS, iPadOS et macOS fournit le meilleur UX pour les utilisateurs de ces systèmes d'exploitation.
  • L'utilisateur peut générer des données, 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 est ici unidirectionnel. Il peut être entièrement mis en cache, restauré et écrasé.
  • L'application utilise les composants de base SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. Il fournit également des vues personnalisées, des gestes, UI / UX.


En fait, l'animation est fluide, le GIF s'est révélé un peu nerveux

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

Redux, BindableObject et EnvironmentObject




Je travaille avec Redux depuis environ deux ans maintenant, donc je suis assez bon dans ce domaine. Je l'utilise notamment dans l'interface du site React , 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 construire une application sur SwiftUI. Les moments les plus difficiles lors de l'utilisation de Redux dans l'application UIKit sont de travailler avec le magasin, ainsi que d'obtenir et de récupérer des données et de les comparer avec vos vues / composants. Pour ce faire, j'ai dû créer une sorte de bibliothèque de connecteurs (sur ReSwift et ReKotlin). Fonctionne bien, mais pas mal de code. Malheureusement, il est (pas encore) open source.

Bonne nouvelle! Les seules choses à s'inquiéter avec SwiftUI - si vous prévoyez d'utiliser Redux - sont le magasin, les états et les réducteurs. Le SwiftUI prend en charge l'interaction avec la boutique grâce à @EnvironmentObject. Donc, le magasin commence par BindableObject.

J'ai créé un package Swift simple, SwiftUIFlux , qui fournit l'utilisation de base de Redux. Dans mon cas, cela fait partie de MovieSwiftUI. J'ai également écrit un tutoriel étape par étape pour vous aider à 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émarrez une action, vous activez la boîte de vitesses. Il évaluera les actions en fonction de l'état actuel de la demande. Ensuite, il retournera un nouvel état modifié en fonction du type d'action et des données.

Eh bien, puisque store est un BindableObject, il informera SwiftUI d'un changement de sa valeur en utilisant la propriété willChange fournie par PassthroughSubject. En effet, BindableObject doit fournir PublisherType, mais l'implémentation du protocole est responsable de sa gestion. Dans l'ensemble, c'est un outil très puissant d'Apple. En conséquence, dans le prochain cycle de rendu, SwiftUI aidera à afficher le corps des représentations conformément au changement d'état.

En fait, c'est tout - le cœur et la magie de SwiftUI. Maintenant, dans toute vue abonnée à un état, la vue sera affichée 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 magasin est implémenté en tant qu'Objet Environnement au démarrage de l'application, puis disponible dans n'importe quelle vue à l'aide de @EnvironmentObject. Les performances ne sont pas réduites 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 sur une seule ligne, à l'aide de laquelle les vues sont connectées à l'état. Si vous avez travaillé avec ReSwift sur iOS ou même vous connectez avec React, vous comprendrez ce qu'est la magie SwiftUI.

Et maintenant, vous pouvez essayer d'activer l'action et de publier un 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 habituel pour la suppression. Par conséquent, lorsque l'utilisateur touche le bouton Supprimer, il démarre l'action correspondante et supprime le film de la liste.

Eh bien, étant donné que la propriété de liste est dérivée de l'état de BindableObject et est implémentée en tant qu'environnementObjet, SwiftUI met à jour la liste, car ForEach est associé à la propriété de film calculée.

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 soumettez l'action et renvoyez un nouvel état, comme mentionné ci-dessus.

Je n'entrerai pas dans les détails pour l'instant - comment SwiftUI sait-il vraiment quoi afficher. Pour mieux comprendre cela, vous devriez regarder la session WWDC sur le flux de données dans SwiftUI. Il explique également en détail pourquoi et quand utiliser State , @Binding, ObjectBinding et EnvironmentObject.

Skillbox recommande:

Source: https://habr.com/ru/post/fr460713/


All Articles