Anwendungsentwicklung auf SwiftUI. Teil 1: Datenfluss und Redux

Anwendungsentwicklung auf SwiftUI. Teil 1: Datenfluss und Redux

Nachdem ich an der „State of the Union“-Sitzung auf der WWDC 2019 teilgenommen hatte, beschloss ich, tiefer in SwiftUI einzutauchen. Ich habe viel Zeit damit verbracht, damit zu arbeiten, und habe nun damit begonnen, eine echte Anwendung zu entwickeln, die für ein breites Spektrum von Benutzern nützlich sein kann.

Ich habe es MovieSwiftUI genannt – das ist eine App zum Suchen nach neuen und alten Filmen sowie zum Sammeln dieser in einer Sammlung TMDB-API. Ich habe Filme schon immer geliebt und sogar eine Firma gegründet, die in diesem Bereich tätig ist, allerdings schon vor langer Zeit. Das Unternehmen kann man kaum als cool bezeichnen, die Bewerbung hingegen schon!

Erinnerung: für alle Leser von „Habr“ – ein Rabatt von 10 Rubel bei der Anmeldung zu einem beliebigen Skillbox-Kurs mit dem Aktionscode „Habr“.

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

Was kann MovieSwiftUI also tun?

  • Interagiert mit der API – fast jede moderne Anwendung tut dies.
  • Lädt asynchrone Daten zu Anfragen und analysiert JSON mithilfe von in das Swift-Modell codierbar.
  • Zeigt auf Anfrage geladene Bilder an und speichert sie zwischen.
  • Diese App für iOS, iPadOS und macOS bietet die beste UX für Benutzer dieser Betriebssysteme.
  • Der Benutzer kann Daten generieren und eigene Filmlisten erstellen. Die Anwendung speichert Benutzerdaten und stellt sie wieder her.
  • Ansichten, Komponenten und Modelle werden durch das Redux-Muster klar getrennt. Der Datenfluss ist hier unidirektional. Es kann vollständig zwischengespeichert, wiederhergestellt und überschrieben werden.
  • Die Anwendung verwendet die Grundkomponenten SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal usw. Es bietet auch benutzerdefinierte Ansichten, Gesten und UI/UX.

Anwendungsentwicklung auf SwiftUI. Teil 1: Datenfluss und Redux
Tatsächlich ist die Animation flüssig, das GIF ist etwas ruckelig geworden

Die Arbeit an der App hat mir viel Erfahrung gebracht und insgesamt war es eine positive Erfahrung. Ich konnte eine voll funktionsfähige Anwendung schreiben, im September werde ich sie verbessern und gleichzeitig mit der Veröffentlichung von iOS 13 im AppStore veröffentlichen.

Redux, BindableObject und EnvironmentObject

Anwendungsentwicklung auf SwiftUI. Teil 1: Datenfluss und Redux

Ich arbeite jetzt seit etwa zwei Jahren mit Redux und kenne mich daher relativ gut darin aus. Insbesondere verwende ich es im Frontend für Reagieren Website sowie für die Entwicklung nativer iOS- (Swift) und Android-Anwendungen (Kotlin).

Ich habe es nie bereut, Redux als Datenflussarchitektur für die Erstellung einer SwiftUI-Anwendung gewählt zu haben. Die schwierigsten Teile bei der Verwendung von Redux in einer UIKit-App sind die Arbeit mit dem Store und das Abrufen und Abrufen von Daten sowie deren Zuordnung zu Ihren Ansichten/Komponenten. Dazu musste ich eine Art Bibliothek von Konnektoren erstellen (mithilfe von ReSwift und ReKotlin). Funktioniert gut, aber ziemlich viel Code. Leider ist es (noch) nicht Open Source.

Gute Nachrichten! Die einzigen Dinge, über die Sie sich bei SwiftUI Sorgen machen müssen – wenn Sie Redux verwenden möchten – sind Stores, States und Reducer. Die Interaktion mit dem Store wird dank @EnvironmentObject vollständig von SwiftUI übernommen. Der Store beginnt also mit einem 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. ich auch habe eine Schritt-für-Schritt-Anleitung geschrieben, das Ihnen bei der Verwendung dieser Komponente hilft.

Wie funktioniert es?

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

Jedes Mal, wenn Sie eine Aktion auslösen, aktivieren Sie das Getriebe. Es bewertet die Aktionen entsprechend dem aktuellen Stand der Anwendung. Anschließend wird entsprechend dem Aktionstyp und den Daten ein neuer geänderter Status zurückgegeben.

Da es sich bei „store“ um ein BindableObject handelt, benachrichtigt es SwiftUI mithilfe der von PassthroughSubject bereitgestellten Eigenschaft „willChange“, wenn sich sein Wert ändert. Dies liegt daran, dass das BindableObject einen PublisherType bereitstellen muss, die Protokollimplementierung jedoch für die Verwaltung verantwortlich ist. Insgesamt handelt es sich um ein sehr leistungsfähiges Tool von Apple. Dementsprechend wird SwiftUI im nächsten Rendering-Zyklus dabei helfen, den Körper der Ansichten entsprechend der Statusänderung zu rendern.

Eigentlich ist das das ganze Herz und die Magie von SwiftUI. Jetzt wird in jeder Ansicht, die einen Status abonniert, die Ansicht entsprechend den vom Status empfangenen Daten und den Änderungen gerendert.

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

Der Store wird beim Start der Anwendung als EnvironmentObject eingefügt und ist dann in jeder Ansicht über @EnvironmentObject zugänglich. Es gibt keine Leistungseinbußen, da abgeleitete Eigenschaften schnell aus dem Anwendungsstatus abgerufen oder berechnet werden.

Der obige Code ändert das Bild, wenn sich das Filmplakat ändert.

Und das geschieht tatsächlich mit nur einer Zeile, mit deren Hilfe Ansichten mit dem Staat verbunden werden. Wenn Sie mit ReSwift unter iOS oder sogar gearbeitet haben Connect Mit React werden Sie die Magie von SwiftUI verstehen.

Jetzt können Sie versuchen, die Aktion zu aktivieren und den neuen Stand zu veröffentlichen. Hier ist ein komplexeres Beispiel.

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

Im obigen Code verwende ich für jede IP die Aktion .onDelete von SwiftUI. Dadurch kann die Zeile in der Liste mit dem normalen iOS-Wisch zum Löschen angezeigt werden. Wenn der Benutzer also die Schaltfläche „Löschen“ berührt, wird die entsprechende Aktion ausgelöst und der Film aus der Liste entfernt.

Da die Listeneigenschaft vom BindableObject-Status abgeleitet und als EnvironmentObject eingefügt wird, aktualisiert SwiftUI die Liste, da ForEach mit der berechneten Eigenschaft „Filme“ verknüpft ist.

Hier ist ein Teil des MoviesState-Reduzierers:

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
}

Der Reduzierer wird ausgeführt, wenn Sie eine Aktion auslösen und einen neuen Status zurückgeben, wie oben angegeben.

Ich werde noch nicht näher darauf eingehen, wie SwiftUI eigentlich weiß, was angezeigt werden soll. Es lohnt sich, dies tiefer zu verstehen Sehen Sie sich die WWDC-Sitzung zum Datenfluss an in SwiftUI. Außerdem wird ausführlich erklärt, warum und wann es verwendet werden sollte Bundesstaat, @Binding, ObjectBinding und EnvironmentObject.

Skillbox empfiehlt:

Source: habr.com

Kommentar hinzufügen