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

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

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

L'ho chiamata MovieSwiftUI: è un'app per cercare nuovi e vecchi film, oltre a raccoglierli in una collezione tramite TMDB API. Ho sempre amato i film e ho persino creato un'azienda che opera in questo settore, anche se molto tempo fa. Non si poteva definire una grande azienda, ma l'app 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, di cosa è capace MovieSwiftUI?

  • Interagisce con l'API — questo è ciò che fa quasi ogni applicazione moderna.
  • Carica dati asincroni su richiesta e fa parsing del JSON in modelli Swift, utilizzando Codable.
  • Mostra le immagini caricate su richiesta e le memorizza nella cache.
  • Questa applicazione per iOS, iPadOS e macOS offre la migliore esperienza utente per gli utenti dei rispettivi sistemi operativi.
  • L'utente può generare dati, creare le proprie liste di film. L'applicazione salva e ripristina i dati dell'utente.
  • Le rappresentazioni, i componenti e i modelli sono chiaramente separati tramite il pattern Redux. Il flusso dei dati è unidirezionale. Può essere interamente memorizzato in cache, ripristinato e riscritto.
  • L'app utilizza componenti di base di SwiftUI, come TabbedView, SegmentedControl, NavigationView, Form, Modal, ecc. Inoltre, offre visualizzazioni personalizzate, gesti, UI/UX.

Sviluppo di un'applicazione in SwiftUI. Parte 1: flusso di dati e Redux
In effetti, l'animazione è fluida, il GIF è risultata un po' traballante.

Lavorare sull'app mi ha fornito molta esperienza e, in generale, è stata un'esperienza positiva. Sono riuscito a scrivere un'applicazione completamente funzionale, la migliorerò a settembre e la pubblicherò su AppStore, simultaneamente all'uscita di iOS 13.

Redux, BindableObject e EnvironmentObject

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

Attualmente, lavoro con Redux da circa due anni, quindi ho una buona comprensione. In particolare, lo utilizzo nel frontend per React sito web, oltre per lo sviluppo di applicazioni native iOS (Swift) e Android (Kotlin).

Non ho mai rimpianto di aver scelto Redux come architettura del flusso di dati per creare un'applicazione su SwiftUI. I momenti più complessi nell'utilizzo di Redux in un'applicazione UIKit riguardano la gestione dello store, così come il recupero e l'estrazione dei dati e l'abbinamento con le vostre viste / componenti. Per questo ho dovuto creare una sorta di libreria di connettori (su ReSwift e ReKotlin). Funziona bene, ma ci sono abbastanza codice. Sfortunatamente, al momento non è open source.

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

Ho creato un semplice pacchetto Swift, SwiftUIFlux, che fornisce un utilizzo di base di Redux. Nel mio caso, è parte di MovieSwiftUI. Inoltre ho 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 avvii un'azione, attivi il riduttore. Questo valuterà le azioni in base allo stato attuale dell'applicazione. Successivamente, restituirà un nuovo stato modificato in base al tipo di azione e ai dati.

Poiché lo store è un BindableObject, notificherà SwiftUI quando il suo valore cambia, utilizzando la proprietà willChange fornita da PassthroughSubject. Questo accade perché il BindableObject deve fornire un PublisherType, ma l'implementazione del protocollo è responsabile della gestione. In generale, si tratta di uno strumento molto potente offerto da Apple. Pertanto, nel successivo ciclo di rendering, SwiftUI aiuterà a visualizzare il contenuto delle viste in base alle modifiche dello stato.

In effetti, questo è tutto: il cuore e la magia di SwiftUI. Ora in qualsiasi vista iscritta allo stato, la vista sarà visualizzata in relazione 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
    
    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 implementato come EnvironmentObject all'avvio dell'applicazione e poi è accessibile in qualsiasi vista grazie a @EnvironmentObject. Le prestazioni non diminuiscono, poiché le proprietà derivate vengono rapidamente estratte o calcolate dallo stato dell'app.

Il codice sopra riportato cambia l'immagine se viene modificato il poster del film.

E questo avviene effettivamente con una sola riga, tramite la quale si collegano le viste allo stato. Se hai lavorato con ReSwift su iOS o addirittura connect con React, capirai qual è la magia di SwiftUI.

Ora puoi provare ad attivare l'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 nella lista di mostrare il comune swipe iOS per la rimozione. Quindi, quando l'utente tocca il pulsante di rimozione, viene avviata l'azione corrispondente e il film viene rimosso dalla lista.

Poiché la proprietà della lista è derivata dallo stato di BindableObject e iniettata come EnvironmentObject, SwiftUI aggiorna la lista 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ò nei dettagli su come SwiftUI sappia davvero cosa visualizzare. Per comprenderlo più a fondo, vale la pena dare un'occhiata alla sessione WWDC sul flusso di dati in SwiftUI. Viene anche spiegato in dettaglio perché e quando 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