Desarrollo de una aplicación en SwiftUI. Parte 1: flujo de datos y Redux

Desarrollo de una aplicación en SwiftUI. Parte 1: flujo de datos y Redux

Después de participar en la sesión del Estado de la Unión en WWDC 2019, decidí estudiar SwiftUI en detalle. Pasé mucho tiempo trabajando con él y ahora estoy desarrollando una aplicación real que podría resultar útil para un amplio público.

La llamé MovieSwiftUI — es una app para buscar películas nuevas y antiguas, así como para coleccionarlas utilizando TMDB API. Siempre me han encantado las películas e incluso fundé una empresa que trabajaba en este ámbito, aunque hace tiempo. La empresa era difícil de considerar genial, pero la aplicación sí lo es.

Recordamos: para todos los lectores de «Habr» — un descuento de 10,000 rublos al registrarse en cualquier curso de Skillbox usando el código promocional «Habr».

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

Entonces, ¿qué puede hacer MovieSwiftUI?

  • Interactúa con API — eso hace casi cualquier aplicación moderna.
  • Carga datos asíncronos mediante solicitudes y analiza JSON en modelos Swift utilizando Codable.
  • Muestra imágenes que se cargan bajo demanda y las guarda en caché.
  • Esta aplicación para iOS, iPadOS y macOS ofrece la mejor experiencia de usuario 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 los datos del usuario.
  • Las vistas, componentes y modelos están claramente separados mediante el patrón Redux. El flujo de datos es unidireccional. Puede ser completamente almacenado en caché, restaurado y sobrescrito.
  • La aplicación utiliza componentes básicos de SwiftUI, como TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. También proporciona vistas personalizadas, gestos, UI/UX.

Desarrollo de una aplicación en SwiftUI. Parte 1: flujo de datos y Redux
De hecho, la animación es suave, aunque el gif resultó un poco entrecortado.

Trabajar en la aplicación me dio mucha experiencia, y en general, ha sido una experiencia positiva. Pude escribir una aplicación completamente funcional, en septiembre la mejoraré y la publicaré en App Store, coincidiendo con el lanzamiento de iOS 13.

Redux, BindableObject y EnvironmentObject

Desarrollo de una aplicación en SwiftUI. Parte 1: flujo de datos y Redux

Hasta ahora, he estado trabajando con Redux durante aproximadamente dos años, así que estoy bastante familiarizado con él. En particular, lo utilizo en el frontend para React el sitio web, así como para el desarrollo de aplicaciones nativas en iOS (Swift) y Android (Kotlin).

Nunca me he arrepentido de elegir Redux como arquitectura de flujo de datos para crear aplicaciones en SwiftUI. Los momentos más difíciles al usar Redux en una aplicación de UIKit son trabajar con el store, así como obtener y extraer datos y hacer coincidirlos con tus vistas/componentes. Para esto, tuve que crear una especie de biblioteca de conectores (en ReSwift y ReKotlin). Funciona bien, pero hay que escribir bastante código. Desafortunadamente, aún no es open source.

¡Buenas noticias! Lo único de lo que debes preocuparte con SwiftUI, si planeas usar Redux, son el store, los estados y los reducers. SwiftUI se encarga completamente de la interacción con el store gracias a @EnvironmentObject. Así, el store comienza con BindableObject.

He creado un paquete Swift simple, SwiftUIFlux, que proporciona un uso básico de Redux. En mi caso, es parte de MovieSwiftUI. También he escrito un tutorial paso a paso, que ayudará a utilizar este componente.

¿Cómo funciona?

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

Cada vez que ejecutas una acción, activas el reducer. Este evaluará las acciones de acuerdo al estado actual de la aplicación. Luego, devolverá un nuevo estado modificado en función del tipo de acción y los datos.

Dado que el store es un BindableObject, notificará a SwiftUI sobre el cambio de su valor utilizando la propiedad willChange, proporcionada por PassthroughSubject. Esto se debe a que BindableObject debe ofrecer PublisherType, pero la implementación del protocolo es responsable de administrarlo. En general, es una herramienta muy poderosa de Apple. Por lo tanto, en el siguiente ciclo de renderizado, SwiftUI ayudará a mostrar el cuerpo de las vistas de acuerdo a los cambios en el estado.

En realidad, eso es todo: el corazón y la magia de SwiftUI. Ahora, en cualquier vista que esté suscrita al estado, la vista se mostrará de acuerdo a los datos que se obtengan 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
    
    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 se implementa como EnvironmentObject al iniciar la aplicación y luego está disponible en cualquier vista utilizando @EnvironmentObject. El rendimiento no se ve afectado, ya que las propiedades derivadas se extraen o calculan rápidamente del estado de la aplicación.

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

Y esto se realiza de hecho con solo una línea, con la cual se conectan las vistas al estado. Si has trabajado con ReSwift en iOS o incluso connect con React, entenderás cuál es la magia de SwiftUI.

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

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

En el código anterior, utilizo la acción .onDelete de SwiftUI para cada IP. Esto permite que la fila en la lista muestre el deslizamiento habitual de iOS para eliminar. Así, cuando el usuario toca el botón de eliminación, activa la acción correspondiente y elimina la película de la lista.

Dado que la propiedad de la lista es derivada del estado BindableObject y se inyecta como EnvironmentObject, SwiftUI actualiza la lista ya que ForEach está vinculado a la propiedad calculada movies.

Aquí está parte del reductor MoviesState:

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    
    // otras acciones.
    
    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ías una acción y devuelve un nuevo estado, como se mencionó anteriormente.

No entraré en detalles por ahora sobre cómo SwiftUI realmente sabe qué mostrar. Para entenderlo más a fondo, vale la pena revisar la sesión de WWDC sobre el flujo de datos en SwiftUI. También se habla en detalle sobre cuándo y por qué usar Estado, @Binding, ObjectBinding y EnvironmentObject.

Skillbox recomienda:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster