App-Entwicklung mit SwiftUI. Teil 1: Datenfluss und Redux

App-Entwicklung mit SwiftUI. Teil 1: Datenfluss und Redux

Nach der Teilnahme an der State of the Union-Sitzung auf der WWDC 2019 beschloss ich, mich intensiver mit SwiftUI zu beschäftigen. Ich habe viel Zeit damit verbracht und beginne nun mit der Entwicklung einer echten Anwendung, die einer breiten Nutzergruppe nützlich sein könnte.

Ich habe sie MovieSwiftUI genannt – eine App zur Suche nach aktuellen sowie klassischen Filmen und zum Sammeln dieser in einer Sammlung mit Hilfe von TMDB API. Ich habe schon immer Filme geliebt und sogar ein Unternehmen in dieser Branche gegründet, jedoch ist das schon lange her. Das Unternehmen war schwer als cool zu bezeichnen, aber die App – definitiv!

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-Bildungskurs Beruf: Java-Entwickler.

Also, was kann MovieSwiftUI?

  • Es interagiert mit APIs – das tun fast alle modernen Anwendungen.
  • Ruft asynchrone Daten anhand von Anfragen ab und parst JSON in Swift-Modelle, indem es Codable.
  • verwendet. Es zeigt Bilder an, die upon request geladen werden, und speichert sie im Cache.
  • Diese App für iOS, iPadOS und macOS bietet die beste Benutzererfahrung für Nutzer dieser Betriebssysteme.
  • Der Nutzer kann Daten generieren und eigene Filmlisten erstellen. Die App speichert und stellt die Benutzerdaten wieder her.
  • Darstellungen, Komponenten und Modelle sind klar durch das Redux-Muster getrennt. Der Datenfluss ist hier unidirektional. Er kann vollständig zwischengespeichert, wiederhergestellt und überschrieben werden.
  • Die Anwendung verwendet grundlegende SwiftUI-Komponenten wie TabbedView, SegmentedControl, NavigationView, Form, Modal usw. Außerdem bietet sie benutzerdefinierte Ansichten, Gesten sowie UI/UX.

App-Entwicklung mit SwiftUI. Teil 1: Datenfluss und Redux
Die Animation ist tatsächlich flüssig, das GIF ist etwas ruckartig.

Die Arbeit an der Anwendung hat mir viel Erfahrung gebracht, insgesamt war 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 der Veröffentlichung von iOS 13.

Redux, BindableObject und EnvironmentObject

App-Entwicklung mit SwiftUI. Teil 1: Datenfluss und Redux

Ich arbeite nun seit etwa zwei Jahren mit Redux, sodass ich mich relativ gut damit auskenne. Insbesondere benutze ich es im Frontend für React die Website sowie zur Entwicklung nativer iOS (Swift) und Android (Kotlin) Anwendungen.

Ich habe meine Entscheidung für Redux als Datenflussarchitektur zur Erstellung einer Anwendung in SwiftUI nie bereut. Die kompliziertesten Aspekte bei der Verwendung von Redux in einer UIKit-Anwendung sind die Arbeit mit dem Store sowie das Abrufen und Verarbeiten von Daten und deren Zuordnung zu Ihren Ansichten / Komponenten. Zu diesem Zweck musste ich eine Art von Connector-Bibliothek (basierend auf ReSwift und ReKotlin) erstellen. Es funktioniert gut, aber es erfordert ziemlich viel Code. Leider ist es (bis jetzt) nicht Open Source.

Gute Nachrichten! Das Einzige, worüber man sich mit SwiftUI Gedanken machen sollte – sofern man plant, Redux zu verwenden – ist der Store, der Zustand und die Reducer. SwiftUI übernimmt die Interaktion mit dem Store vollständig über @EnvironmentObject. Somit beginnt der Store mit BindableObject.

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

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. Er bewertet die Aktionen basierend auf dem aktuellen Zustand der Anwendung. Anschließend gibt er einen neuen modifizierten Zustand zurück, der sich nach der Art der Aktion und den Daten richtet.

Da das Store ein BindableObject ist, wird es SwiftUI über die Änderung seines Wertes informieren, indem es die willChange-Eigenschaft nutzt, die von PassthroughSubject bereitgestellt wird. Dies ist notwendig, da ein BindableObject einen PublisherType bereitstellen muss, die Implementierung des Protokolls jedoch für die Verwaltung verantwortlich ist. Insgesamt ist dies ein sehr leistungsfähiges Werkzeug von Apple. Im nächsten Rendering-Zyklus hilft SwiftUI dann, den Inhalt der Ansichten entsprechend der Änderung des Zustands darzustellen.

Das ist also die Essenz und die Magie von SwiftUI. Jetzt wird in jeder Ansicht, die auf den Zustand abonniert ist, die Darstellung entsprechend den Daten, die aus dem Zustand abgerufen werden, und den Änderungen erfolgen.

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 integriert und ist dann in jeder Ansicht über @EnvironmentObject verfügbar. Die Leistung bleibt unberührt, da abgeleitete Eigenschaften schnell aus dem Anwendungszustand extrahiert 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 Verbindung der Ansichten mit dem Zustand herstellt. Wenn Sie mit ReSwift auf iOS oder sogar verbinden mit React gearbeitet haben, werden Sie die Magie von SwiftUI verstehen.

Jetzt können Sie versuchen, die Aktion zu aktivieren und einen neuen Status 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 Aktion .onDelete aus SwiftUI für jede IP. Dies ermöglicht es der Zeile in der Liste, die gewohnte iOS-Wischbewegung zum Löschen anzuzeigen. Wenn der Benutzer auf die Löschtaste tippt, wird die entsprechende Aktion ausgelöst und der Film aus der Liste entfernt.

Da die Listen-Eigenschaft von BindableObject abgeleitet ist und als EnvironmentObject injiziert 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 den neuen Zustand zurückgeben, wie oben beschrieben.

Ich werde noch nicht ins Detail gehen – woher SwiftUI tatsächlich weiß, was angezeigt werden soll. Um dies tiefer zu verstehen, sollten Sie die WWDC-Sitzung über Datenströme in SwiftUI ansehen. Dort wird auch ausführlich erklärt, warum und wann man State, @Binding, ObjectBinding und EnvironmentObject verwenden sollte.

Skillbox empfiehlt:

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster