Dezvoltare de aplicații pe SwiftUI. Partea 1: Flux de date și Redux

Dezvoltare de aplicații pe SwiftUI. Partea 1: Flux de date și Redux

După ce am participat la sesiunea State of the Union la WWDC 2019, am decis să fac o scufundare profundă în SwiftUI. Am petrecut mult timp lucrând cu el și acum am început să dezvolt o aplicație reală care poate fi utilă unei game largi de utilizatori.

L-am numit MovieSwiftUI - aceasta este o aplicație pentru căutarea de filme noi și vechi, precum și pentru colectarea lor într-o colecție folosind TMDB API. Întotdeauna mi-au plăcut filmele și chiar am creat o companie care lucrează în acest domeniu, deși cu mult timp în urmă. Compania cu greu putea fi numită cool, dar aplicația a fost!

Amintim: pentru toți cititorii „Habr” - o reducere de 10 de ruble la înscrierea la orice curs Skillbox folosind codul promoțional „Habr”.

Skillbox recomandă: Curs educativ online „Profesie Dezvoltator Java”.

Deci, ce poate face MovieSwiftUI?

  • Interacționează cu API - aproape orice aplicație modernă face acest lucru.
  • Încarcă date asincrone privind solicitările și analizează JSON în modelul Swift folosind Codabil.
  • Afișează imaginile încărcate la cerere și le memorează în cache.
  • Această aplicație pentru iOS, iPadOS și macOS oferă cel mai bun UX pentru utilizatorii acestor sisteme de operare.
  • Utilizatorul poate genera date și își poate crea propriile liste de filme. Aplicația salvează și restaurează datele utilizatorului.
  • Vizualizările, componentele și modelele sunt clar separate folosind modelul Redux. Fluxul de date aici este unidirecțional. Poate fi memorat în cache, restaurat și suprascris.
  • Aplicația folosește componentele de bază ale SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal etc. De asemenea, oferă vizualizări personalizate, gesturi, UI/UX.

Dezvoltare de aplicații pe SwiftUI. Partea 1: Flux de date și Redux
De fapt, animația este netedă, GIF-ul s-a dovedit 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ă, în septembrie o voi îmbunătăți și o voi publica în AppStore, concomitent cu lansarea iOS 13.

Redux, BindableObject și EnvironmentObject

Dezvoltare de aplicații pe SwiftUI. Partea 1: Flux de date și Redux

Lucrez cu Redux de aproximativ doi ani acum, așa că sunt relativ bine versat în asta. În special, îl folosesc în frontend pentru Reacţiona site-ul web, precum și pentru dezvoltarea de aplicații native iOS (Swift) și Android (Kotlin).

Nu am regretat niciodată că am ales Redux ca arhitectură de flux de date pentru construirea unei aplicații SwiftUI. Cele mai dificile părți atunci când utilizați Redux într-o aplicație UIKit sunt lucrul cu magazinul și obținerea și preluarea datelor și maparea acestora cu vizualizările/componentele dvs. Pentru a face acest lucru, a trebuit să creez un fel de bibliotecă de conectori (folosind ReSwift și ReKotlin). Funcționează bine, dar destul de mult cod. Din păcate, nu este (încă) open source.

Vești bune! Singurele lucruri de care să vă faceți griji cu SwiftUI - dacă intenționați să utilizați Redux - sunt magazinele, statele și reductoarele. Interacțiunea cu magazinul este complet îngrijită de SwiftUI datorită @EnvironmentObject. Deci, magazinul începe cu un BindableObject.

Am creat un pachet Swift simplu, SwiftUIFlux, care oferă utilizarea de bază a Redux. În cazul meu, face parte din MovieSwiftUI. și eu a scris un tutorial pas cu pas, care vă va ajuta să utilizați această componentă.

Cum funcționează?

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)
    }
}

De fiecare dată când declanșați o acțiune, activați cutia de viteze. Acesta va evalua acțiunile în funcție de starea actuală a aplicației. Va returna apoi o nouă stare modificată în conformitate cu tipul de acțiune și datele.

Ei bine, deoarece magazinul este un BindableObject, acesta va notifica SwiftUI când valoarea sa se schimbă folosind proprietatea willChange furnizată de PassthroughSubject. Acest lucru se datorează faptului că BindableObject trebuie să furnizeze un PublisherType, dar implementarea protocolului este responsabilă pentru gestionarea acestuia. În general, acesta este un instrument foarte puternic de la Apple. În consecință, în următorul ciclu de randare, SwiftUI va ajuta la redarea corpului vizualizărilor în funcție de schimbarea stării.

De fapt, aceasta este toată inima și magia SwiftUI. Acum, în orice vizualizare care subscrie la o stare, vizualizarea va fi redată în funcție de ce date sunt primite de la stat și ce s-au 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<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())
    }
}

Magazinul este injectat ca EnvironmentObject când pornește aplicația și este apoi accesibil în orice vizualizare folosind @EnvironmentObject. Nu există nicio penalizare de performanță deoarece proprietățile derivate sunt rapid recuperate sau calculate din starea aplicației.

Codul de mai sus modifică imaginea dacă se schimbă afișul filmului.

Și acest lucru se face de fapt cu o singură linie, cu ajutorul căreia vederile sunt conectate la stat. Dacă ați lucrat cu ReSwift pe iOS sau chiar conectaţi cu React, vei înțelege magia SwiftUI.

Acum puteți încerca să activați acțiunea și să publicați noua stare. Iată un exemplu mai complex.

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!]))
            }
        }
    }
}

În codul de mai sus, folosesc acțiunea .onDelete din SwiftUI pentru fiecare IP. Acest lucru permite rândului din listă să afișeze glisarea obișnuită pentru iOS pentru a șterge. Deci, atunci când utilizatorul atinge butonul de ștergere, declanșează acțiunea corespunzătoare și elimină filmul din listă.

Ei bine, deoarece proprietatea listă este derivată din starea BindableObject și este injectată ca EnvironmentObject, SwiftUI actualizează lista deoarece ForEach este asociat cu proprietatea calculată pentru filme.

Iată o parte din reductorul 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
}

Reductorul este executat atunci când trimiteți o acțiune și returnați o nouă stare, așa cum sa menționat mai sus.

Nu voi intra încă în detalii - cum SwiftUI știe de fapt ce să afișeze. Pentru a înțelege acest lucru mai profund, merită vizualizați sesiunea WWDC despre fluxul de date în SwiftUI. De asemenea, explică în detaliu de ce și când să utilizați Stat, @Binding, ObjectBinding și EnvironmentObject.

Skillbox recomandă:

Sursa: www.habr.com

Adauga un comentariu