
Après avoir participé à la session State of the Union à la WWDC 2019, j'ai décidé d'étudier en détail SwiftUI. J'ai passé beaucoup de temps à travailler avec et j'ai maintenant commencé à développer une application réelle qui pourrait être utile à un large public.
Je l'ai nommé MovieSwiftUI — c'est une application pour rechercher des films récents et anciens, ainsi que pour les rassembler dans une collection grâce à . J'ai toujours aimé le cinéma et j'ai même créé une entreprise dans ce domaine, il y a longtemps. L'entreprise était difficile à qualifier de géniale, mais l'application l'est !
Rappelons-le : pour tous les lecteurs de « Habr » — une réduction de 10 000 roubles lors de l'inscription à tout cours Skillbox avec le code promo « Habr ».
Skillbox recommande : Cours en ligne éducatif .
Alors, que peut faire MovieSwiftUI ?
- Elle interagit avec l'API — c'est ce que fait presque toute application moderne.
- Elle charge des données asynchrones sur demande et analyse le JSON en modèles Swift, en utilisant .
- Affiche les images chargées sur demande et les met en cache.
- Cette application pour iOS, iPadOS et macOS offre la meilleure expérience utilisateur 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 des utilisateurs.
- Les vues, composants et modèles sont clairement séparés grâce au modèle Redux. Le flux de données y est unidirectionnel. Il peut être entièrement mis en cache, restauré et réécrit.
- L'application utilise des composants de base SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. Elle propose également des vues personnalisées, des gestes, et une UI/UX.

En réalité, l'animation est fluide, mais le GIF s'est avéré un peu saccadé.
Le travail sur l'application m'a beaucoup apporté, et dans l'ensemble, c'est une expérience positive. J'ai pu écrire une application entièrement fonctionnelle, que j'améliorerai en septembre et publierai sur l'App Store, en même temps que la sortie d'iOS 13.
Redux, BindableObject et EnvironmentObject

À l'heure actuelle, je travaille avec Redux depuis environ deux ans, donc je m'y connais assez bien. En particulier, je l'utilise dans le frontend pour le 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 sur SwiftUI. Les aspects les plus complexes lors de l'utilisation de Redux dans une application UIKit sont la gestion du store, ainsi que la récupération et l'extraction de données et leur correspondance avec vos vues / composants. Pour cela, j'ai dû créer une sorte de bibliothèque de connecteurs (sur ReSwift et ReKotlin). Ça fonctionne bien, mais il y a pas mal de code. Malheureusement, cela n'est pas (encore) open source.
Bonne nouvelle ! La seule chose dont il faut se préoccuper avec SwiftUI — si vous envisagez d'utiliser Redux — c'est le store, les états et les réducteurs. L'interaction avec le store est entièrement gérée par SwiftUI grâce à @EnvironmentObject. Ainsi, le store commence avec BindableObject.
J'ai créé un simple paquet Swift, , qui offre une utilisation de base de Redux. Dans mon cas, cela fait partie de MovieSwiftUI. J'ai également , qui aidera à utiliser ce composant.
Comment cela fonctionne ?
final public class Store: BindableObject {
public let willChange = PassthroughSubject()
private(set) public var state: State
private func _dispatch(action: Action) {
willChange.send()
state = reducer(state, action)
}
}Chaque fois que vous exécutez une action, vous activez le réducteur. Celui-ci évaluera les actions en fonction de l'état actuel de l'application. Ensuite, il retournera un nouveau statut modifié en fonction de l'action et des données.
Et comme le store est un BindableObject, il informera SwiftUI du changement de sa valeur, en utilisant la propriété willChange fournie par PassthroughSubject. C'est ainsi que le BindableObject doit fournir un PublisherType, mais l'implémentation du protocole est responsable de sa gestion. En général, c'est un outil très puissant de la part d'Apple. Par conséquent, lors du prochain cycle de rendu, SwiftUI aidera à afficher le corps des vues en fonction des changements d'état.
En fait, c'est tout — le cœur et la magie de SwiftUI. Maintenant, dans n'importe quelle vue qui est abonnée à l'état, la vue sera affichée en fonction des données reçues de l'état et des changements qui ont eu lieu.
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é comme EnvironmentObject lors du lancement de l'application, puis est accessible dans n'importe quelle vue à l'aide de @EnvironmentObject. Les performances ne diminuent pas, car les propriétés dérivées sont rapidement extraites ou calculées à partir de l'état de l'application.
Le code ci-dessus modifie l'image lorsque l'affiche du film change.
Et cela se fait en fait en une seule ligne, qui connecte les vues à l'état. Si vous avez travaillé avec ReSwift sur iOS ou même avec React, vous comprendrez la magie de SwiftUI.
Essayez maintenant d'activer une 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 à une ligne dans la liste d'afficher le glissement habituel d'iOS pour supprimer. Ainsi, lorsque l'utilisateur touche le bouton de suppression, cela déclenche l'action correspondante et supprime le film de la liste.
Puisque la propriété de la liste est dérivée de l'état de BindableObject et est injectée comme EnvironmentObject, SwiftUI met à jour la liste car ForEach est lié à la propriété calculée movies.
Voici une partie du réducteur MoviesState :
func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
var state = state
switch action {
// autres 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 s'exécute lorsque vous envoyez une action et renvoie un nouvel état, comme indiqué ci-dessus.
Je ne vais pas encore entrer dans les détails — d'où SwiftUI sait vraiment ce qu'il faut afficher. Pour comprendre cela plus en profondeur, il vaut la peine dans SwiftUI. Il y est également détaillé quand et pourquoi utiliser , @Binding, ObjectBinding et EnvironmentObject.
Skillbox recommande :
- Cours pratique .
- Cours en ligne appliqué .
- Cours pratique de deux ans .
Source : habr.com
