Rozwój aplikacji w SwiftUI. Część 1: przepływ danych i Redux

Rozwój aplikacji w SwiftUI. Część 1: przepływ danych i Redux

Po uczestnictwie w sesji State of the Union na WWDC 2019 postanowiłem szczegółowo zbadać SwiftUI. Spędziłem dużo czasu na pracy z nim i teraz przystąpiłem do opracowywania rzeczywistej aplikacji, która może okazać się użyteczna dla szerokiego grona użytkowników.

Nazłem ją MovieSwiftUI — to aplikacja do wyszukiwania nowych i starych filmów, a także ich zbierania w kolekcję przy pomocy TMDB API. Zawsze kochałem filmy i nawet założyłem firmę, która działa w tej branży, choć było to już dawno. Firmę trudno było nazwać atrakcyjną, ale aplikację — tak!

Przypominamy: dla wszystkich czytelników „Habra” — zniżka 10 000 rubli przy zapisie na dowolny kurs Skillbox z kodem promocyjnym „Habra”.

Skillbox poleca: Edukacyjny kurs online «Zawód programista Java».

A więc, co potrafi MovieSwiftUI?

  • Interaguje z API — to cecha niemal każdej nowoczesnej aplikacji.
  • Pobiera asynchroniczne dane na żądania i parsuje JSON do modeli Swift, używając Codable.
  • Wyświetla obrazy pobierane na żądanie i je buforuje.
  • Ta aplikacja dla iOS, iPadOS i macOS zapewnia najlepsze UX dla użytkowników tych systemów operacyjnych.
  • Użytkownik może generować dane, tworzyć własne listy filmów. Aplikacja zapisuje i przywraca dane użytkownika.
  • Widoki, komponenty i modele są jasno rozdzielone za pomocą wzorca Redux. Przepływ danych jest jednokierunkowy. Może być w pełni buforowany, przywracany i nadpisywany.
  • Aplikacja korzysta z podstawowych komponentów SwiftUI, takich jak TabbedView, SegmentedControl, NavigationView, Form, Modal itp. Zapewnia również niestandardowe widoki, gesty, UI/UX.

Rozwój aplikacji w SwiftUI. Część 1: przepływ danych i Redux
W rzeczywistości animacja jest płynna, gif wyszedł nieco szarpany.

Praca nad aplikacją dała mi dużo doświadczenia, a w ogóle było to pozytywne doświadczenie. Udało mi się napisać w pełni funkcjonalną aplikację, we wrześniu ją poprawię i umieszczę w AppStore, równocześnie z premierą iOS 13.

Redux, BindableObject i EnvironmentObject

Rozwój aplikacji w SwiftUI. Część 1: przepływ danych i Redux

Na chwilę obecną pracuję z Reduxem już od około dwóch lat, więc w miarę dobrze się w tym orientuję. W szczególności wykorzystuję go w frontendzie dla React strony internetowej, a także do rozwijania natywnych aplikacji iOS (Swift) i Android (Kotlin).

Nigdy nie żałowałem wyboru Redux jako architektury przepływu danych do tworzenia aplikacji w SwiftUI. Najbardziej złożonymi kwestiami przy używaniu Redux w aplikacji UIKit są praca ze store, a także pobieranie i wyodrębnianie danych oraz ich dopasowywanie do twoich widoków / komponentów. W tym celu musiałem stworzyć coś w rodzaju biblioteki łączników (na ReSwift i ReKotlin). Działa dobrze, ale jest dość dużo kodu. Niestety, (jak na razie) nie jest to open source.

Dobrze wiadomo! Jedyna rzecz, o którą warto się martwić przy korzystaniu z SwiftUI — jeśli planujesz używać Redux — to store, stany i reducery. Interakcja ze store jest całkowicie przejmowana przez SwiftUI dzięki @EnvironmentObject. Tak, store zaczyna się od BindableObject.

Stworzyłem prosty pakiet Swift, SwiftUIFlux, który zapewnia podstawowe użycie Redux. W moim przypadku jest to część MovieSwiftUI. Dodatkowo, napisałem krok po kroku tutorial, który pomoże w użyciu tego komponentu.

Jak to działa?

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

Za każdym razem, gdy uruchamiasz akcję, aktywujesz reduktor. Będzie on oceniać akcje zgodnie z aktualnym stanem aplikacji. Następnie zwróci nowe zmodyfikowane stany na podstawie rodzaju akcji i danych.

A ponieważ store jest BindableObject, będzie powiadamiać SwiftUI o zmianie swojego wartości, korzystając z właściwości willChange, dostarczonej przez PassthroughSubject. Dzieje się tak, ponieważ BindableObject musi dostarczać PublisherType, ale implementacja protokołu odpowiada za jego zarządzanie. Ogólnie, to bardzo potężne narzędzie od Apple. W związku z tym, w następnym cyklu renderowania SwiftUI pomoże wyświetlić treść widoków zgodnie z zmianą stanu.

Właściwie to wszystko — serce i magia SwiftUI. Teraz w każdym widoku, który subskrybuje stan, widok będzie wyświetlany zgodnie z danymi, które zostały otrzymane ze stanu i co się zmieniło.

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 jest wstrzykiwane jako EnvironmentObject przy uruchamianiu aplikacji, a następnie dostępne w każdym widoku za pomocą @EnvironmentObject. Wydajność nie spada, ponieważ właściwości pochodne są szybko pobierane lub obliczane ze stanu aplikacji.

Kod podany powyżej zmienia obraz, jeśli zmienia się plakat filmu.

I wszystko to odbywa się w zasadzie w jednej linii, która łączy widoki z stanem. Jeśli pracowałeś z ReSwift na iOS lub nawet połącz w React, zrozumiesz, na czym polega magia SwiftUI.

A teraz można spróbować aktywować akcję i opublikować nowy stan. Oto bardziej złożony przykład.

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

W powyższym kodzie używam akcji .onDelete z SwiftUI dla każdego IP. Umożliwia to wierszowi na liście wyświetlanie standardowego przesunięcia iOS do usunięcia. Dlatego, gdy użytkownik dotknie przycisku usunięcia, wywołuje odpowiednią akcję i usuwa film z listy.

Ponieważ właściwość listy jest pochodną od stanu BindableObject i jest wstrzykiwana jako EnvironmentObject, SwiftUI aktualizuje listę, ponieważ ForEach jest powiązany z obliczaną właściwością movies.

Oto część reduktora MoviesState:

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    
    // inne akcje.
    
    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 jest wywoływany, gdy wysyłasz akcję i zwraca nowe stanie, jak to już powiedziano powyżej.

Na razie nie zamierzam zagłębiać się w szczegóły, skąd tak naprawdę SwiftUI wie, co wyświetlić. Aby lepiej to zrozumieć, warto obejrzeć sesję WWDC na temat przepływu danych w SwiftUI. Tam również szczegółowo omawia się, dlaczego i kiedy używać State, @Binding, ObjectBinding i EnvironmentObject.

Skillbox poleca:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster