Desenvolvimento de aplicativos no SwiftUI. Parte 1: fluxo de dados e Redux



Depois de participar da sessão State of the Union na WWDC 2019, decidi estudar o SwiftUI em detalhes. Passei muito tempo trabalhando com ele e agora comecei a desenvolver um aplicativo real que pode ser útil para uma ampla gama de usuários.

Eu o chamei MovieSwiftUI - este é um aplicativo para encontrar filmes novos e antigos, além de coletá-los em uma coleção usando a API do TMDB . Eu sempre amei filmes e até criei uma empresa trabalhando nessa área, embora por muito tempo. Era difícil chamar a empresa de legal, mas a aplicação - sim!

Lembramos que: para todos os leitores de "Habr" - um desconto de 10.000 rublos ao se inscrever em qualquer curso Skillbox usando o código promocional "Habr".

A Skillbox recomenda: O curso educacional on-line "Profissão Java-developer" .

Então, o que o MovieSwiftUI faz?

  • Interage com a API - é o que quase qualquer aplicativo moderno faz.
  • Carrega dados de solicitação assíncrona e analisa JSON no modelo Swift usando Codable .
  • Mostra imagens baixadas sob demanda e as armazena em cache.
  • Este aplicativo para iOS, iPadOS e macOS fornece o melhor UX para usuários desses sistemas operacionais.
  • O usuário pode gerar dados, criar suas próprias listas de filmes. O aplicativo salva e restaura os dados do usuário.
  • As vistas, componentes e modelos são claramente separados usando o padrão Redux. O fluxo de dados é unidirecional aqui. Pode ser totalmente armazenado em cache, restaurado e substituído.
  • O aplicativo usa os componentes básicos SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. Ele também fornece visualizações personalizadas, gestos, UI / UX.


De fato, a animação é suave, o GIF acabou sendo um pouco contorcido

Trabalhar no aplicativo me proporcionou muita experiência e, no geral, é uma experiência positiva. Consegui escrever um aplicativo totalmente funcional; em setembro, o aprimorarei e o colocarei na AppStore, simultaneamente com o lançamento do iOS 13.

Redux, BindableObject e EnvironmentObject




Trabalho com Redux há cerca de dois anos, então sei disso relativamente bem. Em particular, eu o uso no frontend do site React , bem como no desenvolvimento de aplicativos iOS nativos (Swift) e Android (Kotlin).

Nunca me arrependi de ter escolhido o Redux como arquitetura de fluxo de dados para criar um aplicativo no SwiftUI. Os momentos mais difíceis ao usar o Redux no aplicativo UIKit estão trabalhando com a loja, além de obter e recuperar dados e compará-los com suas visualizações / componentes. Para fazer isso, tive que criar um tipo de biblioteca de conectores (no ReSwift e ReKotlin). Funciona bem, mas bastante código. Infelizmente, é (ainda não) código aberto.

Boas notícias! As únicas coisas com que se preocupar com o SwiftUI - se você planeja usar o Redux - são lojas, estados e redutores. O SwiftUI assume a interação com a loja graças ao @EnvironmentObject. Portanto, a loja começa com BindableObject.

Criei um pacote Swift simples, o SwiftUIFlux , que fornece o uso básico do Redux. No meu caso, isso faz parte do MovieSwiftUI. Também escrevi um tutorial passo a passo para ajudar você a usar esse componente.

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

Sempre que você inicia uma ação, você ativa a caixa de velocidades. Ele avaliará as ações de acordo com o estado atual do aplicativo. Em seguida, ele retornará um novo estado modificado de acordo com o tipo de ação e dados.

Bem, como store é um BindableObject, ele notificará o SwiftUI sobre uma alteração em seu valor usando a propriedade willChange fornecida por PassthroughSubject. Isso ocorre porque BindableObject deve fornecer PublisherType, mas a implementação do protocolo é responsável por gerenciá-lo. Em suma, esta é uma ferramenta muito poderosa da Apple. Assim, no próximo ciclo de renderização, o SwiftUI ajudará a exibir o corpo das representações de acordo com a mudança de estado.

Na verdade, isso é tudo - o coração e a magia do SwiftUI. Agora, em qualquer exibição que esteja inscrita em um estado, a exibição será exibida de acordo com quais dados são recebidos do estado e o que foi alterado.

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

O armazenamento é implementado como um EnvironmentObject quando o aplicativo é iniciado e, em seguida, disponível em qualquer visualização usando @EnvironmentObject. O desempenho não é reduzido porque as propriedades derivadas são recuperadas ou calculadas rapidamente a partir do estado do aplicativo.

O código acima altera a imagem se o pôster do filme mudar.

E isso é realmente feito em apenas uma linha, com a ajuda da qual as visualizações estão conectadas ao estado. Se você trabalhou com o ReSwift no iOS ou até mesmo se conectou ao React, entenderá o que é a mágica do SwiftUI.

E agora você pode tentar ativar a ação e publicar um novo estado. Aqui está um exemplo mais complexo.

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

No código acima, eu uso a ação .onDelete da SwiftUI para cada IP. Isso permite que a linha da lista exiba o furto usual do iOS para exclusão. Portanto, quando o usuário toca no botão excluir, ele inicia a ação correspondente e remove o filme da lista.

Bem, como a propriedade list é derivada do estado de BindableObject e implementada como EnvironmentObject, o SwiftUI atualiza a lista porque o ForEach está associado à propriedade calculada do filme.

Aqui está parte do redutor 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 } 

O redutor é executado quando você envia a ação e retorna um novo estado, conforme mencionado acima.

Por enquanto, não entrarei em detalhes - como o SwiftUI realmente sabe o que exibir. Para entender isso mais profundamente, você deve examinar a sessão da WWDC sobre fluxo de dados no SwiftUI. Também explica em detalhes por que e quando usar State , @Binding, ObjectBinding e EnvironmentObject.

A Skillbox recomenda:

Source: https://habr.com/ru/post/pt460713/


All Articles