Applicatieontwikkeling op SwiftUI. Deel 1: Dataflow en Redux

Applicatieontwikkeling op SwiftUI. Deel 1: Dataflow en Redux

Na het bijwonen van de State of the Union-sessie op WWDC 2019 besloot ik een diepe duik te nemen in SwiftUI. Ik heb er veel tijd mee gewerkt en ben nu begonnen met het ontwikkelen van een echte applicatie die voor een breed scala aan gebruikers nuttig kan zijn.

Ik noemde het MovieSwiftUI - dit is een app voor het zoeken naar nieuwe en oude films, en voor het verzamelen ervan in een verzameling met behulp van TMDB-API. Ik heb altijd van films gehouden en heb zelfs een bedrijf opgericht dat zich op dit gebied richt, al is het lang geleden. Het bedrijf was nauwelijks cool te noemen, maar de applicatie wel!

Herinnering: voor alle lezers van "Habr" - een korting van 10 roebel bij inschrijving voor een Skillbox-cursus met behulp van de promotiecode "Habr".

Skillbox beveelt aan: Educatieve online cursus "Beroep Java-ontwikkelaar".

Dus wat kan MovieSwiftUI doen?

  • Werkt samen met de API - bijna elke moderne applicatie doet dit.
  • Laadt asynchrone gegevens over verzoeken en parseert JSON in het Swift-model met behulp van Codeerbaar.
  • Toont afbeeldingen die op verzoek zijn geladen en slaat ze op in de cache.
  • Deze app voor iOS, iPadOS en macOS biedt de beste UX voor gebruikers van deze besturingssystemen.
  • De gebruiker kan gegevens genereren en zijn eigen filmlijsten maken. De applicatie bewaart en herstelt gebruikersgegevens.
  • Aanzichten, componenten en modellen zijn duidelijk gescheiden met behulp van het Redux-patroon. De gegevensstroom is hier unidirectioneel. Het kan volledig in de cache worden opgeslagen, hersteld en overschreven.
  • De applicatie maakt gebruik van de basiscomponenten van SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. Het biedt ook aangepaste weergaven, gebaren en UI/UX.

Applicatieontwikkeling op SwiftUI. Deel 1: Dataflow en Redux
In feite is de animatie vloeiend, de GIF is een beetje schokkerig geworden

Het werken aan de app heeft mij veel ervaring opgeleverd en over het algemeen was het een positieve ervaring. Ik heb een volledig functionele applicatie kunnen schrijven, in september zal ik deze verbeteren en publiceren in de AppStore, gelijktijdig met de release van iOS 13.

Redux, BindableObject en EnvironmentObject

Applicatieontwikkeling op SwiftUI. Deel 1: Dataflow en Redux

Ik werk nu ongeveer twee jaar met Redux, dus ik ben er relatief goed in thuis. In het bijzonder gebruik ik het in de frontend voor Reageren website, evenals voor het ontwikkelen van native iOS (Swift) en Android (Kotlin) applicaties.

Ik heb er nooit spijt van gehad dat ik voor Redux heb gekozen als datastroomarchitectuur voor het bouwen van een SwiftUI-applicatie. De meest uitdagende onderdelen bij het gebruik van Redux in een UIKit-app zijn het werken met de winkel en het ophalen en ophalen van gegevens en het toewijzen ervan aan uw weergaven/componenten. Om dit te doen, moest ik een soort bibliotheek met connectoren maken (met behulp van ReSwift en ReKotlin). Werkt prima, maar wel veel code. Helaas is het (nog) niet open source.

Goed nieuws! Het enige waar u zich zorgen over hoeft te maken met SwiftUI (als u van plan bent Redux te gebruiken) zijn winkels, staten en verloopstukken. Interactie met de winkel wordt volledig verzorgd door SwiftUI dankzij @EnvironmentObject. Winkel begint dus met een BindableObject.

Ik heb een eenvoudig Swift-pakket gemaakt, SwiftUIFlux, dat basisgebruik van Redux biedt. In mijn geval maakt het deel uit van MovieSwiftUI. ik ook schreef een stap-voor-stap handleiding, waarmee u dit onderdeel kunt gebruiken.

Hoe werkt het?

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

Elke keer dat je een actie activeert, activeer je de versnellingsbak. Het evalueert acties op basis van de huidige status van de applicatie. Het zal dan een nieuwe gewijzigde status retourneren in overeenstemming met het actietype en de gegevens.

Omdat store een BindableObject is, zal het SwiftUI op de hoogte stellen wanneer de waarde ervan verandert met behulp van de willChange-eigenschap van PassthroughSubject. Dit komt omdat het BindableObject een PublisherType moet leveren, maar de protocolimplementatie verantwoordelijk is voor het beheer ervan. Over het algemeen is dit een zeer krachtig hulpmiddel van Apple. Dienovereenkomstig zal SwiftUI in de volgende weergavecyclus helpen de hoofdtekst van de weergaven weer te geven volgens de statuswijziging.

Eigenlijk is dit het hele hart en de magie van SwiftUI. Nu wordt in elke weergave die een staat onderschrijft, de weergave weergegeven op basis van welke gegevens van de staat zijn ontvangen en wat er is veranderd.

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

De Store wordt geïnjecteerd als een EnvironmentObject wanneer de toepassing start en is vervolgens in elke weergave toegankelijk met @EnvironmentObject. Er is geen prestatieverlies omdat afgeleide eigenschappen snel worden opgehaald of berekend op basis van de applicatiestatus.

De bovenstaande code verandert de afbeelding als de filmposter verandert.

En dit gebeurt eigenlijk met slechts één regel, met behulp waarvan opvattingen verbonden zijn met de staat. Als je met ReSwift op iOS hebt gewerkt of zelfs connect met React begrijp je de magie van SwiftUI.

Nu kunt u proberen de actie te activeren en de nieuwe status te publiceren. Hier is een complexer voorbeeld.

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

In de bovenstaande code gebruik ik voor elk IP-adres de actie .onDelete van SwiftUI. Hierdoor kan de rij in de lijst de normale iOS-veegbeweging weergeven om te verwijderen. Dus wanneer de gebruiker de verwijderknop aanraakt, wordt de bijbehorende actie geactiveerd en wordt de film uit de lijst verwijderd.

Omdat de eigenschap list is afgeleid van de staat BindableObject en wordt geïnjecteerd als een EnvironmentObject, werkt SwiftUI de lijst bij omdat ForEach is gekoppeld aan de berekende eigenschap van de film.

Hier is een deel van de MoviesState-verkleiner:

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
}

De reducer wordt uitgevoerd wanneer u een actie verzendt en een nieuwe staat retourneert, zoals hierboven vermeld.

Ik zal nog niet in detail treden: hoe SwiftUI eigenlijk weet wat hij moet weergeven. Om dit dieper te begrijpen, is het de moeite waard bekijk WWDC-sessie over gegevensstroom in SwiftUI. Het legt ook in detail uit waarom en wanneer te gebruiken Land, @Binding, ObjectBinding en EnvironmentObject.

Skillbox beveelt aan:

Bron: www.habr.com

Voeg een reactie