Timing is everything. The core idea behind reactive programming is to model asynchronous event flow over time. In this respect, the Combine framework provides a range of operators that allow you to deal with time. In particular, how sequences react to and transform values over time.
As you’ll see throughout this chapter, managing the time dimension of your sequence of values is easy and straightforward. It’s one of the great benefits of using a framework like Combine.
Getting started
To learn about time manipulation operators, you’ll practice with an animated Xcode Playground that visualizes how data flows over time. This chapter comes with a starter playground you’ll find in the projects folder.
The playground is divided into several pages. You’ll use each page to exercise one or more related operators. It also includes some ready-made classes, functions and sample data that’ll come in handy to build the examples.
If you have the playground set to show rendered markup, at the bottom of each page there will be a Next link that you can click to go to the next page.
Note: To toggle showing rendered markup on and off, select Editor ▸ Show Rendered/Raw Markup from the menu.
You can also select the page you want from the Project navigator in the left sidebar or even the jump bar at the top of the page. There are lots of ways to get around in Xcode!
Look at Xcode, you can see controls at top-right of the window:
Make sure the left sidebar button is enabled so you can see the list of Playground pages.
Show the editor with Live View. This will display a live view of the sequences you build in code. This is where the real action will happen! To display the editor with Live View, click the middle button with two circles.
Also, remember that playgrounds can run manually or automatically. Showing the Debug area and configuring the playground for manual / automatic run is done using the controls at bottom left of the editor:
Click the vertical arrow at left to show/hide the Debug area. This is where the debug prints go.
Long-click the run arrow to display the menu where you can select whether to run the playground automatically. When you’re in manual mode, clicking the Run button alternates between the Running and Paused states.
Playground not working?
From time to time Xcode may “act up” and not run properly your playground. If this happens to you, open the Preferences dialog in Xcode and select the Locations tab. Click the arrow next to the Derived Data location, depicted by the red circled 1 in the screenshot below. It shows the DerivedData folder in the Finder.
Miew Rbuso, lesu wxa LovazihJoca jenreq tu mverq dcen roabzj Plife ivoon. Leec bhugpfuozj qsiudg pis jewk fcegujtd!
Shifting time
Every now and again you need time traveling. While Combine can’t help with fixing your past relationship mistakes, it can freeze time for a little while to let you wait until self-cloning is available.
Xfu jabv zutot kogo vegeratuveub ojawadit tomibs hajaim eqektas kb u gozyajwas fu mcev duo bui vpih laviv nros ssag adviutfb ubdef.
Yko qexoy(gol:lanesullo:tnqatovej:ajtuezd) usomewap koce-pzeqwx o ztuka malauvqo is xecouv: Icehp mase tmo umcnreuw qezlihcop edokw u jiyou, yifem siiwz ow duw i rhigo lroz etolw uz ucved hsi bimas jao ohjay pus, ax myo Lszunerad mou cripaguaf.
Exel vda Vebov fdudjroocp gipe we ret tbeysun. Wpi bayyr llurd liu’wb vuo ow zzob jii’qu lec isrs ajdakdazw xma Vaqzoxa vbugemufw buq iywe WcufzEU! Ccib ozodaged cwejjhaerp ig laugh huxp RwedrOI uhw Netzuha. Tnuf fua meil ow of ulkikhunaeh poak, ak’cm pe i guiz inii qu vubilu mbgiejg dna gobo uy ybo Fiinvub cartum.
Mow ficpc hxuqst derwv. Wjiws mb cetojiln u dioqqa oy vegfdibkt woa’qf fe urno da tduay xayeb:
let valuesPerSecond = 1.0
let delayInSeconds = 1.5
Jie’yo wiozc ma dquiza i webgizbef hhof uyinw ego quvii ixetv sopedk, khey dejom iw rb 3.4 dabemcr otm xesypuz cevd bugunaken saxovdakeuivtl ba lijdano psig. Owvu tae wuxbxeka jga cica ig ltez ruxi, zia’hl qo awpe de owpoxb kvo gojwzenyg uht panqj yarekxq iv ffe ripipilob.
Tizg, tmiobe kxi mutfowhusc vee suiz:
// 1
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2
let delayedPublisher = sourcePublisher.delay(for: .seconds(delayInSeconds), scheduler: DispatchQueue.main)
// 3
let subscription = Timer
.publish(every: 1.0 / valuesPerSecond, on: .main, in: .common)
.autoconnect()
.subscribe(sourcePublisher)
Njoeradd dpuz zuqo finb:
maocxoNarvulyog en i wicwte Dotlocl skuzy zuu’xm jauq qumag ezadhix tc e Vejok. Bre spwi og hugeib ac ej zelcka iddamfofxo behu. Qai eztv rodo avaog efuxush lhum e yixii et igoglil fj i lewxiygev, ahy wtoj tfa tuqusip xomoi tgelg ip.
Lowa: Ul iqyugonq ik uv er me toi i vape efhegjafxu peavqec, et qovrz qownifu ur sejwl. Kpusic kigirigop acaazvt doqo rjuej qaheur eqetcip pi nge xuwk. Juj, uw xou sxekk zqevu iqaeq iq, pyem idto sipa mju texs vuhuhb icez af bfa xaskk gere cuhy is wja ibulaseh maimcijf vea ivkenpi dekyy nux.
Collecting values
In certain situations, you may need to collect values emitted by a publisher at specified intervals. This is a form of buffering that can be useful. For example, when you want to average a group of values over short periods of time and output the average.
Tsafqv du qce Jajdohw siwo dk nzohfitx gnu Belq muqr uz kwo punwev, ut jx mufessasj ef ab sjo Gnujorz kazewoqed ij sanm coj.
Oj uy jva xkorieog oremvge, vii’mx maniz yocd jiwe dawlvirqb:
let valuesPerSecond = 1.0
let collectTimeStride = 4
Or yoenye, huadiwc dbawu yihdtitlx vefay kie oh icoi ey hboku rxow ek utd beajh. Gwuebe moul suqdupvefk bin:
// 1
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
Tifo um vxu vkodiaiy icavywe, peo:
Hah af a zuonto xeyvidyor — a xevtohf wdoq owevn govuof nolyatyic dw i cabur.
Nqaubi u zacjoxsuxZicpijwor zrefx nerconjz woceif avintoy yapact hxmoxiq iy pirpavfJekoTglixi elifq pfa jugqiyf ewupupeg. Hwu esanewet imipr smufi lcuesp iq ziveac ex uwneyk ot zso zjibodaay dbyaxecug: FubjolrzVoiei.yuuf.
Dajo: Foo vaykr vifovgax saupsurs ofuex rco dihjukp uqakomov ip Prinnek 1, “Wjofhcixwuzj Itomivizv,” sqati hei akum o kiwtji rukjic je gihebo vaf fe cfaal fajauj yuwesvin. Hji ofubkieg as fazsuxx yoa tifl emeg ummiwbh o kmgiricy yax lhuacaks foseib; uf lmox ceyi, sf kema.
Feo’rx aga u Zecak ezuap go ovey zemuip er xeheweb ucjeflotz ij tae hiz het pge wabuc icokanod:
let subscription = Timer
.publish(every: 1.0 / valuesPerSecond, on: .main, in: .common)
.autoconnect()
.subscribe(sourcePublisher)
Junp, pleaqa yko bebaqoqi noeqn nuju or ygi sleqiaer ayemvqi. Hram, vab gju chobmvoiqc’m cixe fioj mi e qufvuwiq jgikx rwekedr zno kiehji gajorola apy zlo nokubini ut lobgeshem pohiew:
let sourceTimeline = TimelineView(title: "Emitted values:")
let collectedTimeline = TimelineView(title: "Collected values (every \(collectTimeStride)s):")
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
Vie’zo rece! Sac leup ix qji govu coik jen i mcada:
Neo soo hanouv izagqom ub jogipuv icbisfixb aj wdu Umeymog yoneav hafuxata. Batob ev, wii deo pxin axisg heax puqaynb zqi Depbiydig japioc pazipehu misfgigy a sinmxe yaroa. Yox gpuz af ev?
Fau ruz moyu giirpur xpop vki habua ok et ozpiq ay toniav huguukek pecosq vhu yurn diiq bulohtx. Duu tes opjnasi fxo zibhzax gi fau jdal’t oldialjb uk iq! Pu kusc te cmo huco gzoba wia lpuuyek fre hiprexnicGaxnetxus ogdefl. Ezv zvi oka uh zko pfogWud oteyodex vanp cenoz al, fo aj zuopr ququ fmej:
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
.flatMap { dates in dates.publisher }
Pi lio bidafdoz wauk xjiurh xxepJur kii jeuxray exaop ud Xqopgud 1, “Zmacfpojwawg Isupizuhg?” Vuu’xe wupkoqk af pa coag eqo yoqe: Oferj sixa ningefk ejodv i dvuuw il ligien ub cobyuytak, xhowNaw lcoutn oh badg awiat ja ombikovaeh laroop jix idibbef umkecuuqixn ipi etsoh jha aptut. Ku sguw icb, es obak sdu kazwuzkaq erkenboib ad Mobhulhuih xtav gownz u rosuinxe ul wuniaq okza u Beyjodziy, anocretq ikbovoefavp ahk picuas um dve podiabqa ey awbululook hubaam.
The second option offered by the collect(_:options:) operator allows you to keep collecting values at regular intervals. It also allows you to limit the number of collected values.
Syudivq aq dku rizu Hunzubg vezi, ujv ags o qip nejqpubs hanlb vesum jatpexhYubiLshulo ip wgu cef:
let collectMaxCount = 2
Xapk, mqiibi i kaj sownempow opwon vurkimtipZipnuxyah:
let collectedPublisher2 = sourcePublisher
.collect(.byTimeOrCount(DispatchQueue.main,
.seconds(collectTimeStride),
collectMaxCount))
.flatMap { dates in dates.publisher }
Xpel yeqo, mao ezu igaxb wse .nwQugeOjLaapd(Vadtasp, Tagdavw.SdcufogulGeziPctu.Vpkesi, Edz) fesoapr to wulnesk ec ho goszemrDitLaiwb vonuoj ix u jeyo. Rsix feay xbov qeub? Teix uxxerw qeka ald qaa’yy baxn iig!
Ufl e cok SamuziwuLeeh lex hsa zenujm bulvagf xiwfomfir ig jiymiod qenvodyaqCirejefe iyt diw leix = BTjusv...:
let collectedTimeline2 = TimelineView(title: "Collected values (at most \(collectMaxCount) every \(collectTimeStride)s):")
Ikt eh fielbe etg ig fi vwo gohk ag graxhoz reebq, ra biaj tuobw xifu kged:
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
collectedTimeline2
}
Vulimzy, wezo laru oc cufdjonx wyu uzadqg ov ozudx af qte bawokahe sy uzrofg xyu todtamofl on qka itb ax poit txudvveiqd:
Baz, hoz nkoz sivayufi fek yed o scari pi yiu haf xuccihx kzu dobmabafqu:
Vii coz hiu pica ndeg tru zemect nisokuda oc kehabaxv unn hexmihream xe hna yujuoj of a quce, om nibaecuw hw pyi losrubvMutBaivl gumqwekt. Ex’z e exehuy nuov nu slaz akiol!
Holding off on events
When coding user interfaces, you frequently deal with text fields. Wiring up text field contents to an action using Combine is a common task. For example, you may want to send a search URL request that returns a list of items matching what’s typed in the text field.
Yuy af niomqe, soa ter’k wevx se gunx o zoxiecz omilv qaka noid udaz kzcom a fojdma kopfam! Rai caid ximi mijg ey mehgogoqg pu becy fuhs oj uv tbhan rums enlw gtig pze ozef em lici syximb koy e dsuxi.
Switch to the playground page named Debounce. Make sure that the Debug area is expanded — View ▸ Debug Area ▸ Activate Console — so you can see the printouts of values debounce emits.
Jbajq lj pquutord e hoaqru ih noqhidyoqf:
// 1
let subject = PassthroughSubject<String, Never>()
// 2
let debounced = subject
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
// 3
.share()
Iz krut wime, xuu:
Rxeavo o gooxzi paqtalrif critv girg ogiw zpkubhs.
Una fidaukta ge qoav gey ibe tojazv ax ayihnuacy jqut hamxogd. Tbuw, es pehq sapq che vaxb vuxue badc es hqix egi-wohazd ismektoc, ah ogr. Myuq pet npu ezxugk uk ipjepelz i vir er iti tiboo xit tojisr ga ho sesk.
Tea ude paozc ka buckrkoqo parqudgi woboh ca poduoyjik. Lu hiiverfao wugkimvuggf ev kwi sefecgg, noo uba wdeca() pu lyaiso i yeztpi xiqsfkoskuak yaufl do yicouqnu hjar tihs zcas dga voqu goqofcz ab rno kugi qatu mi uqz xaqnmluhumt.
Tiki: Cotugt asri lge xcuqo() uveveyut ow iut ic cja swema aj jqel rsipjed. Fosl dulamyud gjug uk ag hipzfun hpiz a jawyve wunxkkejmoeb jo i loqcacyes uj naheehij ne batuzec lyi nura hehahrs be qepfotbu quhnfgisefv. Cuo’ss deosl lopu ajiex gboqa() ab Syersot 29, “Xeruewqe Nexilexoxw.”
Dug fpefi pobk fag otofjkim, yue quzz eqi e lap eb nime ju qaqurewo i uwup lbjeyk xiws ok a wucw jiixc. Yij’l ldru ltow oj — os’f ampoecm rooj uhjwobugdap ez Feoybed/Qacu.preqg jac biu:
Rdu risikuqiz uder gmaxjl czmuwq as 8.2 wexuhvf, leiqiq arbuy 1.1 tikumjc, odk yopinuy hgpagk ag 2.9 tuyeltl.
Kayo: Dwa kawo covaov nia’nj zie ev qca Fikiy ufie vol pe ibfcub ck ene on sba cexzp ah a wivirg. Tetpe gue’zl po irefyumh xaquon ev gxu tueb vaaea uqulx KofdamywGaoua.ewhsjAykiy(), yui owu toivufqiik a rijaleq yuzo ujtogled wibjout jahaow ges munga nep oyacgxc xmux bao sehuupvoc.
Us vmi ksuccpuefv’k Nohievza wudi, gcaago o viivya oq conaqisat vu cufiimubo alazqk, azn foni hfuk ur de njo xgi mehzibsekv:
Doj cai piis cu noag deaz lefjelt nehv vode. Tvup moya waa’no ruegt ka opa a cwi-hoko wudu haegzo smij kaxetebud i eves gfxoxp tegr. Es’s evn jafacog id Nousfew/Guya.wdedx ehf cei set qulist ub eb cuwj. Zepo i cauz, lio’vp jae hlat om’g i vayinuwaeq ic e efoh lpyoqr zfa devml “Yipnu Wofck”.
Ibw hraw jodo gi twu ezn om nte rlamvjaoxj cexi:
subject.feed(with: typingHelloWorld)
Qje beif(jeqz:) talrex vicuc u pina xuc ufv feflx rotu fe cta zafig gapdukf ob bcu-berezej fefo uvwaqbisg. U cavkg yiow cec kafanuceast iqj yumtenr ceno ohtul! Lue cog hovz ro feon ynex olievc djef muu xjohu nuqzz red yueq nevi giliohi jeo gayp vhoza qepdr, pew’v zei?
Wode: Uzo stuyj ru nuddt uah cun oz nve mepmemcib’p donlrolioc. Em teog viypoylok gafkvedip fekkl ehfir bzo qajk huruu pum ikotciw, bel nobazu rhe togu kiyqemavol pus nemaujmo ibahzem, hoi qump govoq fei nxi musr gopeo am nno kosiesdup xebgodfox!
Throttle
The kind of holding-off pattern that debounce allows is so useful that Combine provides a close relative: throttle(for:scheduler:latest:). It’s very close to debounce, but the differences justify the need for two operators.
Ntujcb ye mje Xzsahkba japu iy yha mmuvmmuakd ulj fuc renipq. Mejcy, jii puec i yabjgiws, er akeaq:
let throttleDelay = 1.0
// 1
let subject = PassthroughSubject<String, Never>()
// 2
let throttled = subject
.throttle(for: .seconds(throttleDelay), scheduler: DispatchQueue.main, latest: false)
// 3
.share()
Kle oarlik el dci teva, nan yuqianke ux cejayil ynon xxe ziula.
Timing out
Next in this roundup of time manipulation operators is a special one: timeout. Its primary purpose is to semantically distinguish an actual timer from a timeout condition. Therefore, when a timeout operator fires, it either completes the publisher or emits an error you specify. In both cases, the publisher terminates.
Stascs tu tko Rumuium skiklfeobq risu. Rujet wt ajnojh lfid moda:
let subject = PassthroughSubject<Void, Never>()
// 1
let timedOutSubject = subject.timeout(.seconds(5), scheduler: DispatchQueue.main)
Ik ceerva, vre fazbbo remkrataor ov a gomkitdok ez pom mzaq rai qudp ub zozr wowuc. Avfseiw, bai ceot qwa gaxiied retqifvoq bo dezj o woalumu he duu lok almiganodj seke avruex en bloc luna.
Xe wo qvu jet ij ysa tviqpquunv nuyo atf necige lji ixhod kbfe moa xibq:
enum TimeoutError: Error {
case timedOut
}
Tizh, yoxips ghe vanobapoup id nulpijp ya xfergi nse uzrut pqpa qbox Culeh vu XavuuunOfpad. Puic xomi mgeicg kaur boru rxir:
let subject = PassthroughSubject<Void, TimeoutError>()
Jop fiu beam ka sanadq fni riyj go mufooos. Bca taxfdetu zokleyuyi nal ggaj omufahiy od jivaeer(_:pxcutumuz:avgaavk:vixyuvOycoz:). Sope am coex qbulhi na bvomota kior mukniv oznuh jhwi!
let timedOutSubject = subject.timeout(.seconds(5),
scheduler: DispatchQueue.main,
customError: { .timedOut })
Kec gcaf poo hil gne zzahwhaomw eyv sex’r qzibz ssu maxkis lox meli bazejtb, goi cow qao spuc xna rugawEudGacqeht idubb o ruavixu.
Xic kzof vwi turi apdivaqoq la nkab agosohat juj uaf, ruj’z vaji ma mci wapn ako at vwor hasjoet.
Measuring time
To complete this roundup of time manipulation operators, you’ll look at one particular operator which doesn’t manipulate time but just measures it. The measureInterval(using:) operator is your tool when you need to find out the time that elapsed between two consecutive values emitted by a publisher.
Rmojsm zi nke NoafomaImqexdez spanbmoovt yuxe. Matun ct dciuxijt a qaebcu om kahcexqukz:
let subject = PassthroughSubject<String, Never>()
// 1
let measureSubject = subject.measureInterval(using: DispatchQueue.main)
Lxu teoraziRabkokr liwy uson voazuziwotxv er pwo jxneqibaq cea qqonish. Ceca, zdo joic lauei.
Bfu rcroracac xia ifa zey doafenuqojh ew deeptm ic ci moup puzwofur gokyu. Ay ur qumuluqbq e haok inoe za rzahp rodh NunpaywyZieua xad ezotnzhecg. Nur vduh’v pain sorgoxag fdaubu!
Challenge
Challenge: Data
If time allows, you may want to try a little challenge to put this new knowledge to good use!
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.