Entwicklung einer Anwendung mit SwiftUI. Teil 1: Datenstrom und Redux

Entwicklung einer Anwendung mit SwiftUI. Teil 1: Datenstrom und Redux

Nach der Teilnahme an der State of the Union-Sitzung auf der WWDC 2019 entschied ich mich, SwiftUI intensiv zu studieren. Ich habe viel Zeit damit verbracht, daran zu arbeiten, und nun habe ich mit der Entwicklung einer echten Anwendung begonnen, die für eine breite Nutzergruppe nützlich sein könnte.

Ich nannte sie MovieSwiftUI – eine App zur Suche nach neuen und alten Filmen sowie deren Sammlung in einer Kollektion mit Hilfe von TMDB API. Ich habe Filme schon immer geliebt und sogar ein Unternehmen gegründet, das in diesem Bereich tätig war, allerdings schon vor einer Weile. Es war schwierig, das Unternehmen als cool zu bezeichnen, aber die App — ja!

Wir erinnern daran: alle Leser von „Habr“ erhalten einen Rabatt von 10.000 Rubel bei der Anmeldung zu einem beliebigen Kurs von Skillbox mit dem Aktionscode „Habr“.

Skillbox empfiehlt: Online-Kurs „Beruf Java-Entwickler“.

Also, was kann MovieSwiftUI?

  • Es interagiert mit APIs – das macht fast jede moderne Anwendung.
  • Es lädt asynchrone Daten über Abfragen herunter und parst JSON in Swift-Modelle unter Verwendung von Codable.
  • Es zeigt Bilder, die auf Anfrage heruntergeladen werden, und cached sie.
  • Diese Anwendung für iOS, iPadOS und macOS bietet das beste UX für die Nutzer dieser Betriebssysteme.
  • Der Benutzer kann Daten generieren und eigene Film-Listen erstellen. Die Anwendung speichert und stellt benutzerspezifische Daten wieder her.
  • Ansichten, Komponenten und Modelle sind klar durch das Redux-Muster getrennt. Der Datenstrom ist hier unidirektional. Er kann vollständig zwischengespeichert, wiederhergestellt und überschrieben werden.
  • Die Anwendung nutzt grundlegende SwiftUI-Komponenten, wie TabbedView, SegmentedControl, NavigationView, Form, Modal usw. Außerdem bietet sie benutzerdefinierte Ansichten, Gesten, UI/UX.

Entwicklung einer Anwendung mit SwiftUI. Teil 1: Datenstrom und Redux
Tatsächlich ist die Animation flüssig, das GIF ist etwas ruckartig.

Die Arbeit an der Anwendung hat mir viel Erfahrung gebracht, und insgesamt ist es eine positive Erfahrung. Ich konnte eine voll funktionsfähige Anwendung schreiben, die ich im September verbessern und im App Store veröffentlichen werde, zeitgleich mit dem Release von iOS 13.

Redux, BindableObject und EnvironmentObject

Entwicklung einer Anwendung mit SwiftUI. Teil 1: Datenstrom und Redux

Momentan arbeite ich seit etwa zwei Jahren mit Redux, also kenne ich mich relativ gut damit aus. Insbesondere verwende ich es im Frontend für React Webseiten sowie für die Entwicklung von nativen iOS (Swift) und Android (Kotlin) Anwendungen.

Ich habe nie bereut, Redux als Architektur für den Datenfluss bei der Erstellung einer Anwendung mit SwiftUI gewählt zu haben. Die schwierigsten Momente bei der Verwendung von Redux in einer UIKit-Anwendung sind die Arbeit mit dem Store sowie das Abrufen und Extrahieren von Daten und deren Zuordnung zu Ihren Views / Komponenten. Dafür musste ich eine Art Bibliothek von Connectoren (auf Basis von ReSwift und ReKotlin) erstellen. Funktioniert gut, aber es ist ziemlich viel Code. Leider ist er (noch) nicht Open Source.

