Разработка на приложение на 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 модели, използвайки Codable.
  • Показва изображения, зареждани по искане, и ги кешира.
  • Това приложение за 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 от около две години, така че относително добре разбирам тази технология. По-специално, използвам я на фронтенда за React уебсайта, а също така и за разработката на нативни iOS (Swift) и Android (Kotlin) приложения.

Нито веднъж не съжалих за избора на Redux като архитектура на потока от данни за разработване на приложение на SwiftUI. Най-трудните моменти при използването на Redux в приложение с UIKit са работата с store, както и получаването и извличането на данни и съпоставянето им с вашите представления/компоненти. За това се наложи да създам нещо като библиотека от конектори (на ReSwift и ReKotlin). Работи добре, но изисква доста код. За съжаление, той (засега) не е с отворен код.

Добри новини! Единственото, което трябва да ви притеснява относно SwiftUI — ако планирате да използвате Redux — е store, състоянията и редюсерите. Взаимодействието със store изцяло се поема от SwiftUI благодарение на @EnvironmentObject. Така store започва с BindableObject.

Създадох прост пакет Swift, SwiftUIFlux, който осигурява базово използване на Redux. В моя случай това е част от MovieSwiftUI. Освен това написах стъпка по стъпка ръководство, което ще помогне да се използва този компонент.

Как работи това?

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

Всеки път, когато стартирате действие, активирате редюсера. Той ще оцени действията в съответствие с текущото състояние на приложението. След това ще върне ново модифицирано състояние в зависимост от вида на действието и данните.

А тъй като 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
    
    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
 
    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:

фунцкия moviesStateReducer(състояние: MoviesState, действие: Action) -> MoviesState {
    var state = state
    switch action {
    
    
    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. Също така там подробно се обсъжда защо и кога да се използват State, @Binding, ObjectBinding и EnvironmentObject.

Skillbox препоръчва:

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster