Vývoj aplikací na SwiftUI. Část 1: Datový tok a redux

Vývoj aplikací na SwiftUI. Část 1: Datový tok a redux

Poté, co jsem se zúčastnil zasedání State of the Union na WWDC 2019, rozhodl jsem se hluboce ponořit do SwiftUI. Strávil jsem s ním spoustu času a nyní jsem začal vyvíjet skutečnou aplikaci, která může být užitečná pro široké spektrum uživatelů.

Nazval jsem to MovieSwiftUI - je to aplikace pro vyhledávání nových a starých filmů a jejich shromažďování do sbírky pomocí TMDB API. Vždycky jsem miloval filmy a dokonce jsem založil společnost pracující v této oblasti, i když už dávno. Společnost se sotva dala nazvat cool, ale aplikace ano!

Připomínáme: pro všechny čtenáře "Habr" - sleva 10 000 rublů při zápisu do jakéhokoli kurzu Skillbox pomocí propagačního kódu "Habr".

Skillbox doporučuje: Vzdělávací online kurz "Profese Java Developer".

Co tedy MovieSwiftUI umí?

  • Interaguje s API – to dělá téměř každá moderní aplikace.
  • Načte asynchronní data o požadavcích a analyzuje JSON do modelu Swift pomocí Kódovatelné.
  • Zobrazuje obrázky načtené na vyžádání a ukládá je do mezipaměti.
  • Tato aplikace pro iOS, iPadOS a macOS poskytuje uživatelům těchto OS to nejlepší uživatelské rozhraní.
  • Uživatel může generovat data a vytvářet vlastní seznamy filmů. Aplikace ukládá a obnovuje uživatelská data.
  • Pohledy, komponenty a modely jsou jasně odděleny pomocí vzoru Redux. Datový tok je zde jednosměrný. Lze jej plně uložit do mezipaměti, obnovit a přepsat.
  • Aplikace využívá základní komponenty SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal atd. Poskytuje také vlastní pohledy, gesta, UI/UX.

Vývoj aplikací na SwiftUI. Část 1: Datový tok a redux
Ve skutečnosti je animace plynulá, GIF se ukázal trochu trhaný

Práce na aplikaci mi dala spoustu zkušeností a celkově to byla pozitivní zkušenost. Podařilo se mi napsat plně funkční aplikaci, v září ji současně s vydáním iOS 13 vylepším a zveřejním v AppStore.

Redux, BindableObject a EnvironmentObject

Vývoj aplikací na SwiftUI. Část 1: Datový tok a redux

S Reduxem pracuji už asi dva roky, takže se v něm poměrně dobře orientuji. Konkrétně to používám ve frontendu pro REACT webové stránky a také pro vývoj nativních aplikací pro iOS (Swift) a Android (Kotlin).

Nikdy jsem nelitoval, že jsem si vybral Redux jako architekturu datového toku pro vytvoření aplikace SwiftUI. Nejnáročnější částí při používání Redux v aplikaci UIKit je práce s obchodem a získávání a načítání dat a jejich mapování na vaše pohledy/komponenty. K tomu jsem musel vytvořit jakousi knihovnu konektorů (pomocí ReSwift a ReKotlin). Funguje dobře, ale má hodně kódu. Bohužel to (zatím) není open source.

Dobré zprávy! Jediné, čeho se musíte se SwiftUI obávat – pokud plánujete používat Redux – jsou obchody, stavy a redukce. O interakci s obchodem se kompletně stará SwiftUI díky @EnvironmentObject. Store tedy začíná BindableObject.

Vytvořil jsem jednoduchý balíček Swift, SwiftUIFlux, který poskytuje základní použití Redux. V mém případě je to součást MovieSwiftUI. Já také napsal návod krok za krokem, který vám pomůže tuto komponentu používat.

Jak to funguje?

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

Pokaždé, když spustíte akci, aktivujete převodovku. Bude vyhodnocovat akce podle aktuálního stavu aplikace. Poté vrátí nový upravený stav v souladu s typem akce a daty.

Protože store je BindableObject, upozorní SwiftUI, když se jeho hodnota změní, pomocí vlastnosti willChange poskytované PassthroughSubject. Důvodem je, že BindableObject musí poskytovat PublisherType, ale za jeho správu je zodpovědná implementace protokolu. Celkově se jedná o velmi silný nástroj od Applu. Podle toho v dalším vykreslovacím cyklu SwiftUI pomůže vykreslit tělo pohledů podle změny stavu.

Ve skutečnosti je to celé srdce a kouzlo SwiftUI. Nyní, v každém pohledu, který se přihlásí ke stavu, bude pohled vykreslen podle toho, jaká data jsou přijata ze stavu a co se změnilo.

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

Store je vložen jako EnvironmentObject při spuštění aplikace a je pak přístupný v jakémkoli zobrazení pomocí @EnvironmentObject. Nedochází k žádnému snížení výkonu, protože odvozené vlastnosti se rychle načítají nebo vypočítají ze stavu aplikace.

Výše uvedený kód změní obrázek, pokud se změní plakát filmu.

A to se děje vlastně jen jednou čárou, pomocí které jsou pohledy propojeny se státem. Pokud jste pracovali s ReSwift na iOS nebo dokonce připojit s Reactem pochopíte kouzlo SwiftUI.

Nyní můžete zkusit aktivovat akci a publikovat nový stav. Zde je složitější příklad.

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

Ve výše uvedeném kódu používám pro každou IP akci .onDelete ze SwiftUI. To umožňuje, aby se řádek v seznamu zobrazil normálním přejetím prstem v systému iOS, který chcete odstranit. Když se tedy uživatel dotkne tlačítka smazat, spustí odpovídající akci a odstraní film ze seznamu.

Protože vlastnost list je odvozena ze stavu BindableObject a je vložena jako EnvironmentObject, SwiftUI aktualizuje seznam, protože ForEach je spojen s vypočítanou vlastností filmů.

Zde je část redukce 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
}

Redukce se provede, když odešlete akci a vrátíte nový stav, jak je uvedeno výše.

Nebudu se zatím rozepisovat – jak vlastně SwiftUI ví, co má zobrazit. Chcete-li tomu porozumět hlouběji, stojí za to zobrazit relaci WWDC o datovém toku ve SwiftUI. Také podrobně vysvětluje, proč a kdy používat Stát, @Binding, ObjectBinding a EnvironmentObject.

Skillbox doporučuje:

Zdroj: www.habr.com

Přidat komentář