Sviluppo di applicazioni su SwiftUI. Parte 1: flusso di dati e Redux

Sviluppo di applicazioni su SwiftUI. Parte 1: flusso di dati e Redux

Dopo aver partecipato alla sessione sullo stato dell'Unione alla WWDC 2019, ho deciso di approfondire SwiftUI. Ci ho lavorato molto tempo e ora ho iniziato a sviluppare una vera applicazione che può essere utile a un'ampia gamma di utenti.

L'ho chiamata MovieSwiftUI: è un'app per cercare film vecchi e nuovi, nonché per raccoglierli in una raccolta utilizzando APITMDB. Ho sempre amato il cinema e ho anche creato un'azienda che lavora in questo campo, anche se molto tempo fa. L'azienda difficilmente poteva essere definita interessante, ma l'applicazione lo era!

Ti ricordiamo: per tutti i lettori di "Habr" - uno sconto di 10 rubli al momento dell'iscrizione a qualsiasi corso Skillbox utilizzando il codice promozionale "Habr".

Skillbox consiglia: Corso didattico online "Sviluppatore Java professionista".

Allora cosa può fare MovieSwiftUI?

  • Interagisce con l'API: quasi tutte le applicazioni moderne lo fanno.
  • Carica dati asincroni sulle richieste e analizza JSON nel modello Swift utilizzando Codificabile.
  • Mostra le immagini caricate su richiesta e le memorizza nella cache.
  • Questa app per iOS, iPadOS e macOS offre la migliore UX per gli utenti di questi sistemi operativi.
  • L'utente può generare dati e creare i propri elenchi di film. L'applicazione salva e ripristina i dati dell'utente.
  • Viste, componenti e modelli sono chiaramente separati utilizzando il modello Redux. Il flusso di dati qui è unidirezionale. Può essere completamente memorizzato nella cache, ripristinato e sovrascritto.
  • L'applicazione utilizza i componenti di base di SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, ecc. Fornisce inoltre visualizzazioni personalizzate, gesti, UI/UX.

Sviluppo di applicazioni su SwiftUI. Parte 1: flusso di dati e Redux
In effetti, l'animazione è fluida, la GIF è risultata un po' a scatti

Lavorare sull'app mi ha dato molta esperienza e nel complesso è stata un'esperienza positiva. Sono riuscito a scrivere un'applicazione perfettamente funzionante, a settembre la migliorerò e la pubblicherò su AppStore, contemporaneamente al rilascio di iOS 13.

Redux, BindableObject e EnvironmentObject

Sviluppo di applicazioni su SwiftUI. Parte 1: flusso di dati e Redux

Lavoro con Redux da circa due anni ormai, quindi ne sono relativamente esperto. In particolare, lo utilizzo nel frontend per Reagire sito web, nonché per lo sviluppo di applicazioni native iOS (Swift) e Android (Kotlin).

Non mi sono mai pentito di aver scelto Redux come architettura del flusso di dati per creare un'applicazione SwiftUI. Le parti più impegnative quando si utilizza Redux in un'app UIKit sono lavorare con lo store, ottenere e recuperare dati e mapparli sulle visualizzazioni/componenti. Per fare questo ho dovuto creare una sorta di libreria di connettori (usando ReSwift e ReKotlin). Funziona bene, ma c'è molto codice. Sfortunatamente non è (ancora) open source.

Buone notizie! Le uniche cose di cui preoccuparsi con SwiftUI, se prevedi di utilizzare Redux, sono negozi, stati e riduttori. L'interazione con il negozio è completamente gestita da SwiftUI grazie a @EnvironmentObject. Quindi, l'archivio inizia con un BindableObject.

Ho creato un semplice pacchetto Swift, SwiftUIFlux, che fornisce l'utilizzo di base di Redux. Nel mio caso fa parte di MovieSwiftUI. anche io ha scritto un tutorial passo passo, che ti aiuterà a utilizzare questo componente.

Come funziona?

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 che attivi un'azione, attivi il cambio. Valuterà le azioni in base allo stato attuale dell'applicazione. Restituirà quindi un nuovo stato modificato in base al tipo di azione e ai dati.

Bene, poiché store è un BindableObject, avviserà SwiftUI quando il suo valore cambia utilizzando la proprietà willChange fornita da PassthroughSubject. Questo perché BindableObject deve fornire un PublisherType, ma l'implementazione del protocollo è responsabile della sua gestione. Nel complesso, questo è uno strumento molto potente di Apple. Di conseguenza, nel prossimo ciclo di rendering, SwiftUI aiuterà a eseguire il rendering del corpo delle viste in base al cambiamento di stato.

In realtà, questo è tutto il cuore e la magia di SwiftUI. Ora, in qualsiasi vista che sottoscrive uno stato, la vista verrà renderizzata in base ai dati ricevuti dallo stato e a cosa è cambiato.

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

Lo Store viene inserito come EnvironmentObject all'avvio dell'applicazione ed è quindi accessibile in qualsiasi visualizzazione utilizzando @EnvironmentObject. Non vi è alcuna riduzione delle prestazioni perché le proprietà derivate vengono rapidamente recuperate o calcolate dallo stato dell'applicazione.

Il codice sopra cambia l'immagine se cambia la locandina del film.

E questo in realtà viene fatto con una sola linea, con l'aiuto della quale le opinioni sono collegate allo Stato. Se hai lavorato con ReSwift su iOS o anche connect con React capirai la magia di SwiftUI.

Ora puoi provare ad attivare l'azione e pubblicare il nuovo stato. Ecco un esempio più complesso.

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

Nel codice sopra, sto utilizzando l'azione .onDelete di SwiftUI per ciascun IP. Ciò consente alla riga nell'elenco di visualizzare il normale scorrimento iOS da eliminare. Pertanto, quando l'utente tocca il pulsante Elimina, attiva l'azione corrispondente e rimuove il film dall'elenco.

Bene, poiché la proprietà list deriva dallo stato BindableObject e viene inserita come EnvironmentObject, SwiftUI aggiorna l'elenco perché ForEach è associato alla proprietà calcolata dei film.

Ecco parte del 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
}

Il riduttore viene eseguito quando si invia un'azione e si restituisce un nuovo stato, come indicato sopra.

Non entrerò ancora nei dettagli: come SwiftUI sa effettivamente cosa visualizzare. Per capirlo più profondamente, vale la pena visualizzare la sessione del WWDC sul flusso di dati in SwiftUI. Spiega inoltre in dettaglio perché e quando utilizzarlo Regione / Stato, @Binding, ObjectBinding e EnvironmentObject.

Skillbox consiglia:

Fonte: habr.com

Aggiungi un commento