
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 . 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 .
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 .
- 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.

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

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 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, , który zapewnia podstawowe użycie Redux. W moim przypadku jest to część MovieSwiftUI. Dodatkowo, , 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 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 w SwiftUI. Tam również szczegółowo omawia się, dlaczego i kiedy używać , @Binding, ObjectBinding i EnvironmentObject.
Skillbox poleca:
- Praktyczny kurs .
- Kurs online .
- Dwuletni praktyczny kurs .
Źródło: habr.com
