Разработка на приложение на 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
Всъщност анимацията е гладка, GIF-ът се получи малко рязък.

Работата по приложението ми предостави много опит, и като цяло това е положителен опит. Успях да напиша функционално приложение, през септември ще го подобря и ще го пусна в AppStore, съвместно с излизането на iOS 13.

Redux, BindableObject и EnvironmentObject

Разработка на приложение на SwiftUI. Част 1: поток от данни и Redux

В момента работя с Redux вече около две години, така че прилично разбирам в него. В частност, използвам го на фронтенда за React уебсайта, а също и за разработка на нативни 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: 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:

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. Там също подробно се обяснява защо и кога да се използват State, @Binding, ObjectBinding и EnvironmentObject.

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

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

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