Sviluppo di un'applicazione su SwiftUI. Parte 1: flusso di dati e Redux

Sviluppo di un'applicazione su SwiftUI. Parte 1: flusso di dati e Redux

Dopo aver partecipato alla sessione State of the Union a WWDC 2019, ho deciso di approfondire SwiftUI. Ho dedicato molto tempo a lavorarci e ora sto iniziando a sviluppare un'applicazione reale che potrebbe rivelarsi utile per un vasto pubblico.

L'ho chiamata MovieSwiftUI: è un'app per cercare film nuovi e vecchi, oltre a raccoglierli in una collezione tramite TMDB API. Ho sempre amato i film e ho anche creato un'azienda che opera in questo settore, anche se è passato un po' di tempo. L'azienda non si può definire fantastica, ma l'applicazione sì!

Ricordiamo: per tutti i lettori di «Habr» — sconto di 10.000 rubli per l'iscrizione a qualsiasi corso Skillbox con il codice promozionale «Habr».

Skillbox consiglia: Corso online educativo «Professione Sviluppatore Java».

Quindi, cosa può fare MovieSwiftUI?

  • Interagisce con le API, come fa quasi ogni applicazione moderna.
  • Carica dati asincroni su richiesta e analizza JSON in modelli Swift usando Codable.
  • Mostra immagini caricate su richiesta e le memorizza nella cache.
  • Questa applicazione per iOS, iPadOS e macOS offre la migliore esperienza utente per gli utenti di questi OS.
  • L'utente può generare dati e creare le proprie liste di film. L'app memorizza e ripristina i dati dell'utente.
  • Le visualizzazioni, i componenti e i modelli sono chiaramente separati tramite il pattern Redux. Il flusso di dati è unidirezionale. Può essere completamente memorizzato nella cache, ripristinato e sovrascritto.
  • L'app utilizza i componenti di base di SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, ecc. Garantisce anche visualizzazioni personalizzate, gesti, UI/UX.

Sviluppo di un'applicazione su SwiftUI. Parte 1: flusso di dati e Redux
In realtà, l'animazione è fluida, mentre il GIF è risultato un po' scattoso.

Lavorare sull'applicazione mi ha dato molta esperienza e, in generale, è stata un'esperienza positiva. Sono riuscito a scrivere un'applicazione completamente funzionale; a settembre la migliorerò e la pubblicherò su App Store, insieme al rilascio di iOS 13.

Redux, BindableObject e EnvironmentObject

Sviluppo di un'applicazione su SwiftUI. Parte 1: flusso di dati e Redux

Attualmente lavoro con Redux da circa due anni, quindi ho una buona comprensione al riguardo. In particolare, lo uso nel frontend per React siti web, oltre che per lo sviluppo di app native iOS (Swift) e Android (Kotlin).

Non ho mai rimpianto di aver scelto Redux come architettura per il flusso di dati per creare un'applicazione su SwiftUI. I momenti più complessi nell'utilizzo di Redux all'interno di un'applicazione UIKit sono lavorare con lo store, così come ottenere e prelevare dati e mappare questi ultimi alle vostre viste/componenti. Per fare ciò, ho dovuto creare una sorta di libreria di connettori (su ReSwift e ReKotlin). Funziona bene, ma c'è molto codice. Purtroppo, al momento, non è open source.

Buone notizie! L'unica cosa di cui preoccuparsi con SwiftUI — se intendete utilizzare Redux — è lo store, gli stati e i reducer. L'interazione con lo store è completamente gestita da SwiftUI grazie a @EnvironmentObject. Così, lo store inizia con BindableObject.

Ho creato un semplice pacchetto Swift, SwiftUIFlux, che garantisce un utilizzo di base di Redux. Nel mio caso è parte di MovieSwiftUI. Ho anche scritto un tutorial passo-passo, che aiuterà a utilizzare questo componente.

Come funziona?

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

Ogni volta che eseguite un'azione, attivate il reducer. Questo valuterà le azioni in base all'attuale stato dell'applicazione. Successivamente, restituirà un nuovo stato modificato in base al tipo di azione e ai dati.

Poiché lo store è un BindableObject, notificherà SwiftUI della modifica del proprio valore, utilizzando la proprietà willChange fornita da PassthroughSubject. Questo accade perché il BindableObject deve fornire un PublisherType, ma l'implementazione del protocollo è responsabile della sua gestione. In generale, è uno strumento molto potente di Apple. Di conseguenza, nel prossimo ciclo di rendering, SwiftUI aiuterà a visualizzare il corpo delle viste in base alla modifica dello stato.

In effetti, questo è tutto — il cuore e la magia di SwiftUI. Ora in qualsiasi vista che è sottoscritta allo stato, la vista verrà visualizzata in base a quali dati sono stati ottenuti dallo stato e 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
    
    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 viene iniettato come EnvironmentObject all'avvio dell'applicazione e poi è accessibile in qualsiasi vista tramite @EnvironmentObject. Le prestazioni non diminuiscono, poiché le proprietà derivate vengono estratte o calcolate rapidamente dallo stato dell'applicazione.

Il codice sopra modifica l'immagine se il poster del film cambia.

E ciò avviene effettivamente con una sola riga, che collega le viste allo stato. Se hai lavorato con ReSwift su iOS o anche connect con React, capirai qual è la magia di SwiftUI.

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

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

Nel codice sopra, utilizzo l'azione .onDelete di SwiftUI per ogni IP. Questo consente alla riga nell'elenco di visualizzare il normale swipe di iOS per eliminare. Quindi, quando l'utente tocca il pulsante di eliminazione, attiva l'azione corrispondente ed elimina il film dall'elenco.

Poiché la proprietà dell'elenco è derivata dallo stato di BindableObject e viene iniettata come EnvironmentObject, SwiftUI aggiorna l'elenco poiché ForEach è collegato alla proprietà calcolata movies.

Ecco una parte del riduttore MoviesState:

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    
    // altre azioni.
    
    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 invii un'azione e restituisce un nuovo stato, come detto sopra.

Non entrerò ancora nei dettagli su come SwiftUI sappia effettivamente cosa visualizzare. Per comprendere questo più a fondo, vale la pena guardare la sessione WWDC sul flusso di dati in SwiftUI. Viene anche spiegato in dettaglio quando e perché utilizzare Stato, @Binding, ObjectBinding e EnvironmentObject.

Skillbox consiglia:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster