
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 comencé a desarrollar una aplicación real que puede ser útil para una amplia gama de usuarios.
Lo llamé MovieSwiftUI: esta es una aplicación para encontrar películas nuevas y antiguas, así como para recopilarlas en una colección utilizando la
API TMDB . Siempre me encantaron las películas e incluso creé una empresa que trabaja en esta área, aunque durante mucho tiempo. Fue difícil llamar a la compañía genial, pero la aplicación, ¡sí!
Le recordamos: para todos los lectores de "Habr": un descuento de 10.000 rublos al registrarse en cualquier curso de Skillbox con el código de promoción "Habr".
Skillbox recomienda: El curso educativo en línea "Profession Java-developer" .
Entonces, ¿qué hace MovieSwiftUI?
- Interactúa con la API: esto es lo que hace casi cualquier aplicación moderna.
- Carga datos de solicitud asíncronos y analiza JSON en el modelo Swift usando Codificable .
- Muestra imágenes descargadas a pedido y las almacena en caché.
- Esta aplicación para iOS, iPadOS y macOS proporciona la mejor experiencia de usuario para los usuarios de estos sistemas operativos.
- El usuario puede generar datos, crear sus propias listas de películas. La aplicación guarda y restaura los datos del usuario.
- Las vistas, los componentes y los modelos están claramente separados usando el patrón Redux. El flujo de datos es unidireccional aquí. Se puede almacenar en caché, restaurar y sobrescribir por completo.
- La aplicación utiliza los componentes básicos SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. También proporciona vistas personalizadas, gestos, UI / UX.
De hecho, la animación es suave, el GIF resultó ser un poco nerviosoTrabajar en la aplicación me dio mucha experiencia, y en general es una experiencia positiva. Pude escribir una aplicación totalmente funcional, en septiembre la mejoraré y la pondré en la AppStore, simultáneamente con el lanzamiento de iOS 13.
Redux, BindableObject y EnvironmentObject

He estado trabajando con Redux durante aproximadamente dos años, así que lo sé relativamente bien. En particular, lo uso en la interfaz del sitio web
React , así como para desarrollar aplicaciones nativas de iOS (Swift) y Android (Kotlin).
Nunca me he arrepentido de elegir Redux como la arquitectura de flujo de datos para construir una aplicación en SwiftUI. Los momentos más difíciles al usar Redux en la aplicación UIKit son trabajar con la tienda, así como obtener y recuperar datos y compararlos con sus vistas / componentes. Para hacer esto, tuve que crear una especie de biblioteca de conectores (en ReSwift y ReKotlin). Funciona bien, pero bastante código. Desafortunadamente, es (todavía no) de código abierto.
Buenas noticias! Las únicas cosas de las que preocuparse con SwiftUI, si planea usar Redux, son tiendas, estados y reductores. SwiftUI se hace cargo de la interacción con la tienda gracias a @EnvironmentObject. Entonces, la tienda comienza con BindableObject.
Creé un paquete simple de Swift,
SwiftUIFlux , que proporciona el uso básico de Redux. En mi caso, esto es parte de MovieSwiftUI. También
escribí un tutorial paso a
paso para ayudarlo a usar este componente.
Como funcionafinal 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 comienzas una acción, activas la caja de cambios. Evaluará las acciones de acuerdo con el estado actual de la aplicación. Luego devolverá un nuevo estado modificado de acuerdo con el tipo de acción y datos.
Bueno, dado que store es un BindableObject, notificará a SwiftUI de un cambio en su valor utilizando la propiedad willChange proporcionada por PassthroughSubject. Esto se debe a que BindableObject debe proporcionar PublisherType, pero la implementación del protocolo es responsable de administrarlo. Con todo, esta es una herramienta muy poderosa de Apple. En consecuencia, en el próximo ciclo de representación, SwiftUI ayudará a mostrar el cuerpo de representaciones de acuerdo con el cambio de estado.
En realidad, esto es todo: el corazón y la magia de SwiftUI. Ahora, en cualquier vista que esté suscrita a un estado, la vista se mostrará de acuerdo con qué datos se reciben del estado y qué ha 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()) } }
Store se implementa como un EnvironmentObject cuando se inicia la aplicación, y luego está disponible en cualquier vista usando @EnvironmentObject. El rendimiento no se reduce porque las propiedades derivadas se recuperan o calculan rápidamente desde el estado de la aplicación.
El código anterior cambia la imagen si cambia el póster de la película.
Y esto realmente se hace en una sola línea, con la ayuda de la cual las vistas están conectadas al estado. Si trabajó con ReSwift en iOS o incluso se
conectó con React, comprenderá qué es la magia de SwiftUI.
Y ahora puede intentar activar la acción y publicar un nuevo estado. Aquí hay 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, uso la acción .onDelete de SwiftUI para cada IP. Esto permite que la línea de la lista muestre el deslizamiento habitual de iOS para su eliminación. Por lo tanto, cuando el usuario toca el botón Eliminar, inicia la acción correspondiente y elimina la película de la lista.
Bueno, dado que la propiedad de la lista se deriva del estado de BindableObject y se implementa como un EnvironmentObject, SwiftUI actualiza la lista, porque ForEach está asociado con la propiedad de película calculada.
Aquí está parte del reductor MoviesState:
func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState { var state = state switch action {
El reductor se ejecuta cuando envía la acción y devuelve un nuevo estado, como se mencionó anteriormente.
No voy a entrar en detalles por ahora: ¿cómo sabe SwiftUI qué mostrar? Para comprender esto más profundamente, debe
mirar la sesión de WWDC sobre el flujo de datos en SwiftUI. También explica en detalle por qué y cuándo usar
State , @Binding, ObjectBinding y EnvironmentObject.
Skillbox recomienda: