Ανάπτυξη εφαρμογών στο SwiftUI. Μέρος 1: Ροή δεδομένων και Redux

Ανάπτυξη εφαρμογών στο SwiftUI. Μέρος 1: Ροή δεδομένων και Redux

Αφού παρακολούθησα τη συνεδρία State of the Union στο WWDC 2019, αποφάσισα να κάνω μια βαθιά βουτιά στο SwiftUI. Έχω ξοδέψει πολύ χρόνο δουλεύοντας με αυτό και τώρα άρχισα να αναπτύσσω μια πραγματική εφαρμογή που μπορεί να είναι χρήσιμη σε ένα ευρύ φάσμα χρηστών.

Το ονόμασα MovieSwiftUI - αυτή είναι μια εφαρμογή για την αναζήτηση νέων και παλιών ταινιών, καθώς και τη συλλογή τους σε μια συλλογή χρησιμοποιώντας TMDB API. Πάντα μου άρεσαν οι ταινίες και μάλιστα δημιούργησα μια εταιρεία που εργάζεται σε αυτόν τον τομέα, αν και εδώ και πολύ καιρό. Η εταιρεία δύσκολα θα μπορούσε να χαρακτηριστεί cool, αλλά η εφαρμογή ήταν!

Υπενθύμιση: για όλους τους αναγνώστες του "Habr" - έκπτωση 10 ρούβλια κατά την εγγραφή σε οποιοδήποτε μάθημα Skillbox χρησιμοποιώντας τον κωδικό προσφοράς "Habr".

Το Skillbox προτείνει: Εκπαιδευτικό διαδικτυακό μάθημα "Επάγγελμα προγραμματιστή Java".

Τι μπορεί λοιπόν να κάνει το MovieSwiftUI;

  • Αλληλεπιδρά με το API - σχεδόν κάθε σύγχρονη εφαρμογή το κάνει αυτό.
  • Φορτώνει ασύγχρονα δεδομένα σε αιτήματα και αναλύει το JSON στο μοντέλο Swift χρησιμοποιώντας Κωδικοποιήσιμο.
  • Εμφανίζει εικόνες που έχουν φορτωθεί κατόπιν αιτήματος και τις αποθηκεύει στην κρυφή μνήμη.
  • Αυτή η εφαρμογή για iOS, iPadOS και macOS παρέχει το καλύτερο UX για τους χρήστες αυτών των λειτουργικών συστημάτων.
  • Ο χρήστης μπορεί να δημιουργήσει δεδομένα και να δημιουργήσει τις δικές του λίστες ταινιών. Η εφαρμογή αποθηκεύει και επαναφέρει δεδομένα χρήστη.
  • Οι προβολές, τα στοιχεία και τα μοντέλα διαχωρίζονται σαφώς χρησιμοποιώντας το μοτίβο Redux. Η ροή δεδομένων εδώ είναι μονής κατεύθυνσης. Μπορεί να αποθηκευτεί πλήρως στην προσωρινή μνήμη, να αποκατασταθεί και να αντικατασταθεί.
  • Η εφαρμογή χρησιμοποιεί τα βασικά στοιχεία των SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal κ.λπ. Παρέχει επίσης προσαρμοσμένες προβολές, χειρονομίες, UI/UX.

Ανάπτυξη εφαρμογών στο SwiftUI. Μέρος 1: Ροή δεδομένων και Redux
Στην πραγματικότητα, το κινούμενο σχέδιο είναι ομαλό, το GIF αποδείχθηκε λίγο σπασμωδικό

Η εργασία στην εφαρμογή μου έδωσε μεγάλη εμπειρία και συνολικά ήταν μια θετική εμπειρία. Μπόρεσα να γράψω μια πλήρως λειτουργική εφαρμογή, τον Σεπτέμβριο θα τη βελτιώσω και θα τη δημοσιεύσω στο AppStore, ταυτόχρονα με την κυκλοφορία του iOS 13.

Redux, BindableObject και EnvironmentObject

Ανάπτυξη εφαρμογών στο SwiftUI. Μέρος 1: Ροή δεδομένων και Redux

Δουλεύω με το Redux εδώ και περίπου δύο χρόνια, οπότε το γνωρίζω σχετικά καλά. Συγκεκριμένα, το χρησιμοποιώ στο frontend για Αντίδραση ιστοσελίδα, καθώς και για την ανάπτυξη εγγενών εφαρμογών iOS (Swift) και Android (Kotlin).

Ποτέ δεν μετάνιωσα που επέλεξα το Redux ως αρχιτεκτονική ροής δεδομένων για την κατασκευή μιας εφαρμογής SwiftUI. Τα πιο δύσκολα μέρη κατά τη χρήση του Redux σε μια εφαρμογή UIKit είναι η εργασία με το κατάστημα και η λήψη και η ανάκτηση δεδομένων και η αντιστοίχισή τους στις προβολές/εξαρτήματά σας. Για να γίνει αυτό, έπρεπε να δημιουργήσω ένα είδος βιβλιοθήκης συνδέσεων (χρησιμοποιώντας ReSwift και ReKotlin). Λειτουργεί καλά, αλλά πολύς κώδικας. Δυστυχώς, δεν είναι (ακόμη) ανοιχτού κώδικα.

