Хабрахабр

[Перевод] Разработка приложения на SwiftUI. Часть 1: поток данных и Redux

Я потратил много времени на работу с ним и теперь приступил к разработке реального приложения, которое может оказаться полезным широкому кругу пользователей. После участия в сессии State of the Union на WWDC 2019 я решил детально изучить SwiftUI.

Я всегда любил фильмы и даже создал компанию, работающую в этой сфере, правда давно. Его я назвал MovieSwiftUI — это апп для поиска новых и старых фильмов, а также их сбора в коллекцию при помощи TMDB API. Компанию сложно было назвать классной, а вот приложение — да!

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Skillbox рекомендует: Образовательный онлайн-курс «Профессия Java-разработчик».

Итак, что умеет MovieSwiftUI?

  • Взаимодействует с API — это делает почти любое современное приложение.
  • Загружает асинхронные данные по запросам и парсит JSON в Swift-модели, используя Codable.
  • Показывает изображения, загружаемые по запросу, и кэширует их.
  • Это приложение для iOS, iPadOS, и macOS обеспечивает лучший UX для пользователей этих ОС.
  • Пользователь может генерировать данные, создавать собственные списки фильмов. Приложение сохраняет и восстанавливает пользовательские данные.
  • Представления, компоненты и модели четко разделены при помощи паттерна Redux. Поток данных здесь однонаправлен. Он может быть полностью кэширован, восстановлен и перезаписан.
  • Приложение использует базовые компоненты SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal и т.п. Также оно обеспечивает кастомные представления, жесты, UI/UX.


На самом деле анимация плавная, гифка получилась немного дерганая

Я смог написать полнофункциональное приложение, в сентябре я его улучшу и выложу в AppStore, одновременно с выходом iOS 13. Работа над приложением дала мне много опыта, и в целом это положительный опыт.

Redux, BindableObject и EnvironmentObject

В частности, я использую его во фронтенде для React веб-сайта, а также для разработки нативных iOS (Swift) и Android (Kotlin) приложений. На сегодняшний момент я работаю с Redux уже около двух лет, так что относительно хорошо в этом разбираюсь.

Наиболее сложные моменты при использовании Redux в приложении UIKit — это работа со store, а также получение и извлечение данных и сопоставление их с вашими представлениями / компонентами. Я ни разу не пожалел о выборе Redux в качестве архитектуры потока данных для создания приложения на SwiftUI. Работает хорошо, но довольно много кода. Для этого пришлось создать своего рода библиотеку коннекторов (на ReSwift и ReKotlin). К сожалению, он (пока) не open source.

Единственное, о чем стоит беспокоиться со SwiftUI — если вы планируете использовать Redux, — это store, состояния и редьюсеры. Хорошие новости! Так, store начинается с BindableObject. Взаимодействие со store полностью берет на себя SwiftUI благодаря @EnvironmentObject.

В моем случае это часть MovieSwiftUI. Я создал простой пакет Swift, SwiftUIFlux, который обеспечивает базовое использование Redux. Также я написал пошаговый туториал, который поможет использовать этот компонент.

Как это работает?

final public class Store<State: FluxState>: BindableObject
}

Каждый раз, когда вы запускаете действие, вы активируете редуктор. Он будет оценивать действия в соответствии с текущим состоянием приложения. Далее он будет возвращать новое модифицированное состояние в соответствии с видом действия и данными.

Это так потому, что BindableObject должен предоставлять PublisherType, но реализация протокола отвечает за управление им. Ну а поскольку store является BindableObject, он будет уведомлять SwiftUI об изменении своего значения, используя свойство willChange, предоставленное PassthroughSubject. Соответственно, в следующем цикле рендеринга SwiftUI поможет отобразить тело представлений в соответствии с изменением состояния. В общем, это очень мощный инструмент от Apple.

Теперь в любом представлении, которое подписано на состояние, представление будет отображаться в соответствии с тем, какие данные получены из состояния и что изменилось. Собственно, это все — сердце и магия 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 связан с вычисляемым свойством movies.

Вот часть редьюсера 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
}

Редуктор выполняется, когда вы отправляете действие и возвращаете новое состояние, как сказано выше.

Для того, чтобы понять это более глубоко, стоит просмотреть сессию WWDC о потоке данных в SwiftUI. Я пока не буду вдаваться в подробности — откуда SwiftUI на самом деле знает, что отображать. Также там подробно рассказывается, зачем и когда использовать State, @Binding, ObjectBinding и EnvironmentObject.

Skillbox рекомендует:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть