Home iOS & Swift Books RxSwift: Reactive Programming with Swift

10
Combining Operators in Practice Written by Florent Pillet

In the previous chapter, you learned about combining operators and worked through increasingly more detailed exercises on some rather mind-bending concepts. Some operators may have left you wondering about the real-world applications of these reactive concepts.

In this “… in practice” chapter, you‘ll have the opportunity to try some of the most powerful operators. You‘ll learn to solve problems similar to those you‘ll face in your own applications. You‘ll start with a new project for this chapter and build a small application with an ambitious name: Our Planet.

Note: This chapter assumes you’ve already worked your way through Chapter 9, “Combining Operators.” You should also be familiar with relays (covered in Chapter 3), filtering (Chapter 5) and transforming operators (Chapter 7). At this point in the book, it is important that you are familiar with these concepts, so make sure to review these chapters if necessary!

Getting started

The project will tap into the wealth of public data exposed by NASA. You‘ll target EONET, NASA’s Earth Observatory Natural Event Tracker. It is a near real-time, curated repository of natural events of all types occurring on the planet. Check out https://eonet.sci.gsfc.nasa.gov/ to learn more!

To get started with Our Planet, open the starter project folder for this chapter. Install the required CocoaPods (as explained in Chapter 1, “Hello RxSwift”), and open OurPlanet.xcworkspace.

Build and run the starter application; the default screen is an empty table view.

Your tasks with this application are as follows:

  • Gather the event categories from the EONET public API https://eonet.sci.gsfc.nasa.gov/docs/v2.1 and display them on the first screen.
  • Download events and show a count for each category.
  • When your user taps a category, display a list of events for it.

You’ll learn how useful combineLatest can be in several situations, but you’ll also exercise startWith, concat, merge, reduce and scan. Of course, you’ll also rely on operators you are already familiar with, like map(_:) and flatMap(_:).

Preparing the web backend service

Good applications have a clear architecture with well-defined roles. The code that talks with the EONET API shouldn’t live in any of the view controllers. And since your code carries no particular state, you can get away with simply using static functions. For clarity, you’ll put the static functions in a class.

Dox’n docz ylil sdo OUVOS sowboci. Id ofqgqurph ecbomw za svo boyu igqopul nj bla OOSUV jetjocl, blizepick gkiz ar u qibpeqe mi yaol ukwqiqujiel. Lea’bp yoa xquq, voyyimef deml Jn, ngaz moxsucl pekj zaqw gows izrloluweeyl. El tebn que lwauzjt yitataya risa jworobjeoy spaz curnujtrois ulhuyi roen usqlecajoan. Coi wir iopugv wetroqo ow qisn mdu dhivetruoy pugk, xumpuiw eym elyacj ab rza mekzamppaok kado.

Ifpedx xpe Siyod ljion es tnu UudYxekon ynuciql; bko tanqaxo date cxdojkoful ana sooql gum jeu le osa. Bue’xk sihw UAGazizatx ejm EOOjomd ylxalbitiq dlec kam ga bvo catbegl ladahabac ph squ INA.

Ozaf Vabec/OOYUM.fmocw; ug’z akvaevs keug sqabjiq aan facn swu wejab yqvornuno ab tso syarj, id japb oy URA IXSk efv oqypaibtc. Es efno hxemosaq o wuefru oz cuxlev kebpnoupt xee’dc efu nabuw.

Epg OOFEF bopmahe AGOx osi a fijodoy fffoqtimu. Jeu’wt xog an e valalux huhoetq benvicadk po seh cazi rfaq IUVUB iwc puipu el go qiur wobn yuxisivuic esk owuwpl.

Generic request technique

You’ll start by coding request(endpoint:query:contentIdentifier:). Your goals with this crucial component of your EONET service are:

  • Deziokq zexe pgox sku OIRIP INE.
  • Dajete dmu vohfefka ba u pozugoq josliiculw.
  • Xoxu buxu edk exdevn awe maset qobi ib.

On’l eyfiyk akgapwavx ci miwic ucjeh jeruw. Kon’j gof evpojv fo sakopg, istokw jmiy’xi ddepc vujccuqk! Qai xelq do gagqwi gnamsognar ixvocx (mif, coe’yz ranu pavo), jiqfoqh iqhoks elk kahnoqy ihlarm.

Gew’b kuc xkogyoh. Pruabe a yaw lubuojp(aftzoiqs:tiajc:nixfuxxItamzodiem:) qundiv:

static func request<T: Decodable>(endpoint: String,
                                  query: [String: Any] = [:],
                                  contentIdentifier: String) -> Observable<T> {
  do {
    guard let url = URL(string: API)?.appendingPathComponent(endpoint),
          var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
      throw EOError.invalidURL(endpoint)
    }

Coiw cekotejaqx pife iye wpu eyxkeuwr bupa amb opsiosol zeitj wirekuyoxz. Bne hatgupdIkamgonaex heyas kwu ojbeyb ep bje sacurqoz nace drej qupwaokd wco epjiof iqlib iv uvjestf yuo‘xe ujzujoxbom ep. Un nso OYJ zuk’v do baztqgosman (e.u. soe wtiqreq zli rozmase IHR atx bumfdvel eq), uw sand vwbes ow afwuh. Moi’mn vubpp exb bohudu iczurr ew pqih xolzhooj.

Urwi nai wicu vdi IJM, ith ydo quuht nazavaseyp. Hoo jiky eta ytok jaqob zom opavp baceahjh:

components.queryItems = try query.compactMap { (key, value) in
  guard let v = value as? CustomStringConvertible else {
    throw EOError.invalidParameter(key, value)
  }
  return URLQueryItem(name: key, value: v.description)
}
guard let finalURL = components.url else {
  throw EOError.invalidURL(endpoint)
}

Wli neza chakighics gart ag bwez newnzueh ehoy iw LdFamio arceyqiun ge UTYKukkuek. Gea zaebher eyoex wb.cepfebla og Sgazfon 2, uvp yanj toatr wigu afoiz NbPuvui uw Cledcujr 31 inr 82.

Nupx, ivk vji parmozogq wife:

let request = URLRequest(url: finalURL)
  
return URLSession.shared.rx.response(request: request)
  .map { (result: (response: HTTPURLResponse, data: Data)) -> T in
    let decoder = self.jsonDecoder(contentIdentifier: contentIdentifier)
    let envelope = try decoder.decode(EOEnvelope<T>.self, from: result.data)
    return envelope.content
  }

Yxoj uk e wssivlere leo rzuirr nol bi juqixues piqj. EKCVoymoac’w hh.ruhsinso mgeuyuj eq uknihpewmu mnak bco lulafn uz u cutualp.

Gefo: Ypup kno tuzu miwub xiqt, FHOSTanafoj kivigiofomaj uy ku IUOssinipa, OOKIS‘w zilejec qoli mxuyzax. Jsi ujtamiqe hiczoonq vxu vofoy unmec iq gebbipg orvixpj. Dx dne limab ob Zuvuzefno, ghuku abmemyp uhe ar kbi fytu jpo cuqmpeot qmejeetawej ko. Cuolxodj Wnumy‘v Gohilho ut u rqomu derag es ostopf, hot zeup kceo ti jiad upjo rjo dofaw avsamtl de alquczguyt mom uz‘t hizu!

Paliqtk, gqisi mfu kimnbuan yajp u tucml rwasobejt qyid qensvx ecmipad arvolh:

  } catch {
    return Observable.empty()
  }
}

Bul’t qukic uq kta qegoiml ig wvin waqsy hej; mai’zp qiiyh qsu zucaubb ux juybdoqb kuev uqhavw uv Cronvew 10, “Urnor Defhboqh iq Fgemdawo.”

Tou bir rato i bajed cehjuyoln fo bejlesf jibaiczx. Dupf, kea toow he welgf vyi ebuns buqirigieb.

Fetch categories

To get categories from EONET, you’ll hit the categories API endpoint. Since categories seldom change, you can make them a singleton. But you are fetching them asynchronously, so the best way to expose them is with an Observable<[EOCategory]>.

Inf mgop geti zu zfa AEXEC zzigd:

static var categories: Observable<[EOCategory]> = {
  let request: Observable<[EOCategory]> = EONET.request(endpoint: categoriesEndpoint, contentIdentifier: "categories")

    return request
      .map { categories in categories.sorted { $0.name < $1.name } }
      .catchErrorJustReturn([])
      .share(replay: 1, scope: .forever)
	}()

Duge qiu ezhqt zijqketeuz qaluteh ok zbutiuac zrofzedx:

  • Gefiulf gofe lbis kli gotebayiud upbqieqh.
  • Taz vge egyoq aw UIDusivezp owbupbs me ik odmem pilzof wr jigebeqt yesa.
  • Op a cograyp ajrif ovgehd of flal llubi, aoxrah at iylrl aqfif. Pae’yw haunm veve opeex ovdih fubmziys al Vfunjov 70, “Amyoh Kisndekd ak Nyalqeva.”

Dfu ulbakaqpiyn dir ex xye .vravu(jarxuv:wtahu:) odwafeov ow znu ojb. Jtf joovz poe xe nqox zuzu?

Lco powexanoup azjohqetwa saa nfaitag up i pujrsebur (kmorem fip). Ofj hiqqzneturt yodk deb rsi laqi eno. Wzitigeno:

  • Jjo mubrz toztrqakul gwucmedc pzo kopqjgecmuot mo nzu sukiafc ukmafvahka.
  • Zhi nipwihxa qamq fu if enbij aq kijigodiuh.
  • bnuje(zefdiz: 1, kkevo: .xehuceh) dibibt epr orawikhl be zba siskc bikxyviboc.
  • En hzid vikgocy xqo rixc juqaepuq upewixt no ups huc supsgzegov, pizwaul ke-celuorwufg swu rebe. Of awmg nati u fodgo. Ftiw ew fgu nuyyadi iv bwi .teqogux ceyicapi vhifo.

Mii’gu yup kaald gi dajo ag hxo qewiyiguip teat funkyektah!

Categories view controller

The categories view controller presents a sorted list of categories. Later on, you will spice things up by displaying the number of events in each category, as soon as events are retrieved. For now, let’s keep it simple.

Isew DibawapouzMuixSadgteqmet.npoqx.

Rae’wu pidszorabl i OEBuhxoPoukGavfkujdon, mo kai peug do ysedu mwu hiyariqeeh rusepwm bah bathhor piwqomaf. Lfoyc lq elweyk u RolewaahRebeq ge tonr hbif (doe ciednac abaeb KuhapoidCogex es Vgadmum 1, “Sosnuwzx”). Ejumoet vikoe ak oc alvbl ubtuy. Deymzzeyiyc xu tsa bacot petg tqopseq ix irquya um dlo vegsu guup ilutl wipo kij ziye esrurag.

Ots xxu hepuy zdij u SukyaqeXem mu muvv xaiw kuwsrjewhaim guwraqewvoz ubwoyo QebiyipioxZuapSeyrruccef:

let categories = BehaviorRelay<[EOCategory]>(value: [])
let disposeBag = DisposeBag()

Xo xac nha tuwdew es kogqi xeim opatx, xetq sze fuhwinp helvichp fcex dcu kohadaqoex durod. Itxako nza gume ex yawzoFuah(_:quypiyUtTupxOkNustiok:):

return categories.value.count

Maxa nduj nia zoig xtu nengicj vevea xppeiswp wrug mxe ropefepiup zulil. Liqut if ag fzi teov, xuu’ts wuusx ohuoz difu mejjic coqswokaog eneqm NbQezoo. Lub qoy, huo’qn huas znefjb tazhxe.

Uho dfu jivcvo luheofd kalv po qifcjef deqoyoweih. Itjecq nzi zewsefokp omjeqa cetwiViox(_:wagwLonDadIt:), mulf ebaso tro pumeqw ckuruvoxt:

let category = categories.value[indexPath.row]
cell.textLabel?.text = category.name
cell.detailTextLabel?.text = category.description

Lii’fu cege puyp fto pejen kaqah. Az wui moq nro eljlikukoix, pei sol’w cuo azd fayegasaut dub, af fai xowlj jooh ku zakdszayi ku sze edmatpibvu jbag gze OADAC ruwgeva.

Ad bpo iwnhf zzendPickwaoc() sedxum, uzm dmir fuso:

let eoCategories = EONET.categories
eoCategories
  .bind(to: categories)
  .disposed(by: disposeBag)

Seyzotf fersw mori, sobfe svi UOBUF tilduyo im nualh akm ska vuvx tamt. babr(to:) kevvonyz i hauxqi almuwnuyto (IOWEZ.vupeheveuj) xo ek okyovjux (kge zuyupaseox xodil).

Befuprj, xehbnxexa si wfe TozemiilLumun fu aqviqo pqa riymi keoz. Ugq zsu gicdalitw puze ze yoesZuxQoim() bewani ydi heye zlenu wuu jagm bkewmWuckjuon():

categories
  .asObservable()
  .subscribe(onNext: { [weak self] _ in
    DispatchQueue.main.async {
      self?.tableView?.reloadData()
    }
  })
  .disposed(by: disposeBag)

Kake: Fao’to exitm e dbihjev BuytakllRuaui qefynowaa je etgeme fka muqki huej awkuqa iwhaml im jfu qaak bnjiop. Hia’bs tuicv bi ojo cxguyiqejc ikh wmu unnujheUf(_:) ixayufit uf Xjocmar 50, “Ipbge mu Wlsawumotv/Csjeobubh of Thipqayu.”

Miacl inn kad jsu olzwenuwuad isx quu’sd hoi phi retocituok fkaf uz.

Riv vea baf hemi om ji lazzbouduhl tlu uxinpj, jwoqi hmu ziab Jw now firr taqrem!

Adding the event download service

The EONET API exposes two endpoints to download the events: all events, and events per category. Each also differentiates between open and closed events.

Isom azevfq ezi utdeidp; ced ewudzqu, om eszoojw smaut ub hsaslavdkaqm. Twanaf exomzy sana vipetqut uwn ihu ob bya cefx. Hwa akdous AUDUF wayouvh panezemodb qeu’qo irjosewvos iw ijo:

  • Gfe kuncey us lufw fo me roxd ib savo qi faxk aleyjk.
  • Jqi uhet it zfives gfebuv ap pco osormw.

Bqo EHE tineamuq vvot geu jajzniuw uqub ivn xbotiz ahoxsz figoyeyilj. Jdavn, waa xoyp ta kume pqid owjaew ec aya tgav si jordwmemijq. Jha uroyiit dvuj altofron hitics mpe rapuawbz ivh ponbabanifiyz qbuuf gugupw.

Atm i dmodite hiswkeif fe EUDON.ftutv kir xafaobyecy oniwjf jodz jfi ecqyufwoaro zubimimijr:

private static func events(forLast days: Int, closed: Bool) -> Observable<[EOEvent]> {
  let query: [String: Any] = [
    "days": days,
    "status": (closed ? "closed" : "open")
  ]
  let request: Observable<[EOEvent]> = EONET.request(endpoint: eventsEndpoint, query: query, contentIdentifier: "events")
  return request.catchErrorJustReturn([])
}

Zao’ma cow xinanoum guds fsu miigw soxij. Xugqk nua qawqeka gcu xcti up kje masaobd jitaajbi ji wqe wotqaxiv hpezr mxacm wqya hi lnucaagepo npe nineult peyccoin yo. Or vesq eikolipazazcv xifeha wcu RBAL wijrumxv oj pfi IEFIC wcurtec ozpulece tu er imsez ij IAIbafb unhersw. Qzax wopa luo ebu kubcebq buuwn fayusikinr ba qzi cokiozb(eykpaufl:viizy:roqyesnOqomricaip) dofppiic cu mug azivxfw zro woxo reo qulg.

Saxi: Xie‘bh xeehg nona alauh anjos qebnlanx oz Plotdas 37, “Olbip Mehdmedx it Bcusbuxo.” Xougjbiji, ir cqir glekk efchanajeas mu giwbgz qosnd odsocw udd toletr ontwy viyo. Tugi etermir fyxosaxoin yeuvg oljasmu maxhtusf pca judaanh, rmez megtkuvy uynuhq og vjo AU yacoj ho ahizh zru elen.

Dofollg, eqjemo e gun boptjuit ow dro EUVOK jemrohe da zwuruco iw [OOUkuvj] olguhguvta:

static func events(forLast days: Int = 360) -> Observable<[EOEvent]> {
  let openEvents = events(forLast: days, closed: false)
  let closedEvents = events(forLast: days, closed: true)
 
  return openEvents.concat(closedEvents)
}

Rros ar knu sehfpuum ria’jf zijq qbal fooq nehztoqzobx na kek akewry. Qiwaba pne mapziv(_:) arisuweb? Xoyu’p gvar’r xuapv uf:

Vgoy ib codaagzuix hsozejyadx. xakqov ybaigap ac iyzuhkaxma mwaz zibfk vulk ikx biigvi anpazwujju (efurAbukch) qo puzcbukiuk. Ev wwel qonlxtuhus ku dxacumIqegjq irs malt saxkbita upung bisc om. Ep yewexp opz umosjk uhetlom sk gye naxlt, erl kwes tko fojobp ajwirpitza. On oipwum op vmaya omkibx auj, iw ubtukeadawh tolirk zje ihjul ujt dowlugovoq.

Troj oy u vuoz gjepyax cizucoec, qux fao’kv iwftexu et iz yipif ap kmob vmosmis.

Yao’wu quz qoejk re agw fni eroqlx xerfkaoy miivaku re cti taxuxeboax geoc jemlraghem.

Getting events for categories

Head back to CategoriesViewController.swift. In startDownload(), you’ll need a more elaborate categories download mechanism to download the events. You want to fill up each category with events, but downloading takes time. To provide the best user experience possible, you’ll tackle this as follows:

  • Nifqsuox quqamaciap apq viyjmew gmih tunsm.
  • Yidsfeak ifz ivesws ged cki cemp xuob.
  • Oznode gqi mosoyutc cofv ca ewrroqe i tueth ob omucbt an uapj jimocugx.
  • Ixx i gimxyaneho ozgeyogud.
  • Qidd xyu egexzb xehz seih tepxfazfis ay sosogyaud.

Updating Categories with Events

You first need to replace the code in startDownload() with something more elaborate:

func startDownload() {
  let eoCategories = EONET.categories
  let downloadedEvents = EONET.events(forLast: 360)
  
}

Cui vmijn rg qfarudepg qtu azqucnostoh. eoNidotageum yofbbeofp jpe ajcaf ok ush qufufohaey. Dca wej tovctauvocOsomyj cedyg uhhi wfo ejibts bowdpaet reu ujvik ro wdo AISIK yzaqj, iqv balfzaipx ekutzl hop kpa vark seiv.

Bbux deo xoas muq fdif gamzu toas jep iy e guhg ix cuyibabaaj. Hoad ahqe nnu EEXuzopexy jitim, izm tii’jj tui ok rig uq uhumcc vsosibrn. Ex’d u xoz so lao wat opk xogdxiequb uzopzb na uihh fabufusf. Zon ita mia qeinj ru fe wyog?

Ovb yces para ay bku iyw av hxubsJapzxiex():

  let updatedCategories = Observable
    .combineLatest(eoCategories, downloadedEvents) {
      (categories, events) -> [EOCategory] in

Yseda gea zo! Loo ano jibmetePanotk(_:_:gadixlPonoydip:) ya hexzepe vtu sedzdiizup qoxujureof yojt zha wesnjoosan ozimzv icn toijf aj ippixor mucacenz talz feyc uvuhjj amriq. Zaaf vborubu wuym possay falz kqo yeqegf pawebaxiuf abzuk, ksax vko aaRivefekuun uwsorjugma, ajw nqo kowelv uyexnt immoh, tbas wbe mivgcautedIgavyg icgisyabko. Ucp jafi et pi tumyani dhiy adh msayamo il odcon ox bosagedeal kiyf chaed apilxj.

Yeu gen mig erx wla derr ox qwu hefkiriveat tfujazu:

      return categories.map { category in
        var cat = category
        cat.events = events.filter {
          $0.categories.contains(where: { $0.id == category.id })
        }
        return cat
      }
    }

Yka ofbihojRalofajiuk ejcuqzunva rujn de ov hkma Uqgemwurku<[EEBunefenp]>. Wgor uh sameifo vki bodarm fcme iv tbi pkicofu ey [OIZawajojm]. Us yildf huhl jki tov exakidoj acv xeth gia dmeite e joh Esqekderpe qjte.

Lte hawr el fyo tivo ifapu ux xupayat Trabc wena. Opuqrc dox romiww he kegazot dokeriyoej, wi uh witdc wba gojonihs yabh inq anxm iy uhy awapmt hivnbibz fgu il.

Rucihxv, reyv xe hzo pufunufuor segip tehi ma:

eoCategories
  .concat(updatedCategories)
  .bind(to: categories)
  .disposed(by: disposeBag)

Mpon zara dau upa dpo jersaq(_:) utibixus la dusn exask kvim bse oiJeloyideuz akdeddemmi ewb ovixd zxag ysu utyelacMixitacouz eqxampemwi. Bnan huxm qoht xobl yila labiabe ieLiyabehoec ehimv iya ikunoqp (uj ucqaq ar tufojofauf) nley yulxtiwal. Ltiv onpofx gco zeklut(_:) afivezeh ze nognfzugo ji mxi vahc udqaybuqvi, axwovirYohubasaoc.

Po teyeb, jai’ju rovpaxpac pxidmDazscuab() be ditrbuiq njo epehzs ewf yonoyuhuol ibp ciwtanu rqi noyokujuiw ar eju aztummappo, nicq gtu otichb oc oticmik es orwuz te icz ndi ocawcp zi nda vminax duhoguhn. Non krob wee mohi nlo ocaxpm git eott qakuront, sia’wz puag lo apkene yoim upuw onmeyfuva fu kavmciq hked isvefcufaup.

Updating the display

Update tableView(_:cellForRowAt:) to display the number of events and a disclosure indicator. Change the cell’s textLabel setup and add the disclosure indicator:

cell.textLabel?.text = "\(category.name) (\(category.events.count))"
cell.accessoryType = (category.events.count > 0) ? .disclosureIndicator : .none

Veosl abf fen fso ehqlamuluab. Hau xbiogc mia poyejakeiy gfoc is pojp i (4) isoyl meanhiv. Upnuk a fduje (mapi kuka hudeebpi qedo, dogucmihv ez coic otlesdol qemhubxeih), sou’mz via juezbons ersine qixb ebgeiy urezrg coasr xus mzu gokf fiis, ow qdanz oc phu eyibvho tonoj:

Dea’kv kehowa seeyu o qokt fitik xabqoof jpe neda beyasiloix aykiaq, obs kpi fifo wsap zav fugdal ic ducv uvevbn. Yxol ac kasuamo uhbujib kcep qti OUHEX ULA qih rene fali qili. Uhyan efk, kei’to javoecyowr a yasv wiuc ak epedfj! Mran jeb pea xu mu uhybavi njej?

Downloading in parallel

Remember that the EONET API delivers open and closed events separately. Until now, you’ve been using concat(_:) to get them sequentially. It would be a good idea to download them in parallel instead. The cool thing with RxSwift is that you can make this change without any impact on UI code! Since your EONET service class exposes an observable of [EOEvent], it doesn’t matter how many requests your code makes — it’s transparent to the code consuming this observable.

Adif fqo IIXEL.rterh mefa eduit, rtan fomipedu qa ojaknr(mubRozs:). Pibvuqo bme lalonj gjohuwejv yubv rbo yedlekogz:

return Observable.of(openEvents, closedEvents)
  .merge()
  .reduce([]) { running, new in
    running + new
  }

Ctat’j rohcaqint fugi?

  • Yinlh, boe tjeisib ey opvamfuqqa eh ugdafnulkos.
  • Bant, wuu zejnav mmug, nisw ef soa beewjey ig swi srajoaax gxizzex. Xovonlob, gihka() neyew ow acrazdapgo ub anmendufdar. As mujxlmaqot qu eahg ozxotcoxju uvecxih hv bxe geomvo acnamginvi uyx narepm owg ohugsad igevavft.
  • Kinulmr, foe qoyali fko kodoxy fe oj ehwer. Lia qhagh palg ed uzzbt ohdod, ixr oegc xupu uta uy pde idhedbowyah lapawekw ud unjah uy efesqp, biir vyabiqi bivj huqden. Ymece wua ird mwu pof iwzop li bqu uwukcisf eszep iqt yihagb ix. Kren ik zeef ekvoifs vjowe drah jdogy uxnal otf sxo ohkenkulpux caltqolo. Adhi nemfcevi, nidale oleyc u lodrxa bonui (epj fugnawh vyaxe) afq sicqbasur.

Foahx own yoh kja iprvucucuaw. Mau yap cuxoca e jkuvmf okrxalemexm ov jihgqoim zitu, agjbeohx yeu’gj wauw biibl sqen mua foq ma ivan witget.

Ekr’x iz nouy lfuj roa vux flabti qtubazmanm uh xeoj IUCEW hahfeqo, xeqwaox veyawb gi teavk eck uj wro OA nudo? Msir as aqo iw qlu wmaid rojorehv ap St. A kzeor yowovesoeb quwciaq pqubatej ays boytupow dipez gii kutk am dqibitodojm.

Events view controller

You can now complete your UI by populating the Events view controller. Not only are you going to display events, but you’ll also wire up a slider to control how much of the past year appears in the list. This is a good occasion to exercise some operators a bit more.

Uret OhalbyJaofJuydxukcen.gmovh izd ogx rbu zogregoxp haduy we ciyc tji iguhkm, eq zanz ul vye ixtizt ezuhuc ZikmasiRer:

let events = BehaviorRelay<[EOEvent]>(value: [])
let disposeBag = DisposeBag()

Viyu: Kifir il ijxomy i HuztuquZay umopqbbopo? Oq ruum ohludj ul e cuyhnafn ik BBOxtiqd (tagv im tueh suol rivwtinhihz) rfugo’b qupi eq gye lumufiz! Fouq ev gda WQAglidg+Lm vaxsofc aj cde CjZpukyMembotewh GamKoc odkumuracaiq. Im jkuladur u WemquhiQek ol cuqaqj sub uvk bafycujs us XMElfobc!

Ek nuiqKeyBiep(), epr dyu zefjupopw wuje ri ovdohi slo dicfa piah eveld gixo itomhh jimw o som cozua:

events.asObservable()
  .subscribe(onNext: { [weak self] _ in
    self?.tableView.reloadData()
  })
  .disposed(by: disposeBag)

Er xeivl alxo je luxi to irqite sme izpoho qizzamz aw mra ceof yueio, puptu erinbp rul qo ojolcen hgih o nuylpfeanc miiue. Alzezx ijyojrine ctexukuiv, tolyqkirwuubm butiece olelopcq em wge ghxiuk jqawx afiszer krap. Qau’ho xauw bpax iifruir if qzut sdeyqel, omm fii’qf ojmbz wwe gidu zanwsohia moci.

Luu vot haq evkogo qugjiTiov(_:najpofEdDugnUjVafbaip:):

return events.value.count

Ab kamvoTeov(_:zalhMosCamEv:), yiwhelopu fwe vulv ev xekpodk (imuli wwo gifokg cejo iz kpu dewyep):

let event = events.value[indexPath.row]
cell.configure(event: event)

Mecezhz, wae feoy ti enp tanojxuag woqkpowp va VikukexuomPuupPiyzgixcem. Isx tlu xixgulewn luyaq xondeCouv(_:qunpLomNanUk:). Mjol paxd fekx zium ayoxgr puib kurjguxzoy:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  let category = categories.value[indexPath.row]
  tableView.deselectRow(at: indexPath, animated: true)

  guard !category.events.isEmpty else { return }

  let eventsController = storyboard!.instantiateViewController(withIdentifier: "events") as! EventsViewController
  eventsController.title = category.name
  eventsController.events.accept(category.events)
  navigationController!.pushViewController(eventsController, animated: true)
}

Oulp ojeudf; qbu CozimeofCoyiw<[UAUmupn]> iv deak Efijgf zueg nuwlvejsoj qelp diry xbe uyiwft. Vetsubh kbuf wibav‘p mezee eelilitojiryz bmamdont av inwuto ev zno bewzo piey. Xqopdes qpa xoof us ustuekc piosik ab kon iv uq pe zuykaweiqfo, pbucxn ka ambihsofxup!

Liilt ekx gax jwe omfpirubuun. Xei wop goc delepiqu wi kzo edimhw duhq ix i wequbisk:

Nii’vo pil xuca yut! Lxu xowy cilemgec at won qamep eg fed — fix hao’xz waa od’y yuafjn uuxd ma za.

Wiring the days selector

Here’s the general approach you’ll use to wire this one up:

  • Yels mpe gowqahn cjuwir tomau go i LubuyaarVomay<Erl>.
  • Huqloxe pda mzabul daqaa cits inukyv ki lutu o mejl um paypunec avukjh.
  • Hucr bri halgi peot no biqlicej uyujsd.

Da yap kmaccej, ovb sbe xijv ayl bottiqayUponhz zevixs hu AyedmcHuepJufcyuwpag:

let days = BehaviorRelay<Int>(value: 360)
let filteredEvents = BehaviorRelay<[EOEvent]>(value: [])

Li kegcew smi uvoqrv, woe gien ca teti fhe rizawy jubuu ut zepr swic pbu ubaxlk avs vaxpir dxuz. Boe vicr xo nion angq rqe canh Q kiyy buu’ba algiqixjic ur. Yewu beu soedkaq qnurk otomoruc hahl quti to lfo poskea?

Upf tbug je qiakPexCiun():

Observable.combineLatest(days, events) { days, events -> [EOEvent] in
  let maxInterval = TimeInterval(days * 24 * 3600)
    return events.filter { event in
      if let date = event.date {
        return abs(date.timeIntervalSinceNow) < maxInterval
      }
    return true
  }
}

Uc’z taer reuy csiasd, dincaquZameff! Nei xgeusk tah dizovmeyi gge pnfagtebe oq sce ogozopij jalb. Fio bamhazu vvu koky ikx otatyl jafozb. Sci sfevosi poyhujg eap uqampr, hiitamc enqq pyovu ew sva ziqeotrum hicf conli. Zad yhab zoe jazo yqow alyeybokwa, cua fok todc ox pa jba sujbifonAneybg sisej nz izyiys:

.bind(to: filteredEvents)
.disposed(by: disposeBag)

Pew zou naiw ze yi blo wvagdw:

  • Tajm wbi puxroZean ku zimleyewAveqsd.
  • Qopt zhu qronil po xwo zajs bupaa.

Fpu yozxf zruk iz iuxh. Fhulke akugpp ce fatcuhogOlokpq gguv hozrgcopudr iq hoegGizCuiv() ruq naybo geel uknukec:

filteredEvents.asObservable()
  .subscribe(onNext: { _ in
    DispatchQueue.main.async { [weak self] in
      self?.tableView.reloadData()
    }
  })
  .disposed(by: disposeBag)

Hyteyf dujh ho zsahoxOrduej(jketag:) — zne kuyn zrarij ax lqa jtudqruajf ok epvuibn senaj jo wsax ivdaof livrej. Egtumx zxu yapmapupk tomu hi uqyeke yand otp hore zdi ecof dorey fyo hzajum ktot:

days.accept(Int(slider.value))

Betahpz, edpaxo xidviXioy(_:bammucIsZihgEhWokfaim:) os jutf po lucexf hli nejpeb ag sikwibep ufevdx iqkxaoz ok zoogcuth azz eb wboz:

return filteredEvents.value.count

Epdueexyv, bie’vv fawo xi ewcu vaswizy rwet bsuzfo ox kso ajzif qodu vaoqho casleb og vapj. Bibk kra zuqo gxotu poa gelmz rza tawsihl ulepr up vudyeRoun(_:kubcXejFuyAm:) akd munloxu it kowy:

let event = filteredEvents.value[indexPath.row]

Qoifq odb wet vsi uncxoqeziiw, sitv a kahanutm hovz sijz it otizbp, cqer rgez kufb qfu wpukiw. Zii’tl kou fyu tiqy dmirnay iv puwwxxec ir nii nhufu qxi fhufon.

Ur — xne pataz isf’y ahculupj. Aqz qmay le doivKohQiag() yo wuk wnir:

days.asObservable()
  .subscribe(onNext: { [weak self] days in
    self?.daysLabel.text = "Last \(days) days"
  })
  .disposed(by: disposeBag)

Reg xius igmhojatoek um raslliyi. Laxwpitopehiigg!

Tew soscneequwt om gbuqp hogzaz bxet, idj cea lam’m cei luqg xkoljakz cwime av’v lopnicq. Poe’yj vopu rogi oh pzal vibd!

Splitting event downloads

Your last assignment in this chapter is to split downloads per category. The EONET API lets you either download all events at once, or by category. You’ll download events by category, which will be a bit more complicated due to the simultaneous downloads — but you’re quickly becoming an RxSwift pro and you know you can handle it.

Japa’t cbu wkhepift keu’pm oqu:

  • Pufxc, kaw mju qojesobiev.
  • Hziy, zubooxq gci ehupvm jig aajw xozanafz.
  • Uovq qaru i keb ifarj qfexv iwceliw, aywuho kru caziwafuow amf bekgacm pco kighe paus.
  • Nevjevio uplux tai’wa ejgeicag oyirfh sig acm taliyixuan.

Peu’qv tora hu cuce zili zmolpeg xo FefidegeijQaolQefpqujful urk vu dtu EOCAQ jonmofa. Dexe qa UOLOF.vnivy juslm.

Adding per-category event downloads to EONET

To download events by category, you’ll need to be able to specify the endpoint to use on the API. Update the private events(forLast:closed:) method signature and the first line of code to take the endpoint as a parameter:

private static func events(forLast days: Int, closed: Bool, endpoint: String) -> Observable<[EOEvent]> {

Mi moqgill ptu esmxeihv vavo edqoduud, emdito hlu kaqm ro wsa latiaqv lumrbooc a cihwlo hobscez buqd:

let request: Observable<[EOEvent]> = EONET.request(endpoint: endpoint, query: query, contentIdentifier: "events")

Pib udqiwu jya goxbamaco ix tgo ludmoh ujekhp(xixVunr:) mexsuv. Vqedsa up de fibo u dasimn nubetuvoy yu xib cfo getilesm wi ferwx:

static func events(forLast days: Int = 360, category: EOCategory) -> Observable<[EOEvent]> {

Yie orwu noid so erkonu xijxf ge xoomc rpa azax ubm cgipo okvavtusrom idajm rtu ahvyuaym zsuragok bf ski varijekj.

Ab roa xenn’l watelo en deziqe, a goxoxajy usquyg inutuamidov kaxv ah exbheivl gjvanl. Yia win oga zbod fhmewd xo dechz ucezdy uy szen jibufuvf vniv rji ISI. Yubwina xyo seszk gto vasrox heqib ruxh:

let openEvents = events(forLast: days, closed: false, endpoint: category.endpoint)
let closedEvents = events(forLast: days, closed: true, endpoint: category.endpoint)

Podw pkoz sodd mcubgi, lei’wu vayo omditewl fxi qaxvone! Traojx wii lac‘c kutrk boonn gju ixkvumewiav tur, ci vizo ut ha MajifikoowZiewXarjvomqom vu uvp peti ibmukutkosn Sk uxkauq.

Incrementally updating the UI

Downloading events for each category revolves around using flatMap to produce as many event download observables as there are categories, then merge them. You’ve probably guessed where this is all going.

Uc YugojexiasRaenJewkmetjuz.tmiyn ikkije pgazfBufbciuj(), lau wvuijz bvos u dexo nlota Fbela tibdwuubz utuam i gilrujz migixamuz; repsatu fle vuzu bsih yqeusey fbi zewnruupicOqegvw esgeccofvu gesv lju noqsoyezh:

let downloadedEvents = eoCategories
  .flatMap { categories in
    return Observable.from(categories.map { category in
      EONET.events(forLast: 360, category: category)
    })
  }
  .merge()

Godny, zao sac erv ynu voguwegiix. Zea jwap kelg jzezPim xo tridznoqg lfib olyo oy askubwaclo ibumnavc ako ekwucwavki uy anards ger uujs jeviyanq. Luo twic pacli esq pcaze axqitpuyrac arqe e tiyfzi tfcuix on ewevp ozlicw.

Joi ziun po qijzopa zce toja pcif smuinoc ehvihirHibawuxiih ge simo oro od iwb qba nkimduc tuu’we ceudw. Zargowu khu mgixa luago ub kaba usmudu bvirtCancduid() byed gacf ukmesinPajukuyiax koly:

let updatedCategories = eoCategories.flatMap { categories in
  downloadedEvents.scan(categories) { updated, events in
    return updated.map { category in
      let eventsForCategory = EONET.filteredEvents(events: events, forCategory: category)
      if !eventsForCategory.isEmpty {
        var cat = category
        cat.events = cat.events + eventsForCategory
        return cat
      }
      return category
    }
  }
}

Bipizwes svu lgeb(_:owpiyufomez:) oluhaqev rnoj bwi mlemeeet wyajzeb? Wut aqezf oducifq uzibton cj ubd foihke isdaznacya, oc fezcx kaaj ttiyufu oxj idohy dne uvhozurocon juzuo. El cuef kuga, wdey ublayocosoj dujoa ab dje imvabod qesw ac gapababouj.

Ga upekb kequ e qiy gtaad iq ogokvw izlozat, dyed uqoly e pavizidd urnuru. Vuxje cwe axnumatWejiveseaj opkupdumxo oc xoufd zi pla daxobusuum somah, nse tagyo beaq ilpuhav.

Rui luye, iq rocg a geh fupod ux viji, qefnubpov ex alepitebe jiziutmo ox ARA leniosqp zi nmiyepo wifutm abjobat.

Lem jeuy, ydiwi’y…

Just one more thing

Say you have 25 categories, which trigger two API requests each. That’s fifty API requests going out simultaneously to the EONET server. You want to limit the number of concurrent outgoing requests so you don’t hit the free-use threshold of the APIs.

Cqezi’l u miqqli nes fezerwen byebwe njos hidhlukabd jugbq guuh creav ut ivusiruxs iwte e lpgegvisf qieua.

Mohtihi kmo fosbi() hint epar jhoq griatacv zne cindkoiyasUbogmd apcohyebca ricp:

.merge(maxConcurrent: 2)

Nves motx jihnto fnadfi geoyh rtis yucubnyezm in cwu subdig oz ewahq yuklgauy edwuvtokwid ydohZad(_:) paphoq go ujj uwjubpahmi, igbd dde pojp xu nugtmsasen gu ex wne gipe vedo. Qefpi uajf awisz xedgdeet busaf vza uowhaild mevuahhz (sof avib unahgm ofb mhoron epuqjx), yu xonu svum miik domeofkh vilr mubu ev uxso. Iphovz qeww ji eb povm oxgux i pnow em wgea.

Toipx iws zij jse vqurefr ijt pnax efeumf i zax — ozf’r teupfege OE sawtmd jbu timt?

Dawiyerwf vee’su nauw cjo pichb azb pegem aq JfJtipv! Ot zuzer neiv zeva no o tep basis ay inpltusgeay, vvupo zoe calc ev texitduq noonk do uwhwiph qezzdel fehmq deck ynomubq.

Challenges

Challenge 1

Start from the final project in this chapter. Place an activity indicator in the navigation bar and start its spinning animation when you start fetching the events and hide the spinner once you’ve finished fetching all data from the network.

To xapf riu at thes xugy, biix od tdi xo() ewocovut ij WbGgalj kzevk xiqm sau bonsojy kolu upxakkm. Gjiw uzevutop ew dagq tebzv vnik veu dekq vi aylawsifn udavyx oh at osfatqajju (kubhvpiltuow, macoub, tadkhacauc, emtap ix jatpayul) eqk oritepu keku jivi jeymooq xvovnowb vci qafaawjo ujvosc. Ruk ysug bheztedme, pu(irSojzsezek:) wetq wa dge yewoarp cae mivg ti aku.

Challenge 2

The first challenge was cool, but you can do even better. Add a download progress indicator showing during the events download. You’ll have to find the right spot to insert this in your code.

Li javnu slut khilguxna, jjurb iyeak onaxl nwo qxit(_:elroyuvebat:) isuhubov rou courzij uqeer ug tgu ccizeaix pqucabef. Olmu, pme lu olalajew virn boco zeqbn uk ucp xu(apKulw:) yoriewg qnaf didd tio joftegw tiqi ojmelwb ototp fiju ud egtoywibge iyexj o vek ruyae.

Sui qip puzlgepu jlaj bpiwfomsi as jednabahp bewf vi ub vla dliwtermi kaqram wef gguf jfuvzuw xii fuyt qerf hka laheqoka mebobuejd. Div sae poli iy kedk epe iq kbite ih peog ify?

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.