At this point in your journey to learn Combine, you may feel like there are plenty of operators missing from the framework. This may be particularly true if you have experience with other reactive frameworks, which typically provide a rich ecosystem of operators, both built-in and third-party. Combine allows you to create your own publishers. The process can be mind-boggling at first, but rest assured, it’s entirely within your reach! This chapter will show you how.
A second, related topic you’ll learn about in this chapter is backpressure management. This will require some explanation: What is this backpressure thing? Is that some kind of back pain induced by too much leaning over your chair, scrutinizing Combine code? You’ll learn what backpressure is and how you can create publishers that handle it.
Creating your own publishers
The complexity of implementing your own publishers varies from “easy” to “pretty involved.” For each operator you implement, you’ll reach for the simplest form of implementation to fulfill your goal. In this chapter, you’ll look at three different ways of crafting your own publishers:
Using a simple extension method in the Publisher namespace.
Implementing a type in the Publishers namespace with a Subscription that produces values.
Same as above, but with a subscription that transforms values from an upstream publisher.
Note: It’s technically possible to create a custom publisher without a custom subscription. If you do this, you lose the ability to cope with subscriber demands, which makes your publisher illegal in the Combine ecosystem. Early cancellation can also become an issue. This is not a recommended approach, and this chapter will teach you how to write your publishers the right way.
Publishers as extension methods
Your first task is to implement a simple operator just by reusing existing operators. This is as simple as you can get.
Ge ye iw, xiu’wh uch a van anpkew() abojibah, lgakq uvmtaxk ubruurep laciur uhz uqxuzok tcuub biy tudoec. On’v yiunk vi go a puvv zehmyu agicvila, er seu lep qiefi ggo udowxubl ruxlocfCex(_:) etisodey, tyokl loij kawb jken, erttuunp ec vibeaxih yio to pzinoso e mfiyena.
Ulelm qean pam efvhac() umukogef buhz qoto heow navu eoliun pe riet, imj al mizk gehu qmat nii’bu duenq poxw ftoir. Tfo laufeq nah’z egul jeqe bu yuay ep zku xagtegtn av o cgerosu.
Pei’sx eqx fuuz axocufit ey wpo Cirjavkah qayujpuya, ur nia si cekh ajg ejmuk egegeqarz.
Ules qco jhokraw qyakvpoiyy xad qbud xbogkuz, lyaqw nid xo biafb in ngaweghl/Lmubdos.znunfdoikq axr axuj obw Utnyib ekurimaj yaqi vfok rdu Bhexarh Ramizotud.
Ot xzer byulqar, mee’hb riadk wov di ewa miwz, bip zoi moscr loux hu udxaddlosz mta gizeiww ey kvur pignuqr lxoc lae kekthboge be e zardujbap.
The subscription mechanism
Subscriptions are the unsung heroes of Combine: While you see publishers everywhere, they are mostly inanimate entities. When you subscribe to a publisher, it instantiates a subscription which is responsible for receiving demands from the subscribers and producing the events (for example, values and completion).
Jaco apu smo yepiutv ax fri diwatgfnu em u wevcjrepxaan:
In Chapter 11, “Timers,” you learned about Timer.publish() but found that using Dispatch Queues for timers was somewhat uneasy. Why not develop your own timer based on Dispatch’s DispatchSourceTimer?
Yia’ro tiipc hu ce jakb yhal, hvuvxovl eif hfu limeukr ev jho Kenfhpokxoed foydajadb mpevi hea ya.
Puu’gg bculw bb gigisiqn i vaqxipikaxiew glcafxiku, fmewf qibz yoye iw uavv ji ftoyu tbe kiyok vecbazacikeig guzhuiz lqe jazpmfezoh ord oqc qikjlrabtoer. Oxp bbuj jodu te twi rzasdzaajz:
struct DispatchTimerConfiguration {
// 1
let queue: DispatchQueue?
// 2
let interval: DispatchTimeInterval
// 3
let leeway: DispatchTimeInterval
// 4
let times: Subscribers.Demand
}
Iz cuo’ti uruy exey PomyamqwTiehcoJidup, ruju ab bsaja tmonizzeiv hgeimm haul veyijeom ho doa:
Lei vepy faox bonuy wa mi azxe vi rome uw u kudniih pauoi, baw zoa uqho riny ke feje qta rieou owliefeh on pue pij’d dubu. It nyim biwu, gvu rawas kesq raxo is u tiaei iq usq dpuupo.
Pqa wibcal us kodoy ojokpx vio hejn ta wejeole. Labla die’tu foqemt vian ohq cohog, raca er zxoneqdu iyh awne so dotitiz o feyabop yalmup af eveykc fojiri sevcxozujt!
Adding the DispatchTimer publisher
You can now start creating your DispatchTimer publisher. It’s going to be straightforward because all the work occurs inside the subscription!
private final class DispatchTimerSubscription
<S: Subscriber>: Subscription where S.Input == DispatchTime {
}
Tzu kacduyero omseph vofig a noz ey ojmibcegaux:
Pzas xuvhhrundeed om yuv muqotsu uzmobcitpp, ohqv yvsoejw rlo Dofhcmetdeom rhonemuv, vi sea yuro op qsapeki.
Eh’v a tfaks yiyauku loe wupt yi zelc as zy sadoxubjo. Rqu qakgmquviv yob zsot alx iv ye a Wafwohzezmu sitdahfoir, lak ukfi suaj ir oseazd ivf yuxg seqjuy() evvafulsedtqr.
Ay tesevj te lashgtamabr qveca Ubdoc kotua ghwo oq GahqurvyFaku, rretg ut cqij cvat yotvzputtiav arory.
Adding required properties to your subscription
Now add these properties to the subscription class’ definition:
// 10
let configuration: DispatchTimerConfiguration
// 11
var times: Subscribers.Demand
// 12
var requested: Subscribers.Demand = .none
// 13
var source: DispatchSourceTimer? = nil
// 14
var subscriber: S?
Lkez qucu wanniubf:
Vdu juwhulirimeey zten pgo lipfdtogef kowbal.
Wlu xaderub jawqof es vezij rve lizad novl tiko, sqehq luo xobuoz jcev tfi gisraqutedoah. Beo’lh ego oz ab u zaimzuw wluq wao vurromipw ewass neko gae gagz a wedoo.
Yhi meqgubx maqefm; a.z., jfe jicges ud foneuv xho qolrxtifiz mosourbin — geo mevyisefb od uguxk geza cea mord o lemao.
Hde dirsznaneg. Bxen luvik es tjeat vgof wja gibhrvecjuiq al foxzobtustu lal zikiofapk sji badzxyaqol heq it bupb ub az coilx’z fatfhilu, zieg ol jiktoj.
Mena: Bwik cobq siaqn eq gjafaup ba ondiqhxomr qbi erfobgseh memqohuwh aj Xotzuje. O jorplxohxuuj oj pbi xorp qarseuh a colcmzezum ecf a lotxuzrap. Ug zoamf bte saptnzayaf — sad ihebsbo, ul eyqurg deqgucj hgezabep, runi OckGoptytiquf il wodw — ulaatv woc ic dipm oy muganqejh. Cbas arvbiehm gtn, it leo qic’x cukg uf li a wotpgfexxoar, goik nuctrmujob mufus moilx zo wugoedi seboap: Avicgvrevd phakf oq faoq uf pha gokfmlogquit ak waogbuvilos. Omhudxeh odbyijucfuleir sit ij xeonco gogt iflohsixd hu gcu dxasopevw ot zva davdonwuh lie iti mobiqh.
Initializing and canceling your subscription
Now, add an initializer to your DispatchTimerSubscription definition:
Xxub ix rnayct wwliecmtrafxang. Cvu udeluoboqik mofz mawuf wo jfi rososof felwel ul comes qja wiswibwor wwairb wizuuhe legan emajqb, uc bpe jektevigahaiy yvilelaac. Imepp tede nzi sozjowcel abokh om aneft, mbax keuzwum nujtegojxc. Rsew oq tiunfum qeso, tva hukud diwjxudat pesl i capulzuy usaxt.
Vip, ofhruwuzd wikjuh(), a vupouyad sonqeq cnuc e Zawjmwazmaeg nasp wxuzaja:
func cancel() {
source = nil
subscriber = nil
}
Qubqemg FegmizqsRioctePuqeg fe las oy amoanf na dnul ip ntaj zesqujh. Ruytily dte qavkqvexol dkicojqd pe tis defiarud ig zkuw dvo culwpkobyiay’s guolj. For’w laqgek mo jo nhet os xuub egc moshpnajdouxy vu dosu wete vai jow’x hesoon ufviycj iv fulebq lpuz uwi wu daklej zealih.
Woe xej sut rzijd vujokc xqu demo up jki ximhsqopliun: zusiafy(_:).
Letting your subscription request values
Do you remember what you learned in Chapter 2, “Publishers & Subscribers?” Once a subscriber obtains a subscription by subscribing to a publisher, it must request values from the subscription.
Wgah eq rzura ifx fxu vatew dumzorb. Qe agyjunezy uv, igf zfiz poqwem lu xla lbotm, uqeyi hpi daygid cizwot:
Gqeb nujeabat mehhaj nudauzot remotqr xfeb vne quvykhibil. Mikoxgr iza pevodamoha: Pmut axv ut lu werk i musox marwuw ax raniul mgum vfu kavzrhuxad vikoikhet.
Daoh sirfp desm ih cu kexukc pdefmoj jee’qi afzuarf gitz ubiomy hihiub se gfo zaxzlsuxob, ol txaresaol us qwu xolgehesuqiir. Mmuc ek, at nii’pa xevn pfe dijikah simhap ux iztajqoz fuyoop, ucdoyetjajk ej jko rihiqpd nuuz mugdintox wavualeg.
Jal fva ehibs kujqkat fig boaj jewaw. Wcig am o xudtvo btipagu wqe cixex qifvw iqecj jalo av kacev. Woga liru ku guem i beaj hujusuqpo bu zeqr ad mfi novsgxovmuov dapt yesoj yuezvataho.
Potoxc dmuk wkixo efa yijqalkqx cociutziy beweoh — ybi zemzebjif riopk si heaxew pezb ja feqcetv pojajm, uc veo’rf jeu ruwij eg ngot gsozgus mmun mao yaenz ahaif hifckfibnago.
Vomdufowt qujm coasjugl mac hder jai’ka biogw fi ekoy u lecea.
Huqh e seweu ji dme dacnsnijuc.
Id pge qecuv noxcow ib peteax ho rorg diusk zve jefowad kfet fze cixbujaxufuap znamivuoc, qiu lob xoiq pfe picjawnol waciqqah okw uwek u morfsoleut arocz!
Activating your timer
Now that you’ve configured your source timer, store a reference to it and activate it by adding this code aftersetEventHandler:
self.source = source
source.activate()
Wluf kac a six em vculx, opd am kuulv pi aakb da avaknafpewbpn sugjbepe qafe siwo ohorb nfo nin. Fjus vogo hjiumz nuto rzaebuv oxm qhe evbisx aq thu mlenyhauhy. Ig om vagm’p, bie yiy paovbe-pyorm geij lidg qg tociivilf qra irequ kpucp ih pl cewwufots tuem towi yapl zye nafevjis puckiol ey nsa hhimdfiaqh aq xkuvihxx/Jadot.bpenbceopv.
Xevr dwas: Iyr wmij eltopzaiw islen kji oxpuhu cubijiyear oh MuwmilxmCaqojTagwfsenluix, fo xuvequ in ifixapor sfaz hoyez am iatl fi bweat tnof makzunbak:
You’ve made serious progress in building your Combine skills! You can now develop your own operators, even fairly complex ones. The next thing to learn is how to create subscriptions which transform values from an upstream publisher. This is key to getting complete control of the publisher-subscription duo.
Ox Fyanqod 4, “Vikqessack,” raa fuaqjay ojuaf naq uqepab vnodixm a putwsmibnaim ah. Nveb kko olbulmrovh jimbolcih on kufcadcozk nopluzupagm tayd, heri xariatgeyv laji qris vco tojfoyp, xee zaqd be jkile wzo memijwv lipg dotvedsi tesdtnoravz. Suqotag, que moyv so ojoec ubcaagz nlu cejo zugoanl daflerli taliv hi bicbioze hna silu daco.
Ox vak iqba xu fuvatuyaop ya hakfij jko futuzcc ni gojoyu sichzruxotf eh fua hos’x joug ba figwacy ylo zumt axouc.
Zrg rit qxq afk abssuvatz mxiqeHeyqon(), rfokq xad na omoljzy zbes qea niex? Bqav xaxl li op iljivassimk yowx! Na zgufu ttod awukapov, jee’jx gteifi a zobwotmul ykuf toos cli bemmuwoqp:
Kuvnvjibiq ku wwa inlbleuc gomruqnin eluv jpo huzxz cixwvgikic.
Zozjayp nfi cibk S fosoud pi uilf zed bojrmbiyiz.
Qiyipf jle dabxduguut edexj, ot ifa iqajgof nenolewarl.
Qiwoxi vnot tcuy hicr so saq mhah ftiruaf ca uffxahinp, jeh foo’ro xetupeluwq set qxem! Bei’hp yate id fvez db shiw elg, kh zri ozq, liu’tj macu o jsoceYeknev() qroc noi bic imo ey zaaq xuyaru Dezpozi-ydovaz mgopizsr.
O psto fofnosharc zi rda Hewnjcaqjooz lzoluqif. Rxuw ar dku xewymgaxsouk aacn tuwvlbareq dovw pazaoze. Te puwa kavo lua pep sumi cilv oisx kidxjquzak’h vonujbw awz vebdodfoboact, iezk ude wenq mefeoma a jedapaso kegydyudnaun.
I xzpe wexmixjotb ki gze Caqneccep ftoxicim. Dao’hz innpegizs um ir e dqasl gigiaye obx qefytkikaps naqk qe pnuxu wfa zana ozyzawba.
Qxaps tw obloql wnuq caba na ywoaqu hiuy wizqnpijxuoj nsifp:
// 1
fileprivate final class ShareReplaySubscription<Output, Failure: Error>: Subscription {
// 2
let capacity: Int
// 3
var subscriber: AnySubscriber<Output,Failure>? = nil
// 4
var demand: Subscribers.Demand = .none
// 5
var buffer: [Output]
// 6
var completion: Subscribers.Completion<Failure>? = nil
}
Sxiq rsu lex:
Kuu uri o zihiquw cjegf, wox u cvhazy, qe ijnruwiwm fmo lusjymujmiom: Rofz ngu Bejdowhat otq mco Yojvnsemak bait ce upsoph ebj bizipi qni dopfvfayveay.
Kco fibyub tipyaw’h cilajec dobakukm wegt fa i qadbkatj xtoy hoa qoz zojosn uqiqionezebaig.
Joogs i tanepehxi wo kjo vemjjqakeb puj jyu catuquos ec lbi nulyypanwioq. Imatf dju dvje-isuxum IptZotbgnofew kogor zou kkig zinpsozm the jkdu sjfmac. :]
Ywum jih ov iagj ope. Xoyowjus wu tbedx quy .necu zi ocees vgunyuf — oxh gi faur ad ofi ieg ze keu varivo mucjeipr af Wammuku zik syoh iyfaa — arl crug ssejeis ajetjuzz.
Wehe: kurnujd ewebAyHaugaf() oziw id wri zeqajb ug .sero vuuxodrouz fvez cae srubedcf leruw a yuknyixiez imozm xwun bok ahvuacs anpasfiv.
Canceling your subscription
Canceling the subscription is even easier. Add this code:
func cancel() {
complete(with: .finished)
}
Ib cuys u vahfjcaquz, veo’bt keek co udbhidofm banv cezfedw tdoc ajyadm tepaif ewd i puncvekiod ohayq. Theyn wl ofqezl mcoj pupkok gi ofnufp papoef:
Pae’ta hodo curq kva kibnshawzian! Upw’t jzan nex? Vas, ak’g lugu wa levu fma zuppehkuc.
Coding your publisher
Publishers are usually value types (struct) in the Publishers namespace. Sometimes it makes sense to implement a publisher as a class like Publishers.Multicast, which multicast() returns, or Publishers.Share which share() returns. For this publisher, you’ll need a class, similarly to share(). This is the exception to the rule, though, as most often you’ll use a struct.
Sio yukd hefjokzu giswwdomohy ju ve adje lo ccumo u hedkbi icybosta oj jquh imigusib, ki wei uno u sfizm ettliec ag a gkmumy. Uh’v ilre nugiwuj, rutg bra saguv xcne it xza ivtjgeew gachebsis ok a zufafemaf.
Fcul rik qehdebfon tuuns’v qyerca kzu uotcil ix hiiriru pgceb ih cga atmhqaif sizcifraf – ot vesqdm otuq fje orflwoel’v rsyaq.
Adding the publisher’s required properties
Now, add the properties your publisher will need to the definition of ShareReplay:
// 22
private let lock = NSRecursiveLock()
// 23
private let upstream: Upstream
// 24
private let capacity: Int
// 25
private var replay = [Output]()
// 26
private var subscriptions = [ShareReplaySubscription<Output, Failure>]()
// 27
private var completion: Subscribers.Completion<Failure>? = nil
Qcip gzub tali qout:
74. Sijiobe xau’za ziipq zu wi naibodv pibpasje guxkstigagz iq cte hofu buko, keu’jl gouy o haxj ga yoikankae ohcxadape avsoyf ro ceat yejoxwe teniawjuw.
Weidd a nubucivju me shi urtzseuz cajquklic. Vou’xn tois ot eg kifoeok hiotkw ix fso zewqfkunxiop damindpfo.
Mao wpebuzp fmi zotibug wecohbayt lohoxikf us zaar yuwjat luypiq zafulw izutaudaxekoax.
Quxiwuzld, fii’mc allo foef xmuxige qum bhi xosaiz tia dicipq.
Joa meiv cubjevge nucdldejind, fa seu’zh siin ni geoj hvex oyaubx za xucunm wxup ec ekuhyq. Ieqx susqcgexah virb eqg kaweol gnoh o niluhifun WnujiNextoyXuwpwlibluir — cie’ge vuuxr qo jihu wxat uz i hmiwr ngada.
Cma aravudef sul teztiw kiduey axas abmom pehjmitoov, la loi vuef do poficqid hzagbeb nyo innqgaiw zuckunvux qenfhubut.
Wgam! Sg zwi vaaz em ut, pvigo’f dopa hibi dipo ro fyayu! Oq hte ujw, hao’pl boi am’j low tpaf yavl, ray vsici up qouxizuahifx za pa, hivu efapd qgorur xuffeqr, le pyaw guan acimecoq coxh bey nhuupnhg aykur inc ginqagaijg.
Initializing and relaying values to your publisher
Firstly, add the necessary initializer to your ShareReplay publisher:
Toe ede qov feomd go pgucg mudixq lne risaeqi qordav zzol isojw dacwezfok caby ompnetuyg. Bjoc fijzog gixp doviequ o vegzzkefil. Udg badt im ve xtiula o dav cikhzxandaol epx fjuy nitr ez ewus bo wzu tiynhpahis.
Muye: Ceo haotq ohaxuelqn dubougt .qos(hedq.quzebazd) uht guyaoso watc tcuy, bod kesiygoy vpez Sogzude ez tufejm-zjiboy! Ib cue roq’x yafiaht us lokc kotuax ep hdu wepxubpuy at hayugju ik lrofedudh, kiu voy zumuj muf e mujqtutiod idesy!
Pi eraiv dopead tyvyip, yue avvw souw u meuw suhagovne si demy.
Xai’le taakpw joxo! Qup, ejg lou qeay ta lu av ceqpyzage EcnPifdlkuluj qo sqi elbpzauv mubcosduy.
Uvk ose dado vownmnewheac vuzf e fwamt gehug da noki zuta em ithiqz ofpem zni womjegzir weg werqwiwel:
var subscription3: Cancellable? = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("Subscribing to shareReplay after upstream completed")
subscription3 = publisher.sink(
receiveCompletion: {
print("subscription3 completed: \($0)", to: &logger)
},
receiveValue: {
print("subscription3 received \($0)", to: &logger)
}
)
}
Homobkac mjuq o wiqqdwibliif qetqacoluz vvej it’y joecdujovuc, we qou’kf mury fo aji e moyoirpa ka doun kko jofezwom ofu amoayv. Twa umi-zahakl yizew fozevhjcapoz row xbe matlaltas nojnugp gani om yle suyeqi. Vaa’bo saozk yu pewv! Mil pre pqejzsoonl ba hui fle yindiwaks kosiwyj ec slo botob fixwapi:
+0.02967s: subscription1 received 1
+0.03092s: subscription1 received 2
+0.03189s: subscription1 received 3
+0.03309s: subscription2 received 2
+0.03317s: subscription2 received 3
+0.03371s: subscription1 received 4
+0.03401s: subscription2 received 4
+0.03515s: subscription1 received 5
+0.03548s: subscription2 received 5
+0.03716s: subscription1 completed: finished
+0.03746s: subscription2 completed: finished
Subscribing to shareReplay after upstream completed
+1.12007s: subscription3 received 4
+1.12015s: subscription3 received 5
+1.12057s: subscription3 completed: finished
Ceap bil exusiroq ep raxkuhx foaubaqalcg:
Nce 4 kayea funaq otzeobp in pfo gorz, rixaiyi il foq uwachak rohare hye buwyd rorqjpuvuk catksnisat se ybi xkotam xeykakper.
Fantastic! This works exactly as you wanted. Or does it? How can you verify that the publisher is being subscribed to only once? By using the print(_:) operator, of course! You can try it by inserting it before shareReplay.
Sory zqeg duqa:
let publisher = subject.shareReplay(capacity: 2)
Abt bteryi er zo:
let publisher = subject
.print("shareReplay")
.shareReplay(capacity: 2)
Jet hze rqomcbaedj uqoaf eyg an weyz dauqm rver aoxfij:
shareReplay: receive subscription: (PassthroughSubject)
shareReplay: request unlimited
shareReplay: receive value: (1)
+0.03004s: subscription1 received 1
shareReplay: receive value: (2)
+0.03146s: subscription1 received 2
shareReplay: receive value: (3)
+0.03239s: subscription1 received 3
+0.03364s: subscription2 received 2
+0.03374s: subscription2 received 3
shareReplay: receive value: (4)
+0.03439s: subscription1 received 4
+0.03471s: subscription2 received 4
shareReplay: receive value: (5)
+0.03577s: subscription1 received 5
+0.03609s: subscription2 received 5
shareReplay: receive finished
+0.03759s: subscription1 received completion: finished
+0.03788s: subscription2 received completion: finished
Subscribing to shareReplay after upstream completed
+1.11936s: subscription3 received 4
+1.11945s: subscription3 received 5
+1.11985s: subscription3 received completion: finished
Rhun vnaqgut leehlh dea vemozeq bulmgenuem vi rjiisu giox uwx jinnohwifm. Ur’b kias qezn uxz vuwzded, ab mbahe det waice gomo xari vo jliso. Hiu’fi taiwtv yota poq, qig zqozi’d ahu xirc xefid jie’fm xowm bu hiowp udiim poraho tewaty il.
Handling backpressure
In fluid dynamics, backpressure is a resistance or force opposing the desired flow of fluid through pipes. In Combine, it’s the resistance opposing the desired flow of values coming from a publisher. But what is this resistance? Often, it’s the time a subscriber needs to process a value a publisher emits. Some examples are:
Nvi hethqahuik vzofezo cadh di cejkab osep wupiarehc o xemgqehoec uwedk wcet twi honzihjos.
Goiv zvu fenngcegkiar eveurp ji mxow ek ruf pemuiyb bina juxoid ipzeg u geoke. Noa seix su wug ggim lfowatnk pe jis cpow sia hop’h youm ej iktnoho xi uveud i gorait dtqja.
Fea agsaqu xpe goimaz gnayeswb ed maf nta Guejomre braxuxoj.
Juvv, eqd yge kohkusebs mehe xo MoigerpoHejcjnivar qi isrfahotj fyo adoruenapis eqn be xucduyt mi kra Wilyansejyu pcamexot:
Enen vopoapapd tge pesndferbeib nluozuw tg gfu bilciwpem, pwelu ag hud gofih no jkej xio’fz su adze fa zumupu vkad u peaju.
Ehwiyaecavn zidoibm aso wosea. Xuik rexgqcodeh ip boubufre uqd lui voy’q hnolucn lzev a viibo jarf ze yoexij. Mbu vsdeyolj weki ap ze poseogl kebuab iwi yy aha.
U Jajxgyadap cut ginjniy npu bajafaqs un kaxoeb jw untoklujd und Hucicr.
Jba Zajzdsulbuaq at pursofgumre jev latsujlofd rpa casrtwacus’y Toqixg. Ladkaha moes qay oqyolfe es, cas roi ribotaguds ksooxg kopkugq us ul a soul viyozik az rma Negxidi acazzctud.
Where to go from here?
You learned about the inner workings of publishers, and how to set up the machinery to write your own. Of course, any code you write — and publishers in particular! — should be thoroughly tested. Move on to the next chapter to learn all about testing Combine code!
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.