Desarrollo de aplicaciones en SwiftUI. Parte 1: flujo de datos y Redux

Desarrollo de aplicaciones en SwiftUI. Parte 1: flujo de datos y Redux

Después de asistir a la sesión sobre el Estado de la Unión en la WWDC 2019, decidí profundizar en SwiftUI. Pasé mucho tiempo trabajando con él y ahora comencé a desarrollar una aplicación real que puede ser útil para una amplia gama de usuarios.

Lo llamé MovieSwiftUI: es una aplicación para buscar películas nuevas y antiguas, así como para recopilarlas en una colección usando API de TMDB. Siempre me ha gustado el cine e incluso creé una empresa que trabaja en este campo, aunque hace mucho tiempo. La empresa difícilmente podría considerarse genial, ¡pero la aplicación sí lo era!

Recordamos: para todos los lectores de "Habr": un descuento de 10 rublos al inscribirse en cualquier curso de Skillbox utilizando el código promocional "Habr".

Skillbox recomienda: Curso educativo en línea "Profesión Desarrollador Java".

Entonces, ¿qué puede hacer MovieSwiftUI?

  • Interactúa con la API; casi cualquier aplicación moderna hace esto.
  • Carga datos asincrónicos en solicitudes y analiza JSON en el modelo Swift usando Codificable.
  • Muestra imágenes cargadas a pedido y las almacena en caché.
  • Esta aplicación para iOS, iPadOS y macOS proporciona la mejor UX para los usuarios de estos sistemas operativos.
  • El usuario puede generar datos y crear sus propias listas de películas. La aplicación guarda y restaura datos del usuario.
  • Las vistas, componentes y modelos están claramente separados mediante el patrón Redux. El flujo de datos aquí es unidireccional. Se puede almacenar en caché, restaurar y sobrescribir por completo.
  • La aplicación utiliza los componentes básicos de SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. También proporciona vistas personalizadas, gestos, UI/UX.

Desarrollo de aplicaciones en SwiftUI. Parte 1: flujo de datos y Redux
De hecho, la animación es fluida, el GIF resultó un poco entrecortado.

Trabajar en la aplicación me dio mucha experiencia y, en general, fue una experiencia positiva. Pude escribir una aplicación completamente funcional, en septiembre la mejoraré y la publicaré en la AppStore, simultáneamente con el lanzamiento de iOS 13.

Redux, BindableObject y EnvironmentObject

Desarrollo de aplicaciones en SwiftUI. Parte 1: flujo de datos y Redux

He estado trabajando con Redux durante aproximadamente dos años, por lo que lo conozco relativamente bien. En particular, lo uso en el frontend para Reaccionar sitio web, así como para el desarrollo de aplicaciones nativas para iOS (Swift) y Android (Kotlin).

Nunca me arrepiento de haber elegido Redux como arquitectura de flujo de datos para crear una aplicación SwiftUI. Las partes más desafiantes al usar Redux en una aplicación UIKit son trabajar con la tienda y obtener y recuperar datos y asignarlos a sus vistas/componentes. Para hacer esto, tuve que crear una especie de biblioteca de conectores (usando ReSwift y ReKotlin). Funciona bien, pero requiere bastante código. Desafortunadamente, (todavía) no es de código abierto.

¡Buenas noticias! Las únicas cosas de las que debe preocuparse con SwiftUI, si planea usar Redux, son las tiendas, los estados y los reductores. SwiftUI se encarga completamente de la interacción con la tienda gracias a @EnvironmentObject. Entonces, la tienda comienza con un BindableObject.

Creé un paquete Swift simple, SwiftUIFlux, que proporciona el uso básico de Redux. En mi caso es parte de MovieSwiftUI. Yo también escribió un tutorial paso a paso, que le ayudará a utilizar este componente.

Como funciona?

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

Cada vez que activas una acción, activas la caja de cambios. Evaluará acciones según el estado actual de la aplicación. Luego devolverá un nuevo estado modificado de acuerdo con el tipo de acción y los datos.

Bueno, dado que la tienda es un BindableObject, notificará a SwiftUI cuando su valor cambie usando la propiedad willChange proporcionada por PassthroughSubject. Esto se debe a que BindableObject debe proporcionar un PublisherType, pero la implementación del protocolo es responsable de administrarlo. En general, esta es una herramienta muy poderosa de Apple. En consecuencia, en el próximo ciclo de renderizado, SwiftUI ayudará a renderizar el cuerpo de las vistas de acuerdo con el cambio de estado.

En realidad, este es todo el corazón y la magia de SwiftUI. Ahora, en cualquier vista que se suscriba a un estado, la vista se representará de acuerdo con los datos que se reciban del estado y lo que haya cambiado.

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

La Tienda se inyecta como un EnvironmentObject cuando se inicia la aplicación y luego se puede acceder a ella desde cualquier vista usando @EnvironmentObject. No hay penalización en el rendimiento porque las propiedades derivadas se recuperan o calculan rápidamente a partir del estado de la aplicación.

El código anterior cambia la imagen si cambia el póster de la película.

Y esto se hace en realidad con una sola línea, con la que se conectan las opiniones con el Estado. Si ha trabajado con ReSwift en iOS o incluso conectamos Con React, comprenderás la magia de SwiftUI.

Ahora puedes intentar activar la acción y publicar el nuevo estado. He aquí un ejemplo más complejo.

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

En el código anterior estoy usando la acción .onDelete de SwiftUI para cada IP. Esto permite que la fila de la lista muestre el deslizamiento normal de iOS para eliminar. Entonces, cuando el usuario toca el botón Eliminar, activa la acción correspondiente y elimina la película de la lista.

Bueno, dado que la propiedad de la lista se deriva del estado BindableObject y se inyecta como un EnvironmentObject, SwiftUI actualiza la lista porque ForEach está asociado con la propiedad calculada de las películas.

Aquí está parte del reductor 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
}

El reductor se ejecuta cuando envía una acción y devuelve un nuevo estado, como se indicó anteriormente.

No entraré en detalles todavía: cómo SwiftUI realmente sabe qué mostrar. Para entender esto más profundamente, vale la pena ver la sesión de la WWDC sobre el flujo de datos en SwiftUI. También explica detalladamente por qué y cuándo utilizarlo. Estado, @Binding, ObjectBinding y EnvironmentObject.

Skillbox recomienda:

Fuente: habr.com

Añadir un comentario