Razvoj aplikacija na SwiftUI. Dio 1: Protok podataka i Redux

Razvoj aplikacija na SwiftUI. Dio 1: Protok podataka i Redux

Nakon što sam prisustvovao sesiji o stanju u Uniji na WWDC 2019, odlučio sam da duboko zaronim u SwiftUI. Proveo sam dosta vremena radeći s njim i sada sam počeo da razvijam pravu aplikaciju koja može biti korisna širokom krugu korisnika.

Nazvao sam ga MovieSwiftUI - ovo je aplikacija za traženje novih i starih filmova, kao i njihovo prikupljanje u kolekciju pomoću TMDB API. Oduvijek sam volio filmove i čak sam osnovao kompaniju koja radi u ovoj oblasti, iako davno. Kompanija se teško može nazvati cool, ali aplikacija jeste!

Podsećamo: za sve čitaoce "Habra" - popust od 10 rubalja pri upisu na bilo koji Skillbox kurs koristeći "Habr" promotivni kod.

Skillbox preporučuje: Obrazovni online kurs "Profesija Java Developer".

Dakle, šta može MovieSwiftUI?

  • Interagira s API-jem - to radi gotovo svaka moderna aplikacija.
  • Učitava asinkrone podatke o zahtjevima i analizira JSON u Swift model koristeći Codable.
  • Prikazuje slike učitane na zahtjev i kešira ih.
  • Ova aplikacija za iOS, iPadOS i macOS pruža najbolji UX za korisnike ovih OS-a.
  • Korisnik može generirati podatke i kreirati vlastite liste filmova. Aplikacija sprema i vraća korisničke podatke.
  • Pogledi, komponente i modeli su jasno razdvojeni pomoću Redux obrasca. Tok podataka ovdje je jednosmjeran. Može se u potpunosti keširati, vratiti i prepisati.
  • Aplikacija koristi osnovne komponente SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, itd. Takođe pruža prilagođene poglede, pokrete, UI/UX.

Razvoj aplikacija na SwiftUI. Dio 1: Protok podataka i Redux
Zapravo, animacija je glatka, GIF je ispao malo trzav

Rad na aplikaciji dao mi je puno iskustva i općenito je to bilo pozitivno iskustvo. Uspio sam da napišem potpuno funkcionalnu aplikaciju, u septembru ću je poboljšati i objaviti u AppStore-u, istovremeno sa izlaskom iOS-a 13.

Redux, BindableObject i EnvironmentObject

Razvoj aplikacija na SwiftUI. Dio 1: Protok podataka i Redux

Sa Redux-om radim oko dvije godine, tako da sam relativno dobro upućen u to. Konkretno, koristim ga u frontendu za reagovati web stranicu, kao i za razvoj izvornih iOS (Swift) i Android (Kotlin) aplikacija.

Nikada nisam požalio što sam izabrao Redux kao arhitekturu protoka podataka za izgradnju SwiftUI aplikacije. Najzahtjevniji dijelovi pri korištenju Reduxa u aplikaciji UIKit su rad sa trgovinom i dobivanje i dohvaćanje podataka i njihovo mapiranje u vaše poglede/komponente. Da bih to uradio, morao sam da napravim neku vrstu biblioteke konektora (koristeći ReSwift i ReKotlin). Radi dobro, ali dosta koda. Nažalost, nije (još) open source.

Dobre vijesti! Jedine stvari o kojima treba brinuti sa SwiftUI - ako planirate koristiti Redux - su trgovine, stanja i reduktori. SwiftUI u potpunosti brine o interakciji sa trgovinom zahvaljujući @EnvironmentObject. Dakle, prodavnica počinje sa BindableObject.

Napravio sam jednostavan Swift paket, SwiftUIFlux, koji omogućava osnovnu upotrebu Reduxa. U mom slučaju to je dio MovieSwiftUI. i ja isto napisao korak po korak tutorijal, što će vam pomoći da koristite ovu komponentu.

Как это работает?

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

Svaki put kada pokrenete neku radnju, aktivirate mjenjač. On će procijeniti akcije prema trenutnom stanju aplikacije. Zatim će vratiti novo izmijenjeno stanje u skladu s tipom akcije i podacima.

Pa, pošto je store BindableObject, on će obavijestiti SwiftUI kada se njegova vrijednost promijeni pomoću svojstva willChange koje pruža PassthroughSubject. To je zato što BindableObject mora pružiti PublisherType, ali implementacija protokola je odgovorna za upravljanje njime. Sve u svemu, ovo je vrlo moćan Appleov alat. Shodno tome, u sljedećem ciklusu renderiranja, SwiftUI će pomoći u renderiranju tijela pogleda prema promjeni stanja.

Zapravo, ovo je srce i magija SwiftUI-ja. Sada, u bilo kom pogledu koji se pretplati na stanje, pogled će biti prikazan prema tome koji su podaci primljeni od stanja i šta se promenilo.

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

Prodavnica se ubrizgava kao EnvironmentObject kada se aplikacija pokrene, a zatim je dostupna u bilo kojem pogledu pomoću @EnvironmentObject. Ne postoji kazna za performanse jer se izvedena svojstva brzo dohvaćaju ili izračunavaju iz stanja aplikacije.

Kod iznad mijenja sliku ako se promijeni poster filma.

A to se zapravo radi samo jednom linijom, uz pomoć koje se pogledi povezuju sa državom. Ako ste radili sa ReSwiftom na iOS-u ili čak spojiti uz React, shvatit ćete magiju SwiftUI.

Sada možete pokušati aktivirati akciju i objaviti novo stanje. Evo složenijeg primjera.

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

U kodu iznad, koristim akciju .onDelete iz SwiftUI za svaku IP adresu. Ovo omogućava da red na listi prikazuje normalan iOS prevlačenje za brisanje. Dakle, kada korisnik dodirne dugme za brisanje, on pokreće odgovarajuću radnju i uklanja film sa liste.

Pa, pošto je svojstvo liste izvedeno iz stanja BindableObject i ubrizgano je kao EnvironmentObject, SwiftUI ažurira listu jer je ForEach povezan sa izračunatim svojstvom filmova.

Evo dijela reduktora 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
}

Reduktor se izvršava kada pošaljete akciju i vratite novo stanje, kao što je gore navedeno.

Neću još ulaziti u detalje – kako SwiftUI zapravo zna šta da prikaže. Da bismo ovo dublje razumjeli, vrijedi pogledajte WWDC sesiju o protoku podataka u SwiftUI. Također detaljno objašnjava zašto i kada koristiti stanje, @Binding, ObjectBinding i EnvironmentObject.

Skillbox preporučuje:

izvor: www.habr.com

Dodajte komentar