Aplikiĝdisvolviĝo sur SwiftUI. Parto 1: Datumfluo kaj Redux

Aplikiĝdisvolviĝo sur SwiftUI. Parto 1: Datumfluo kaj Redux

Post ĉeesto al la sesio de Ŝtato de la Unio ĉe WWDC 2019, mi decidis profunde plonĝi en SwiftUI. Mi pasigis multe da tempo laborante kun ĝi kaj nun komencis evoluigi veran aplikaĵon kiu povas esti utila al vasta gamo de uzantoj.

Mi nomis ĝin MovieSwiftUI - ĉi tio estas aplikaĵo por serĉi novajn kaj malnovajn filmojn, kaj ankaŭ kolekti ilin en kolekto uzante TMDB API. Mi ĉiam amis filmojn kaj eĉ kreis firmaon laborantan en ĉi tiu kampo, kvankam antaŭ longa tempo. La firmao apenaŭ povus esti nomita cool, sed la aplikaĵo estis!

Ni memorigas vin: por ĉiuj legantoj de "Habr" - rabato de 10 000 rubloj kiam oni enskribas en iu ajn Skillbox-kurso per la reklamkodo "Habr".

Skillbox rekomendas: Eduka interreta kurso "Profesia Java Programisto".

Do kion povas fari MovieSwiftUI?

  • Interagas kun la API - preskaŭ ajna moderna aplikaĵo faras tion.
  • Ŝarĝas nesinkronajn datumojn pri petoj kaj analizas JSON en la Swift-modelon uzante Kodebla.
  • Montras bildojn ŝarĝitajn laŭpeto kaj konservas ilin.
  • Ĉi tiu programo por iOS, iPadOS kaj macOS provizas la plej bonan UX por uzantoj de ĉi tiuj OS-oj.
  • La uzanto povas generi datumojn kaj krei siajn proprajn filmlistojn. La aplikaĵo konservas kaj restarigas datumojn de uzantoj.
  • Vidoj, komponantoj kaj modeloj estas klare apartigitaj per la Redux-ŝablono. La datumfluo ĉi tie estas unudirekta. Ĝi povas esti plene konservita, restaŭrita kaj anstataŭita.
  • La aplikaĵo uzas la bazajn komponantojn de SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal ktp. Ĝi ankaŭ provizas kutimajn vidojn, gestojn, UI/UX.

Aplikiĝdisvolviĝo sur SwiftUI. Parto 1: Datumfluo kaj Redux
Fakte, la animacio estas glata, la GIF rezultis iom saka

Labori pri la apo donis al mi multan sperton kaj ĝenerale ĝi estis pozitiva sperto. Mi povis skribi plene funkcian aplikaĵon, en septembro mi plibonigos ĝin kaj publikigos ĝin en la AppStore, samtempe kun la eldono de iOS 13.

Redux, BindableObject kaj EnvironmentObject

Aplikiĝdisvolviĝo sur SwiftUI. Parto 1: Datumfluo kaj Redux

Mi laboras kun Redux jam de proksimume du jaroj, do mi estas relative bone konita pri ĝi. Aparte, mi uzas ĝin en la fasado por Reagi retejo, same kiel por evoluigado de indiĝenaj iOS (Swift) kaj Android (Kotlin) aplikoj.

Mi neniam bedaŭris elekti Redux kiel la datumfluan arkitekturon por konstrui SwiftUI-aplikaĵon. La plej malfacilaj partoj kiam vi uzas Redux en UIKit-aplikaĵo estas labori kun la vendejo kaj akiri kaj preni datumojn kaj mapi ĝin al viaj vidoj/komponentoj. Por fari tion, mi devis krei specon de biblioteko de konektiloj (uzante ReSwift kaj ReKotlin). Funkcias bone, sed sufiĉe multe da kodo. Bedaŭrinde, ĝi ne estas (ankoraŭ) malferma fonto.

Bona novaĵo! La nuraj aferoj por zorgi pri SwiftUI - se vi planas uzi Redux - estas vendejoj, ŝtatoj kaj reduktiloj. Interago kun la vendejo estas tute zorgata de SwiftUI danke al @EnvironmentObject. Do, vendejo komenciĝas per BindableObject.

Mi kreis simplan Swift-pakaĵon, SwiftUIFlux, kiu provizas bazan uzadon de Redux. En mia kazo ĝi estas parto de MovieSwiftUI. Mi ankaŭ skribis paŝon post paŝo lernilon, kiu helpos vin uzi ĉi tiun komponanton.

Kiel ĝi funkcias?

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

Ĉiufoje kiam vi ekigas agon, vi aktivigas la rapidumujon. Ĝi taksos agojn laŭ la nuna stato de la aplikaĵo. Ĝi tiam resendos novan modifitan staton laŭ la ago-tipo kaj datumoj.

Nu, ĉar vendejo estas BindableObject, ĝi sciigos SwiftUI kiam ĝia valoro ŝanĝiĝas uzante la willChange-posedaĵon provizitan de PassthroughSubject. Ĉi tio estas ĉar la BindableObject devas disponigi PublisherType, sed la protokolo-efektivigo respondecas pri administrado de ĝi. Ĝenerale, ĉi tio estas tre potenca ilo de Apple. Sekve, en la sekva bildiga ciklo, SwiftUI helpos redoni la korpon de la vidoj laŭ la ŝtatŝanĝo.

Efektive, ĉi tio estas la tuta koro kaj magio de SwiftUI. Nun, en iu ajn vido kiu abonas ŝtaton, la vido estos farita laŭ kiaj datumoj estas ricevitaj de la ŝtato kaj kio ŝanĝiĝis.

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

La Vendejo estas injektita kiel EnvironmentObject kiam la aplikaĵo komenciĝas kaj tiam estas alirebla en ajna vido uzante @EnvironmentObject. Ekzistas neniu agadopuno ĉar derivitaj trajtoj estas rapide prenitaj aŭ kalkulitaj de aplikaĵoŝtato.

La supra kodo ŝanĝas la bildon se la filmafiŝo ŝanĝiĝas.

Kaj ĉi tio efektive estas farita per nur unu linio, kun la helpo de kiu vidoj estas konektitaj al la ŝtato. Se vi laboris kun ReSwift en iOS aŭ eĉ konekti kun React, vi komprenos la magion de SwiftUI.

Nun vi povas provi aktivigi la agon kaj publikigi la novan staton. Jen pli kompleksa ekzemplo.

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

En la supra kodo, mi uzas la agon .onDelete de SwiftUI por ĉiu IP. Ĉi tio permesas al la vico en la listo montri la normalan iOS-gliton por forigi. Do kiam la uzanto tuŝas la forigbutonon, ĝi ekigas la respondan agon kaj forigas la filmon el la listo.

Nu, ĉar la listposedaĵo estas derivita de la stato BindableObject kaj estas injektita kiel EnvironmentObject, SwiftUI ĝisdatigas la liston ĉar ForEach estas asociita kun la kalkulita posedaĵo de filmoj.

Jen parto de la reduktilo 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
}

La reduktilo estas ekzekutita kiam vi sendas agon kaj resendas novan staton, kiel dirite supre.

Mi ankoraŭ ne eniros en detalojn - kiel SwiftUI fakte scias kion montri. Por kompreni ĉi tion pli profunde, indas vidi WWDC-sesion pri datumfluo en SwiftUI. Ĝi ankaŭ klarigas detale kial kaj kiam uzi Ŝtato, @Binding, ObjectBinding kaj EnvironmentObject.

Skillbox rekomendas:

fonto: www.habr.com

Aldoni komenton