
După ce am participat la sesiunea State of the Union de la WWDC 2019, am decis să studiez în detaliu SwiftUI. Am investit mult timp în a lucra cu acesta și acum am început dezvoltarea unei aplicații reale, care ar putea fi utilă unui public larg.
Am numit-o MovieSwiftUI — este o aplicație pentru căutarea de filme noi și vechi, precum și pentru organizarea acestora într-o colecție folosind . Întotdeauna am iubit filmele și chiar am creat o companie care a activat în acest domeniu, deși acum mult timp. Compania a fost greu de numit tare, dar aplicația — da!
Vă reamintim: pentru toți cititorii „Habr” — reducere de 10.000 de ruble la înscrierea la orice curs Skillbox cu codul de promovare „Habr”.
Skillbox recomandă: Curs online educațional .
Așadar, ce poate face MovieSwiftUI?
- Interacționează cu API-ul — asta face aproape orice aplicație modernă.
- Încarcă date async la cerere și parsează JSON în modele Swift, folosind .
- Afișează imagini, încărcate pe cerere, și le cachează.
- Această aplicație pentru iOS, iPadOS și macOS asigură cea mai bună experiență utilizator pentru utilizatorii acestor OS-uri.
- Utilizatorul poate genera date, crea propriile liste de filme. Aplicația salvează și restabilește datele utilizatorului.
- Vederile, componentele și modelele sunt clar separate folosind modelul Redux. Fluxul de date este unidirecțional. Poate fi complet cache-uit, restaurat și rescris.
- Aplicația utilizează componentele de bază ale SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal etc. De asemenea, oferă vederi personalizate, gesturi, UI/UX.

De fapt, animația este fluidă, gif-ul a ieșit puțin sacadat.
Lucrul la aplicație mi-a oferit multă experiență și, în general, a fost o experiență pozitivă. Am reușit să scriu o aplicație complet funcțională, iar în septembrie o voi îmbunătăți și voi publica în AppStore, odată cu lansarea iOS 13.
Redux, BindableObject și EnvironmentObject

La momentul actual, lucrez cu Redux de aproximativ doi ani, așa că mă pricep destul de bine. În special, îl folosesc în frontend pentru site-ul web, precum și pentru dezvoltarea de aplicații native iOS (Swift) și Android (Kotlin).
Nu am regretat niciodată alegerea Redux ca arhitectură pentru fluxul de date în crearea unei aplicații pe SwiftUI. Cele mai dificile momente în utilizarea Redux într-o aplicație UIKit sunt lucrul cu store-ul, precum și obținerea și extragerea datelor și corelarea acestora cu viziunile/componentele tale. Pentru aceasta, a fost necesar să creez un fel de bibliotecă de conectori (pe ReSwift și ReKotlin). Funcționează bine, dar există destul de mult cod. Din păcate, acesta (deocamdată) nu este open source.
Vești bune! Unicul lucru de care ar trebui să îți faci griji când folosești SwiftUI — dacă intenționezi să utilizezi Redux — este store-ul, stările și reducerii. Interacțiunea cu store-ul este complet gestionată de SwiftUI prin @EnvironmentObject. Astfel, store-ul începe cu BindableObject.
Am creat un pachet simplu Swift, , care oferă utilizarea de bază a Redux. În cazul meu, aceasta este o parte din MovieSwiftUI. De asemenea, am , care te va ajuta să folosești acest component.
Cum funcționează?
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)
}
}De fiecare dată când declanșezi o acțiune, activezi un reducer. Acesta va evalua acțiunile conform stării curente a aplicației. Apoi, va returna o nouă stare modificată conform tipului de acțiune și datelor.
Și având în vedere că store-ul este un BindableObject, va notifica SwiftUI despre modificarea valorii sale folosind proprietatea willChange, furnizată de PassthroughSubject. Acest lucru se datorează faptului că BindableObject trebuie să furnizeze PublisherType, dar implementarea protocolului este responsabilă de gestionarea acestuia. În general, este un instrument foarte puternic de la Apple. În consecință, în următorul ciclu de redare, SwiftUI va ajuta la afișarea corpului viziunilor conform modificării stării.
De fapt, acestea sunt toate — inima și magia SwiftUI. Acum, în orice viziune care este abonată la stare, viziunea se va afișa în funcție de datele obținute din stare și ce s-a schimbat.
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
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())
}
}Store este implementat ca EnvironmentObject la pornirea aplicației, fiind apoi disponibil în orice vizualizare prin intermediul @EnvironmentObject. Performanța nu scade, deoarece proprietățile derivate sunt extrase sau calculate rapid din starea aplicației.
Codul de mai sus modifică imaginea atunci când posterul filmului se schimbă.
Și se face acest lucru în esență printr-o singură linie, care leagă vizualizările de stare. Dacă ați lucrat cu ReSwift pe iOS sau chiar cu React, veți înțelege care este magia SwiftUI.
Acum putem încerca să activăm o acțiune și să publicăm o nouă stare. Iată un exemplu mai complex.
struct CustomListDetail : View {
@EnvironmentObject var store: Store
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!]))
}
}
}
}În codul de mai sus folosesc acțiunea .onDelete din SwiftUI pentru fiecare IP. Aceasta permite unui rând din listă să afișeze un swipe normal iOS pentru a șterge. Astfel, când utilizatorul apasă pe butonul de ștergere, se declanșează acțiunea corespunzătoare și filmele sunt șterse din listă.
Ei bine, având în vedere că proprietatea listei este derivată din starea BindableObject și implementată ca EnvironmentObject, SwiftUI actualizează lista deoarece ForEach este legat de proprietatea calculată movies.
Iată o parte din reducerul 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
}Reducerul se execută atunci când trimiteți o acțiune și returnează un nou stat, așa cum s-a menționat mai sus.
Nu voi intra deocamdată în detalii — de unde știe de fapt SwiftUI ce să afișeze. Pentru a înțelege mai profund, merită în SwiftUI. De asemenea, se discută detaliat despre de ce și când să folosiți , @Binding, ObjectBinding și EnvironmentObject.
Skillbox recomandă:
- Curs practic .
- Curs online aplicat .
- Curs practic de doi ani .
Sursa: habr.com