Καλα ΝΕΑ! Τα μόνα πράγματα που πρέπει να ανησυχείτε με το SwiftUI - εάν σκοπεύετε να χρησιμοποιήσετε το Redux - είναι καταστήματα, πολιτείες και μειωτήρες. Η αλληλεπίδραση με το κατάστημα φροντίζει πλήρως το SwiftUI χάρη στο @EnvironmentObject. Έτσι, η αποθήκευση ξεκινά με ένα BindableObject.

Δημιούργησα ένα απλό πακέτο Swift, SwiftUIFlux, το οποίο παρέχει βασική χρήση του Redux. Στην περίπτωσή μου είναι μέρος του MovieSwiftUI. εγώ επίσης έγραψε ένα σεμινάριο βήμα προς βήμα, το οποίο θα σας βοηθήσει να χρησιμοποιήσετε αυτό το στοιχείο.

Πώς λειτουργεί;

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

Κάθε φορά που ενεργοποιείτε μια ενέργεια, ενεργοποιείτε το κιβώτιο ταχυτήτων. Θα αξιολογήσει τις ενέργειες σύμφωνα με την τρέχουσα κατάσταση της εφαρμογής. Στη συνέχεια, θα επιστρέψει μια νέα τροποποιημένη κατάσταση σύμφωνα με τον τύπο ενέργειας και τα δεδομένα.

Λοιπόν, δεδομένου ότι το κατάστημα είναι ένα BindableObject, θα ειδοποιήσει το SwiftUI όταν αλλάξει η τιμή του χρησιμοποιώντας την ιδιότητα willChange που παρέχεται από το PassthroughSubject. Αυτό συμβαίνει επειδή το BindableObject πρέπει να παρέχει έναν PublisherType, αλλά η υλοποίηση του πρωτοκόλλου είναι υπεύθυνη για τη διαχείρισή του. Συνολικά, αυτό είναι ένα πολύ ισχυρό εργαλείο από την Apple. Αντίστοιχα, στον επόμενο κύκλο απόδοσης, το SwiftUI θα βοηθήσει στην απόδοση του σώματος των προβολών σύμφωνα με την αλλαγή κατάστασης.

Στην πραγματικότητα, αυτή είναι όλη η καρδιά και η μαγεία του SwiftUI. Τώρα, σε οποιαδήποτε προβολή που εγγράφεται σε μια κατάσταση, η προβολή θα αποδίδεται σύμφωνα με τα δεδομένα που λαμβάνονται από την πολιτεία και τι έχει αλλάξει.

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

Το Store εισάγεται ως EnvironmentObject όταν ξεκινά η εφαρμογή και στη συνέχεια είναι προσβάσιμο σε οποιαδήποτε προβολή χρησιμοποιώντας το @EnvironmentObject. Δεν υπάρχει ποινή απόδοσης επειδή οι παραγόμενες ιδιότητες ανακτώνται γρήγορα ή υπολογίζονται από την κατάσταση εφαρμογής.

Ο παραπάνω κωδικός αλλάζει την εικόνα εάν αλλάξει η αφίσα της ταινίας.

Και αυτό στην πραγματικότητα γίνεται με μία μόνο γραμμή, με τη βοήθεια της οποίας οι απόψεις συνδέονται με το κράτος. Εάν έχετε εργαστεί με το ReSwift σε iOS ή ακόμα connect με το React, θα καταλάβετε τη μαγεία του SwiftUI.

Τώρα μπορείτε να δοκιμάσετε να ενεργοποιήσετε την ενέργεια και να δημοσιεύσετε τη νέα κατάσταση. Εδώ είναι ένα πιο σύνθετο παράδειγμα.

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

Στον παραπάνω κώδικα, χρησιμοποιώ την ενέργεια .onDelete από το SwiftUI για κάθε IP. Αυτό επιτρέπει στη σειρά στη λίστα να εμφανίζει το κανονικό σάρωση iOS για διαγραφή. Έτσι, όταν ο χρήστης αγγίξει το κουμπί διαγραφής, ενεργοποιεί την αντίστοιχη ενέργεια και αφαιρεί την ταινία από τη λίστα.

Λοιπόν, δεδομένου ότι η ιδιότητα λίστας προέρχεται από την κατάσταση BindableObject και εγχέεται ως EnvironmentObject, το SwiftUI ενημερώνει τη λίστα επειδή το ForEach συσχετίζεται με την ιδιότητα που υπολογίζεται από ταινίες.

Εδώ είναι μέρος του μειωτήρα MoviesState:

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
}

Ο μειωτής εκτελείται όταν αποστέλλετε μια ενέργεια και επιστρέφετε μια νέα κατάσταση, όπως αναφέρθηκε παραπάνω.

Δεν θα μπω σε λεπτομέρειες ακόμα - πώς το SwiftUI ξέρει πραγματικά τι να εμφανίζει. Για να το καταλάβετε αυτό πιο βαθιά, αξίζει προβολή συνεδρίας WWDC σχετικά με τη ροή δεδομένων στο SwiftUI. Εξηγεί επίσης λεπτομερώς γιατί και πότε να χρησιμοποιείται Κατάσταση, @Binding, ObjectBinding και EnvironmentObject.

Το Skillbox προτείνει:

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο