Pag-unlad ng application sa SwiftUI. Bahagi 1: Dataflow at Redux

Pag-unlad ng application sa SwiftUI. Bahagi 1: Dataflow at Redux

Pagkatapos dumalo sa sesyon ng State of the Union sa WWDC 2019, nagpasya akong sumisid nang malalim sa SwiftUI. Gumugol ako ng maraming oras sa pagtatrabaho dito at nagsimula na akong bumuo ng isang tunay na application na maaaring maging kapaki-pakinabang sa isang malawak na hanay ng mga user.

Tinawag ko itong MovieSwiftUI - ito ay isang app para sa paghahanap ng bago at lumang mga pelikula, pati na rin ang pagkolekta ng mga ito sa isang koleksyon gamit ang TMDB API. Palagi akong mahilig sa mga pelikula at kahit na lumikha ng isang kumpanya na nagtatrabaho sa larangang ito, kahit na matagal na ang nakalipas. Ang kumpanya ay halos hindi matatawag na cool, ngunit ang aplikasyon ay!

Pinapaalala namin sa iyo: para sa lahat ng mga mambabasa ng "Habr" - isang diskwento na 10 rubles kapag nag-enroll sa anumang kurso sa Skillbox gamit ang code na pang-promosyon ng "Habr".

Inirerekomenda ng Skillbox ang: Online na kursong pang-edukasyon "Propesyon Java Developer".

Kaya ano ang magagawa ng MovieSwiftUI?

  • Nakikipag-ugnayan sa API - ginagawa ito ng halos anumang modernong application.
  • Naglo-load ng asynchronous na data sa mga kahilingan at nag-parse ng JSON sa modelong Swift na ginagamit Codable.
  • Nagpapakita ng mga larawang na-load kapag hiniling at ini-cache ang mga ito.
  • Ang app na ito para sa iOS, iPadOS, at macOS ay nagbibigay ng pinakamahusay na UX para sa mga user ng mga OS na ito.
  • Ang user ay maaaring bumuo ng data at lumikha ng kanilang sariling mga listahan ng pelikula. Ang application ay nagse-save at nagpapanumbalik ng data ng user.
  • Ang mga view, bahagi at modelo ay malinaw na pinaghihiwalay gamit ang Redux pattern. Ang daloy ng data dito ay unidirectional. Maaari itong ganap na mai-cache, maibalik at ma-overwrite.
  • Ginagamit ng application ang mga pangunahing bahagi ng SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, atbp. Nagbibigay din ito ng mga custom na view, kilos, UI/UX.

Pag-unlad ng application sa SwiftUI. Bahagi 1: Dataflow at Redux
Sa katunayan, ang animation ay makinis, ang GIF ay naging medyo maalog

Ang pagtatrabaho sa app ay nagbigay sa akin ng maraming karanasan at sa pangkalahatan ito ay isang positibong karanasan. Nagawa kong magsulat ng isang fully functional na application, sa Setyembre ay pagbubutihin ko ito at i-publish ito sa AppStore, kasabay ng paglabas ng iOS 13.

Redux, BindableObject at EnvironmentObject

Pag-unlad ng application sa SwiftUI. Bahagi 1: Dataflow at Redux

Mahigit dalawang taon na akong nagtatrabaho sa Redux, kaya medyo sanay na ako dito. Sa partikular, ginagamit ko ito sa frontend para sa Gantihin website, gayundin para sa pagbuo ng katutubong iOS (Swift) at Android (Kotlin) na mga application.

Hindi ko kailanman pinagsisihan ang pagpili sa Redux bilang arkitektura ng daloy ng data para sa pagbuo ng isang SwiftUI application. Ang pinaka-mapanghamong bahagi kapag gumagamit ng Redux sa isang UIKit app ay gumagana sa tindahan at pagkuha at pagkuha ng data at pagmamapa nito sa iyong mga view/bahagi. Upang gawin ito, kailangan kong lumikha ng isang uri ng library ng mga konektor (gamit ang ReSwift at ReKotlin). Gumagana nang maayos, ngunit medyo maraming code. Sa kasamaang palad, ito ay hindi (pa) open source.

Magandang balita! Ang tanging bagay na dapat alalahanin sa SwiftUI - kung plano mong gamitin ang Redux - ay mga tindahan, estado, at mga reducer. Ang pakikipag-ugnayan sa tindahan ay ganap na pinangangalagaan ng SwiftUI salamat sa @EnvironmentObject. Kaya, nagsisimula ang tindahan sa isang BindableObject.

Gumawa ako ng isang simpleng pakete ng Swift, SwiftUIFlux, na nagbibigay ng pangunahing paggamit ng Redux. Sa aking kaso ito ay bahagi ng MovieSwiftUI. ako rin nagsulat ng isang hakbang-hakbang na tutorial, na makakatulong sa iyong gamitin ang bahaging ito.

Paano ito gumagana?

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

Sa tuwing magti-trigger ka ng aksyon, ina-activate mo ang gearbox. Susuriin nito ang mga aksyon ayon sa kasalukuyang estado ng aplikasyon. Magbabalik ito ng bagong binagong estado alinsunod sa uri ng pagkilos at data.

Well, dahil ang tindahan ay isang BindableObject, aabisuhan nito ang SwiftUI kapag nagbago ang halaga nito gamit ang willChange property na ibinigay ng PassthroughSubject. Ito ay dahil ang BindableObject ay dapat magbigay ng PublisherType, ngunit ang pagpapatupad ng protocol ay may pananagutan sa pamamahala nito. Sa pangkalahatan, ito ay isang napakalakas na tool mula sa Apple. Alinsunod dito, sa susunod na cycle ng pag-render, makakatulong ang SwiftUI na i-render ang katawan ng mga view ayon sa pagbabago ng estado.

Sa totoo lang, ito ang lahat ng puso at mahika ng SwiftUI. Ngayon, sa anumang view na nag-subscribe sa isang estado, ang view ay ire-render ayon sa kung anong data ang natanggap mula sa estado at kung ano ang nagbago.

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

Ang Store ay ini-inject bilang isang EnvironmentObject kapag nagsimula ang application at pagkatapos ay maa-access sa anumang view gamit ang @EnvironmentObject. Walang parusa sa pagganap dahil ang mga nagmula na ari-arian ay mabilis na nakuha o kinakalkula mula sa estado ng aplikasyon.

Binabago ng code sa itaas ang larawan kung magbabago ang poster ng pelikula.

At ito ay aktwal na ginagawa sa isang linya lamang, sa tulong ng kung aling mga view ay konektado sa estado. Kung nagtrabaho ka sa ReSwift sa iOS o kahit na ikabit sa React, mauunawaan mo ang mahika ng SwiftUI.

Ngayon ay maaari mong subukang i-activate ang aksyon at i-publish ang bagong estado. Narito ang isang mas kumplikadong halimbawa.

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

Sa code sa itaas, gumagamit ako ng .onDelete na pagkilos mula sa SwiftUI para sa bawat IP. Nagbibigay-daan ito sa row sa listahan na ipakita ang normal na iOS swipe para tanggalin. Kaya kapag hinawakan ng user ang delete button, nati-trigger nito ang kaukulang aksyon at inaalis ang pelikula sa listahan.

Well, dahil ang list property ay nagmula sa BindableObject state at ini-inject bilang isang EnvironmentObject, ina-update ng SwiftUI ang listahan dahil ang ForEach ay nauugnay sa mga movies kalkuladong property.

Narito ang bahagi ng MoviesState reducer:

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
}

Ang reducer ay isinasagawa kapag nagpadala ka ng isang aksyon at nagbalik ng isang bagong estado, tulad ng nakasaad sa itaas.

Hindi pa ako magdedetalye - kung paano talaga alam ng SwiftUI kung ano ang ipapakita. Upang maunawaan ito nang mas malalim, ito ay nagkakahalaga tingnan ang session ng WWDC sa daloy ng data sa SwiftUI. Ipinapaliwanag din nito nang detalyado kung bakit at kailan gagamitin estado, @Binding, ObjectBinding at EnvironmentObject.

Inirerekomenda ng Skillbox ang:

Pinagmulan: www.habr.com

Magdagdag ng komento