Gute Nachrichten! Das einzige, worüber man sich mit SwiftUI Gedanken machen sollte, wenn man plant, Redux zu verwenden, sind der Store, der Zustand und die Reducer. SwiftUI kümmert sich vollständig um die Interaktion mit dem Store dank @EnvironmentObject. Der Store beginnt also mit BindableObject.

Ich habe ein einfaches Swift-Paket erstellt, SwiftUIFlux, das die grundlegende Verwendung von Redux ermöglicht. In meinem Fall ist es Teil von MovieSwiftUI. Außerdem habe ich ein schrittweises Tutorial geschrieben, das hilft, diese Komponente zu verwenden.

Wie funktioniert das?

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

Jedes Mal, wenn Sie eine Aktion ausführen, aktivieren Sie den Reducer. Dieser bewertet die Aktionen basierend auf dem aktuellen Zustand der Anwendung. Anschließend gibt er einen neuen modifizierten Zustand gemäß dem Aktionstyp und den Daten zurück.

Da der Store ein BindableObject ist, wird er SwiftUI über die Änderung seines Wertes benachrichtigen, indem er die willChange-Eigenschaft verwendet, die von PassthroughSubject bereitgestellt wird. Das liegt daran, dass ein BindableObject einen PublisherType bereitstellen muss, die Implementierung des Protokolls jedoch für dessen Verwaltung verantwortlich ist. Insgesamt ist es ein sehr leistungsfähiges Werkzeug von Apple. Folglich wird SwiftUI im nächsten Renderzyklus helfen, den Body der Views entsprechend der Zustandsänderung darzustellen.

Das ist im Wesentlichen das Herz und die Magie von SwiftUI. In jeder View, die auf den Zustand abonniert ist, wird die View entsprechend den Daten, die aus dem Zustand abgerufen werden, und was sich geändert hat, angezeigt.

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

Der Store wird beim Start der Anwendung als EnvironmentObject eingefügt und ist dann in jeder Ansicht über @EnvironmentObject verfügbar. Die Leistung wird nicht beeinträchtigt, da abgeleitete Eigenschaften schnell aus dem Anwendungszustand abgerufen oder berechnet werden.

Der oben angegebene Code ändert das Bild, wenn sich das Poster des Films ändert.

Und das geschieht tatsächlich mit nur einer Zeile, die die Ansichten mit dem Zustand verbindet. Wenn Sie mit ReSwift auf iOS oder sogar connect mit React gearbeitet haben, werden Sie den Zauber von SwiftUI verstehen.

Jetzt können Sie versuchen, eine Aktion auszulösen und einen neuen Zustand zu veröffentlichen. Hier ist ein komplizierteres Beispiel.

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

Im obigen Code verwende ich die .onDelete-Aktion von SwiftUI für jedes Element. Dies ermöglicht es einer Zeile in der Liste, das standardmäßige iOS-Wischverfahren zum Löschen anzuzeigen. Wenn der Benutzer also auf die Löschtaste tippt, wird die entsprechende Aktion ausgeführt und der Film wird aus der Liste entfernt.

Da die Eigenschaft der Liste vom BindableObject-Zustand abgeleitet und als EnvironmentObject eingefügt wird, aktualisiert SwiftUI die Liste, da ForEach mit der berechneten Eigenschaft movies verbunden ist.

Hier ist ein Teil des Reducers MoviesState:

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    
    // andere Aktionen.
    
    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
}

Der Reducer wird ausgeführt, wenn Sie eine Aktion senden und einen neuen Zustand zurückgeben, wie oben beschrieben.

Ich möchte nicht ins Detail gehen, woher SwiftUI tatsächlich weiß, was anzuzeigen ist. Um dies tiefer zu verstehen, ist es sinnvoll, die WWDC-Session über Datenströme in SwiftUI anzusehen. Dort wird auch ausführlich erklärt, warum und wann man State, @Binding, ObjectBinding und EnvironmentObject verwenden sollte.

Skillbox empfiehlt:

Quelle: habr.com

60GB SSD 8Gb DDR4