Sviluppu di l'applicazioni nantu à SwiftUI. Parte 1: Dataflow è Redux
Dopu avè assistitu à a sessione di u Statu di l'Unione à a WWDC 2019, aghju decisu di fà una immersione profonda in SwiftUI. Aghju passatu assai tempu à travaglià cun ellu è avà cuminciatu à sviluppà una vera applicazione chì pò esse utile à una larga gamma di utilizatori.
L'aghju chjamatu MovieSwiftUI - questa hè una app per a ricerca di filmi novi è vechji, è ancu di cullà in una cullezzione cù API TMDB. Aghju sempre amatu i filmi è ancu creatu una cumpagnia chì travaglia in questu campu, ancu s'ellu hè assai tempu fà. A cumpagnia ùn pudia micca esse chjamata cool, ma l'applicazione era!
Ramintemu:per tutti i lettori di "Habr" - un scontu di 10 000 rubles quandu si iscrizzione in ogni cursu Skillbox cù u codice promozionale "Habr".
Interagisce cù l'API - quasi ogni applicazione muderna face questu.
Carica dati asincroni nantu à e dumande è analizza JSON in u mudellu Swift utilizendu Codable.
Mostra l'imaghjini caricate nantu à dumanda è li cache.
Questa app per iOS, iPadOS è macOS furnisce u megliu UX per l'utilizatori di questi OS.
L'utilizatore pò generà dati è creà a so propria lista di filmi. L'applicazione salva è restaurà i dati di l'utilizatori.
Viste, cumpunenti è mudelli sò chjaramente separati cù u mudellu Redux. U flussu di dati quì hè unidirezionale. Pò esse cumplettamente in cache, restauratu è sovrascrittu.
L'applicazione usa i cumpunenti basi di SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. Fornisce ancu viste persunalizate, gesti, UI / UX.
In fatti, l'animazione hè liscia, u GIF hè diventatu un pocu saccu
U travagliu nantu à l'app m'hà datu assai sperienza è in generale hè stata una sperienza positiva. Puderaghju scrive una applicazione cumplettamente funziunale, in settembre l'aghju da migliurà è publicà in l'AppStore, simultaneamente cù a liberazione di iOS 13.
Redux, BindableObject è EnvironmentObject
Aghju travagliatu cù Redux per circa dui anni, cusì sò abbastanza versatu in questu. In particulare, l'aghju utilizatu in u frontend per React situ web, è ancu per sviluppà applicazioni native iOS (Swift) è Android (Kotlin).
Ùn aghju mai lamentatu di sceglie Redux cum'è l'architettura di flussu di dati per custruisce una applicazione SwiftUI. E parte più sfida quandu si usa Redux in una app UIKit sò travagliendu cù u magazinu è uttene è ricuperà e dati è mappendu i vostri punti di vista / cumpunenti. Per fà questu, aghju avutu à creà un tipu di biblioteca di connettori (usendu ReSwift è ReKotlin). Funziona bè, ma assai codice. Sfortunatamente, ùn hè micca (ancora) open source.
Bona nutizia ! L'unicu cose da preoccupassi cù SwiftUI - se pensa à aduprà Redux - sò i magazzini, i stati è i riduttori. L'interazzione cù a tenda hè cumpletamente curata da SwiftUI grazia à @EnvironmentObject. Allora, a tenda principia cù un BindableObject.
Aghju creatu un pacchettu Swift simplice, SwiftUIFlux, chì furnisce l'usu basicu di Redux. In u mo casu, hè parte di MovieSwiftUI. anch'eiu hà scrittu un tutoriale passu per passu, chì vi aiuterà à aduprà stu cumpunente.
Cumu viaghja?
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)
}
}
Ogni volta chì attivate una azzione, attivate u gearbox. Evaluerà l'azzioni secondu u statu attuale di l'applicazione. Tandu turnerà un novu statu mudificatu in cunfurmità cù u tipu d'azzione è i dati.
Ebbè, postu chì a tenda hè un BindableObject, notificà à SwiftUI quandu u so valore cambia cù a pruprietà willChange furnita da PassthroughSubject. Questu hè chì u BindableObject deve furnisce un PublisherType, ma l'implementazione di u protocolu hè rispunsevule per a gestione. In generale, questu hè un strumentu assai putente da Apple. In cunsiquenza, in u prossimu ciculu di rendering, SwiftUI aiuterà à rende u corpu di e viste secondu u cambiamentu di statu.
In realtà, questu hè tuttu u core è a magia di SwiftUI. Avà, in ogni vista chì sottumette à un statu, a vista serà resa secondu ciò chì dati hè ricevutu da u statu è ciò chì hà cambiatu.
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())
}
}
U Store hè injected cum'è EnvironmentObject quandu l'applicazione principia è hè dopu accessibile in ogni vista cù @EnvironmentObject. Ùn ci hè micca penalità di rendiment perchè e proprietà derivate sò rapidamente recuperate o calculate da u statu di l'applicazione.
U codice sopra cambia l'imaghjini se u poster di film cambia.
È questu hè in realtà fattu cù una sola linea, cù l'aiutu di quali viste sò cunnessi à u statu. Sè avete travagliatu cù ReSwift in iOS o ancu cunnette cù React, capirete a magia di SwiftUI.
Avà pudete pruvà à attivà l'azzione è publicà u novu statu. Eccu un esempiu più cumplessu.
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!]))
}
}
}
}
In u codice sopra, aghju aduprà l'azzione .onDelete da SwiftUI per ogni IP. Questu permette à a fila in a lista per vede u normale iOS swipe per sguassà. Allora quandu l'utilizatore toccu u buttone di sguassà, attiva l'azzione currispondente è elimina u filmu da a lista.
Ebbè, postu chì a pruprietà di a lista hè derivata da u statu BindableObject è hè injected cum'è EnvironmentObject, SwiftUI aghjurnà a lista perchè ForEach hè assuciatu cù a pruprietà calculata di i filmi.
Eccu una parte di u riduttore 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
}
U riduttore hè eseguitu quandu spedite una azzione è rinviate un novu statu, cum'è dettu sopra.
Ùn entreraghju ancu in dettagli - cumu SwiftUI sapi veramente ciò chì mostra. Per capiscenu questu più profondamente, vale a pena vede a sessione WWDC nantu à u flussu di dati in SwiftUI. Spiega ancu in dettagliu perchè è quandu aduprà statu, @Binding, ObjectBinding è EnvironmentObject.