Распрацоўка прыкладання на 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 ужо каля двух гадоў, так што адносна добра ў гэтым разбіраюся. У прыватнасці, я выкарыстоўваю яго ў франтэндзе для Рэагаваць вэб-сайта, а таксама для распрацоўкі натыўных 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

Дадаць каментар