Tworzenie aplikacji na SwiftUI. Część 1: Przepływ danych i redux

Tworzenie aplikacji na SwiftUI. Część 1: Przepływ danych i redux

Po wzięciu udziału w sesji o stanie Unii podczas WWDC 2019 postanowiłem głębiej zagłębić się w SwiftUI. Spędziłem z nim dużo czasu, a teraz zacząłem tworzyć prawdziwą aplikację, która może być użyteczna dla szerokiego grona użytkowników.

Nazwałem go MovieSwiftUI – jest to aplikacja służąca do wyszukiwania nowych i starych filmów oraz gromadzenia ich w kolekcji za pomocą API TMDB. Zawsze kochałem filmy i nawet założyłem firmę działającą w tej branży, choć dawno temu. Firmę trudno nazwać fajną, ale aplikacja była!

Przypomnienie: dla wszystkich czytelników „Habr” - rabat w wysokości 10 000 rubli przy zapisywaniu się na dowolny kurs Skillbox przy użyciu kodu promocyjnego „Habr”.

Skillbox poleca: Kurs edukacyjny on-line „Zawód programista Java”.

Co zatem potrafi MovieSwiftUI?

  • Współpracuje z API - robi to prawie każda nowoczesna aplikacja.
  • Ładuje asynchroniczne dane na żądaniach i analizuje JSON do modelu Swift za pomocą Kodowalne.
  • Pokazuje obrazy załadowane na żądanie i buforuje je.
  • Ta aplikacja na iOS, iPadOS i macOS zapewnia najlepszy UX dla użytkowników tych systemów operacyjnych.
  • Użytkownik może generować dane i tworzyć własne listy filmów. Aplikacja zapisuje i przywraca dane użytkownika.
  • Widoki, komponenty i modele są wyraźnie oddzielone za pomocą wzorca Redux. Przepływ danych jest tu jednokierunkowy. Można go w pełni buforować, przywracać i nadpisywać.
  • Aplikacja wykorzystuje podstawowe komponenty SwiftUI, TaishedView, SegmentedControl, NawigacjaView, Form, Modal itp. Zapewnia także niestandardowe widoki, gesty, interfejs użytkownika/UX.

Tworzenie aplikacji na SwiftUI. Część 1: Przepływ danych i redux
W rzeczywistości animacja jest płynna, GIF okazał się trochę nierówny

Praca nad aplikacją dała mi dużo doświadczenia i ogólnie było to pozytywne doświadczenie. Udało mi się napisać w pełni funkcjonalną aplikację, we wrześniu ją udoskonalę i opublikuję w AppStore, jednocześnie z wydaniem iOS 13.

Redux, BindableObject i EnvironmentObject

Tworzenie aplikacji na SwiftUI. Część 1: Przepływ danych i redux

Pracuję z Redux już od około dwóch lat, więc jestem w nim stosunkowo dobrze zorientowany. W szczególności używam go w interfejsie do React serwisu internetowego, a także do tworzenia natywnych aplikacji na iOS (Swift) i Android (Kotlin).

Nigdy nie żałowałem, że wybrałem Redux jako architekturę przepływu danych do budowy aplikacji SwiftUI. Najtrudniejsze części podczas korzystania z Redux w aplikacji UIKit to praca ze sklepem oraz pobieranie i pobieranie danych oraz mapowanie ich na widoki/komponenty. Aby tego dokonać musiałem stworzyć swego rodzaju bibliotekę konektorów (za pomocą ReSwift i ReKotlin). Działa dobrze, ale dość dużo kodu. Niestety nie jest to (jeszcze) open source.

Dobre wieści! Jedyne, o co należy się martwić w SwiftUI – jeśli planujesz używać Redux – to sklepy, stany i reduktory. Interakcją ze sklepem w całości zajmuje się SwiftUI dzięki @EnvironmentObject. Zatem sklep zaczyna się od obiektu BindableObject.

Stworzyłem prosty pakiet Swift, SwiftUIFlux, który zapewnia podstawowe użycie Redux. W moim przypadku jest to część MovieSwiftUI. ja również napisał tutorial krok po kroku, co pomoże Ci korzystać z tego komponentu.

Jak to działa?

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

Za każdym razem, gdy uruchomisz akcję, aktywujesz skrzynię biegów. Oceni działania zgodnie z bieżącym stanem aplikacji. Następnie zwróci nowy zmodyfikowany stan zgodnie z typem akcji i danymi.

Cóż, ponieważ store jest obiektem BindableObject, powiadomi SwiftUI, gdy jego wartość ulegnie zmianie, korzystając z właściwości willChange dostarczonej przez PassthroughSubject. Dzieje się tak, ponieważ BindableObject musi udostępniać PublisherType, ale za zarządzanie nim odpowiedzialna jest implementacja protokołu. Ogólnie rzecz biorąc, jest to bardzo potężne narzędzie firmy Apple. W związku z tym w następnym cyklu renderowania SwiftUI pomoże wyrenderować treść widoków zgodnie ze zmianą stanu.

Właściwie to jest całe serce i magia SwiftUI. Teraz w dowolnym widoku subskrybującym stan widok będzie renderowany zgodnie z danymi otrzymanymi od stanu i zmianami.

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

Sklep jest wstrzykiwany jako obiekt EnvironmentObject podczas uruchamiania aplikacji i jest następnie dostępny w dowolnym widoku za pomocą @EnvironmentObject. Nie ma żadnego pogorszenia wydajności, ponieważ właściwości pochodne są szybko pobierane lub obliczane na podstawie stanu aplikacji.

Powyższy kod zmienia obraz w przypadku zmiany plakatu filmowego.

I właściwie odbywa się to za pomocą tylko jednej linii, za pomocą której widoki są powiązane ze stanem. Jeśli pracowałeś z ReSwift na iOS lub nawet connect dzięki React zrozumiesz magię SwiftUI.

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

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

W powyższym kodzie używam akcji .onDelete z SwiftUI dla każdego adresu IP. Dzięki temu wiersz na liście może wyświetlić zwykłe przesunięcie w systemie iOS w celu usunięcia. Kiedy więc użytkownik dotknie przycisku usuwania, uruchamia odpowiednią akcję i usuwa film z listy.

Cóż, ponieważ właściwość list wywodzi się ze stanu BindableObject i jest wstrzykiwana jako obiekt EnvironmentObject, SwiftUI aktualizuje listę, ponieważ ForEach jest powiązany z obliczoną właściwością filmów.

Oto część 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 jest wykonywany, gdy wysyłasz akcję i zwracasz nowy stan, jak opisano powyżej.

Nie będę jeszcze wdawał się w szczegóły – skąd SwiftUI faktycznie wie, co wyświetlić. Aby to głębiej zrozumieć, warto zobacz sesję WWDC dotyczącą przepływu danych w SwiftUI. Wyjaśnia również szczegółowo, dlaczego i kiedy stosować Stan, @Binding, ObjectBinding i EnvironmentObject.

Skillbox poleca:

Źródło: www.habr.com

Dodaj komentarz