Розробка програми на SwiftUI. Частина 1: потік даних та Redux

Розробка програми на SwiftUI. Частина 1: потік даних та Redux

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

Його я назвав MovieSwiftUI - це апп для пошуку нових і старих фільмів, а також їх збору в колекцію за допомогою TMDB API. Я завжди любив фільми і навіть створив компанію, що працює у цій сфері, щоправда, давно. Компанію складно було назвати класною, а ось додаток – так!

Нагадуємо: для всіх читачів "Хабра" - знижка 10 000 рублів при записі на будь-який курс Skillbox за промокодом "Хабр".

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

Отже, що вміє MovieSwiftUI?

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

Розробка програми на SwiftUI. Частина 1: потік даних та Redux
Насправді анімація плавна, гіфка вийшла трохи сіпана

Робота над програмою дала мені багато досвіду, і в цілому це позитивний досвід. Я зміг написати повнофункціональну програму, у вересні я її покращу і викладу в AppStore, одночасно з виходом iOS 13.

Redux, BindableObject та EnvironmentObject

Розробка програми на SwiftUI. Частина 1: потік даних та Redux

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

Я жодного разу не пошкодував про вибір Redux як архітектура потоку даних для створення програми на SwiftUI. Найбільш складні моменти при використанні Redux у додатку UIKit - це робота зі store, а також отримання та вилучення даних та зіставлення їх з вашими уявленнями/компонентами. Для цього довелося створити свого роду бібліотеку конекторів (на ReSwift та ReKotlin). Працює добре, але багато коду. На жаль, він (поки що) не open source.

Гарні новини! Єдине, про що варто турбуватися зі SwiftUI – якщо ви плануєте використовувати Redux, – це store, стани та редьюсери. Взаємодія зі store повністю бере на себе SwiftUI завдяки @EnvironmentObject. Так, store починається з BindableObject.

Я створив простий пакет Swift, SwiftUIFluxщо забезпечує базове використання Redux. У моєму випадку це частина MovieSwiftUI. Також я написав покроковий туторіал, який допоможе використати цей компонент.

Як це працює?

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

Щоразу, коли ви запускаєте дію, ви активуєте редуктор. Він оцінюватиме дії відповідно до поточного стану програми. Далі він повертатиме новий модифікований стан відповідно до виду дії та даних.

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

Власне, це все – серце та магія 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 або навіть з'єднуватися з 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
}

Редуктор виконується, коли ви відправляєте дію та повертаєте новий стан, як сказано вище.

Я поки не вдаватимуся в подробиці - звідки SwiftUI насправді знає, що відображати. Для того, щоб зрозуміти це глибше, варто переглянути сесію WWDC про потік даних у SwiftUI. Також там докладно розповідається, навіщо та коли використовувати стан, @Binding, ObjectBinding та EnvironmentObject.

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

Джерело: habr.com

Додати коментар або відгук