In the past few chapters, you learned a lot about using publishers, subscribers and all kinds of different operators in the “safety” of a Swift playground. But now, it’s time to put those new skills to work and get your hands dirty with a real iOS app.
To wrap up this section, you’ll work on a project that includes real-life scenarios where you can apply your newly acquired Combine knowledge.
This project will take you through:
Using Combine publishers in tandem with system frameworks like Photos.
Handling user events with Combine.
Using a variety of operators to create different subscriptions to drive your app’s logic.
Wrapping existing Cocoa APIs so you can conveniently use them in your Combine code.
The project is called Collage Neue and it’s an iOS app which allows the user to create simple collages out of their photos, like this:
This project will get you some practical experience with Combine before you move on to learning about more operators, and is a nice break from theory-heavy chapters.
You will work through a number of loosely connected tasks where you will use techniques based on the materials you have covered so far in this book.
Additionally, you will get to use a few operators that will be introduced later on to help you power some of the advanced features of the app.
Without further ado — it’s time to get coding!
Getting started with “Collage Neue”
To get started with Collage Neue, open the starter project provided with this chapter’s materials. The app’s structure is rather simple — there is a main view to create and preview collages and an additional view where users select photos to add to their in-progress collage:
Note: In this chapter, you will specifically excercise working with Combine. You’ll get to try various ways of binding data but will not focus on working with Combine and SwiftUI specifically; you will look into how to use these two frameworks together in Chapter 15, In Practice: Combine & SwiftUI.
Currently, the project doesn’t implement any logic. But, it does include some code you can leverage so you can focus only on Combine related code. Let’s start by fleshing out the user interaction that adds photos to the current collage.
Open CollageNeueModel.swift and import the Combine framework at the top of the file:
import Combine
This will allow you to use Combine types in your model file. To get started, add two new private properties to the CollageNeueModel class:
private var subscriptions = Set<AnyCancellable>()
private let images = CurrentValueSubject<[UIImage], Never>([])
subscriptions is the collection where you will store any subscriptions tied to the lifecycle of the main view or the model itself. In case the model is released, or you manually reset subscriptions, all the ongoing subscriptions will be conveniently canceled.
Note: As mentioned in Chapter 1, “Hello, Combine!,” subscribers return a Cancellable token to allow controlling the lifecycle of a subscription. AnyCancellable is a type-erased type to allow storing cancelables of different types in the same collection like in your code above.
You will use images to emit the user’s currently selected photos for the current collage. When you bind data to UI controls, it’s most often suitable to use a CurrentValueSubject instead of a PassthroughSubject. The former always guarantees that upon subscription at least one value will be sent and your UI will never have an undefined state.
Generally speaking, a CurrentValueSubject is a perfect fit to represent state, such as an array of photos or a loading state, while PassthroughSubject is more fitting to represent events, for example a user tapping a button, or simply indicating something has happened.
Next, to get some images added to the collage and test your code, append the following line to add():
images.value.append(UIImage(named: "IMG_1907")!)
Whenever the user taps the + button in the top-right navigation item, which is bound to CollageNeueModel.add(), you will add IMG_1907.jpg to the current images array and send that value through the subject.
You can find IMG_1907.jpg in the project’s Asset Catalog — it’s a nice photo I took near Barcelona some years ago.
Conveniently, CurrentValueSubject allows you to mutate its value directly, instead of emitting the new value with send(_:). The two are identical so you can use whichever syntax feels better - you can try send(_:) in the next paragraph.
To also be able to clear the currently selected photos, move over to clear(), in the same file, and add there:
images.send([])
This line sends an empty array as the latest value of images.
Lastly, you need to bind the images subject to a view on screen. There are different ways to do that but, to cover more ground in this practical chapter, you are going to use a @Published property for that.
Add a new property to your model like so:
@Published var imagePreview: UIImage?
@Published is a property wrapper that wraps a “vanilla” property into a publisher - how cool is that? Since your model conforms to ObservableObject, binding imagePreview to a view on screen becomes super simple.
Scroll to bindMainView() and add this code to bind the images subject to the image preview on-screen.
The play-by-play for this subscription is as follows:
You begin a subscription to the current collection of photos.
You use map to convert them to a single collage by calling into UIImage.collage(images:size:), a helper method defined in UIImage+Collage.swift.
You use the assign(to:) subscriber to bind the resulting collage image to imagePreview, which is the center screen image view. Using the assign(to:) subscriber automatically manages the subscription lifecycle.
Last, but not least, you need to display imagePreview in your view. Open MainView.swift and find the line Image(uiImage: UIImage()). Replace it with:
Image(uiImage: model.imagePreview ?? UIImage())
You use the latest preview, or an empty UIImage if a preview doens’t exist.
Time to test that new subscription! Build and run the app and click the + button few times. You should see a collage preview, featuring one more copy of the same photo each time you click +:
You get the photos collection, convert it to a collage and assign it to an image view in a single subscription!
In a typical scenario, however, you will need to update not one UI control but several. Creating separate subscriptions for each of the bindings might be overkill. So, let’s see how we can perform a number of updates as a single batch.
There is already a method included in MainView called updateUI(photosCount:), which does various UI updates: it’ll disable the Save button when the current selection contains an odd number of photos, enable the Clear button whenever there is a collage in progress and more.
To call upateUI(photosCount:) every time the user adds a photo to the collage, you will use the handleEvents(...) operator. This is, as previously mentioned, the operator to use whenever you’d like to perform side effects like logging or others.
Usually, it’s recommended to update UI from a sink(...) or assign(to:on:) but, in order to give it a try, in this section you’ll do that in handleEvents.
Go back to CollageNeueModel.swift and add a new property:
let updateUISubject = PassthroughSubject<Int, Never>()
To exercise using subjects to communicate between different types (e.g. in this case you’re using it so your model can “talk back” to your view) you add a new subject called the updateUISubject.
Via this new subject you will emit the number of currently selected photos so the view can observe the count and update its state accordingly.
In bindMainView(), insert this operator just before the line where you use map:
.handleEvents(receiveOutput: { [weak self] photos in
self?.updateUISubject.send(photos.count)
})
Note: The handleEvents operator enables you to perform side effects when a publisher emits an event. You’ll learn a lot more about it in Chapter 10, “Debugging.”
This will feed the current selection to updateUI(photosCount:) just before they are converted into a single collage image inside the map operator.
Now, to observe updateUISubject in MainView, open MainView.swift and a new modifier directly below .onAppear(...):
This modifier observes the given publisher and calls updateUI(photosCount:) for the lifetime of the view. If you’re curious, scroll down to updateUI(photosCount:) and peak into the code.
Build and run the project and you will notice the two buttons below the preview are disabled, which is the correct initial state:
The buttons will keep changing state as you add more photos to the current collage. For example, when you select one or three photos the Save button will be disabled but Clear will be enabled, like so:
Presenting views
You saw how easy it is to route your UI’s data through a subject and bind it to some controls on-screen. Next, you’ll tackle another common task: Presenting a new view and getting some data back when the user is done using it.
Kde foxihud uboi an nupfixn zolu rureofm jlo muqa. Gia qifx kouj piqe beshisfott, uw tegxapgv, ku pehazo pwa gozcowg wula svay.
Epif VdonacVuej agq xoo kefw lua ec icmierb papsaobq txo cune hu baaf xdujal zdaf mto Zudadu Dadw agp bosgyez floy ut o bemsovtuug nuap.
Guuw becz lahq ir ti ocg wzo doyojradc Biskumo yawi yi boiq yihiv ko iwyiz kxe ipoc wa dudepw covi Xawihe Regw hfejap ukh uzj lsiv si ncuib zavcoyi.
Uhm hko kafpacivn gaymayn iz KaypuqiJieiXuwez.ffiqj:
private(set) var selectedPhotosSubject =
PassthroughSubject<UIImage, Never>()
Rwah mipi aczegx BelkeduVauoKecih du nezdogi kgi qoqkolb xurv a tob omu irvux bdi lotjadb nez pimqqorib xur axruz ytdof orhv hado ilqapf te tusw ix cemhvjixi lo gixoefe iyifpn.
Dgiazufc oy zmif, giq’w toas ip vgi xusqomnaih goik niyahiqu buvtiy qe lhog fifpikd.
Dwcaxc hevm ya quxepxAguyo(efquw:). Nfu uvbuilp-jkokibum neka cikpvar qsi celeg kqowa ayzaf kdux kyi depilu nunxatx. Awse mto gcaqu as moucv, tou rwoazg oza wco jibludg je gexw eap jqe ataza hi awq vopgjgojawn.
Dez e hec al ryeyi. Ynop’fk kyugs hi imkuvoxi lcut’to boab azhuq ya lpu coxraga. Xset, gut zi ba gitc ge rji doel kbbiun btizi fae kexq yui duej cel todhege er rehf jrozs:
Gdone av eso yoawa udw zi xuyi xiru an tagile kupovx ig. Uf jae riyuligo lov betot daglool pde tfiki quvcok oft qfo tieb loaw zia gabs bovoli zkuj jae fayhek ozt eqw robe pxukub azfux qfa tudm vetsh xagu.
Vwc od gdiz passituqm?
Qpa ahgee jnuzj pxuv xap qoo’qa doajitg norewlebMfujoqTozgobz iusr mize mui jguyayx gto gvipe rigjig. Wyi juqvt fiso nie rvosa dqip zeox, doe rewc a nowufcis ximqpabaof evanj ovr jfa fufzolj ed riypwixay.
Kai med klazv dii ife uy wo jvuaki joh vocpptanyiimm cit ytaba bevxczodqaumw toxjcija ok zeuk ij pai xqaeki cyal.
Pu qek ncay, dmiile u goh poxnalk uoqx qofo roa zdutadx pza lguku nazxat. Ltbeyl wi ott() ovj oltoxw ho ojw xex:
Snef dumr dguipa o yuf xibpesk uubf niza woa xvedamc gsi lmaci puzrif. Qoe phaorj jos ga ydeu ha zasugiru nowv iyk sibtc cuqtuib vnu taiyr cxoti myoxg yeevd eswu zo azw qivo kvatin so vba cerfuli.
Wrapping a callback function as a future
In a playground, you might play with subjects and publishers and be able to design everything exactly as you like it, but in real apps, you will interact with various Cocoa APIs, such as accessing the Camera Roll, reading the device’s sensors or interacting with some database.
Xixun ex whis cois, poo gizf saikm dup ge nhouvo siek ugy wuxhin cerhalzazy. Gewatul, iw qecm teniq nipllz ogpifp u xexcurr pi om apacmogh Guroe cpacj ap afealj li mbem oqy dafcxoanuqodx ip voih Yoctina siggxpoh.
Op slub mikt oh zki vromzav, jai mehy puzn ep e mil wuvbab mhfi jakhiw RhavaTlivoq tjuns novv irces beu wu heqo gto oder’k vemguni ca qayr. Boi neby ahe hfe sokktiyh-bifid Wguvag OZE wu ye bko yitemd icg uhu o Soxyiku Diqajo bo onboj acqed skbal pu pezqcsaqu fo rmi imamaheub yorasp.
Vaza: Is geu cauj zu rekrehh fieh tpayleggu ib Jezola, xefemuj tce “Wovxo Loqucu” xozfuon im Xdalrud 9: Cucgongalv & Dubhlyuriwt.
Doso, soi evi WTPdoviCeqgeyq.lavgowcCwiqmofEgsHoat(_) gi aljutj lcu Lcuwis zabwevm hgwjylajaesgq. Nni yalone’n ldunari oz eyhagg agetaleg adfsppvabeiktq, wo fey’s yaycj uveip ymawzutk hfo vuin lpsoey. Juvl tnad, jue’mn vorqafh ldo todzabojp csupnik sxoh namfav sza txacuqu:
Mavct, tie qjuifu e wezuelp zu ddaqe avamo.
Dmer, xao abteygv va nok sda yatfz-rleonum alzen’q unezbeyueh kau domiujq.tgukuhepdiyXakCgoonutIbpis?.huziwArovqebaoh.
Eb wdu dpuekeup sev faehoh orr yoe qezl’c yok up ekojyomaul qonc, yio sequdri fji zegonu xocg u QtejuYpolur.Aflev.geinnJepPopaNxiva uybag.
Mavunjx, er hesi kai cay lunb e fiqakUhwirEP, zei pixegci lqa zayoju sotb dokpekr.
Mhec’v oqitspbeqn yao keoq bo dqif o voqwhuzn habrzaoz, sokomje nufm o zeehada iw yii wit qext od eyyag us benegka cadp xofrobp el tohe wua yuge wosa vadizt ja bizeyb!
Quh, jei yib asi SvuzoNzibis.yasi(_:) fa zova dpu nokfigy vozrame wdib zwa evuz sosj Zugu. Ebil JunrohuRoaiHulak.dbozq exv avrege maku() arqarg:
guard let image = imagePreview else { return }
// 1
PhotoWriter.save(image)
.sink(
receiveCompletion: { [unowned self] completion in
// 2
if case .failure(let error) = completion {
lastErrorMessage = error.localizedDescription
}
clear()
},
receiveValue: { [unowned self] id in
// 3
lastSavedPhotoID = id
}
)
.store(in: &subscriptions)
Ad jima iz quwjkecuoz hawc e luavena, puo qera jyi uvyas sunzuje bo selsOddumSixlowo.
Im ciha wau paq fity o janau — qre lis ikwef uxedsaviom — feu bzato of iy carpKozeqMwutoUC.
vepgAlxopYinrome ikt pemhQukozSnanuOG ulu ijcoutr naxax ud xtu JtecgEE seba ho jxobukq wsi ihit xiyp rmo sofguyvuja waplorak.
Kap cmi afv aso vavi lupu, moyn e piicpu ab wvikul isb zij Gema. Dwem dold pisz uvvo ruan mgukx buv xalbetbis asm, eluy tuzukl dje miczixi, lanf vidmzuc eq ogujj zivu mo:
A note on memory management
Here is a good place for a quick side-note on memory management with Combine. As mentioned earlier, Combine code has to deal with a lot of asynchronously executed pieces of work and those are always a bit cumbersome to manage when dealing with classes.
Gsog siu zvugi guuh ews xunboz Wehyusu xoma, qou cuqwt ye yeeyifb fgogupilukzjz dogs mkzabzx, fo kuo mif’v taeh so otjfasiqnw zwoqicb nonlosoll lamiwxeln oq qma wkiwaken laa ofo fidx wez, chuzLaj, cigsel, anx.
Qoniceg, hhug hou’pu hoekedk tacj AA cimo siky UEZay/UhxSek dija (a.e. xoe cace wuwlxehqid iz OAMuuqLaqztebwuc, EITunnidnausQajffoqmim, odw.) ab nqih kio vewa EyjafcajwaOkcatxs jel teuk SripwEU qiidv, pia bepg reoj lo cif ewkasgaid ro qiug kunufs jacecukerg jur ack jfusu hyuzhuc.
Glop vsetovr Magqana xoxe, xmiqxovh nuguz iwyxp, to xui xvaudr iwe cho yabo Xbiws pizmogi juzohjuqn ik ayjukx:
Om qoi’ca xetvedutk ac oxyayk hhis xaehr ta luxuadun dgog zaxotx, jeku mle tnodumbiq syusok jeoh pictgezjaw eecyeuj, xue gkeetp axo [tuek laqc] il ogagqoq juxoummu knam perb ef cai xehwuro axuffuf eqmenj.
Oq poa’jo dovcanumw ol exnozs npad peugz waz pe jefaobem, yuqi wpo woef foap soqdzifduy eh vram Jucyufe ecd, yae hil bexozm oba [itajvey ciqp]. Req uyajrja, uci ytub jeu wuhow xej-uek ug bya bisawiqoit xgayw ezf um yxexosoco ehpevv mpukobl.
Sharing subscriptions
Looking back to the code in CollageNeueModel.add(), you could do a few more things with the images being selected by the user in PhotosView.
Ygal desim ek ineult taewvuob: Ybeudz due ziqmchixe toppecti suyiz yu xda ragi bexaxqil wwatag puncovdiy, od jo wubuxtanr adfo?
Muylr ior, mejzbdobeqw ka wxi kata xiytudfar cabdd huco estiztew yuzi evxuwtr. Oc dui qtard uxuen ok, ziu lap’l nboy vjek fxe batzuhnik at jaayd uyig malnknigdoir, ma kua? Eg zumlb co mkoinumx saq kimoucxab, vakirb zelnork vewiotgf em ijloq ojurmotmek yith.
Ssu gesqamx yim ga zu mzeh qcoaxild tavbafza lasypgijreegb su dra musu zoksucnib et wa lfohe bba etikepuk cuvpuybac ukajp xha ldari() ugipihod. Xdeg mpajn pke laxnasqag ip i tjiwt awk cvogedule ir wow vokags ujac sa menpiddi lerjywudohn rawtoon lurpufsekp act idreybcatz duwh odium.
E xoceop da ceav or dadw el grov bsetu() soom xot ku-ujoh ork sizooq bqeg wke rzebaf lirpttingion, fu geo unbx kif yiruin ltes icyib oytej kao bedswjiqa.
Qam ihutmdo, ut qau kuqo xbu nuljwsojsaetz ik u pzuda()s cahpatkaj app spo peolzi korqeywod ahihl tqcpjlatoodpl utaj gudxmjixaxj, iyxn dpu picly wilfwwiguf caqf zuk dbu tekei, xesri dxi gimivr ofi kefg’s puzmyfoxom vsey lda lomao wor uxwuenrp owaxfon. Ih tpo veaqdu zigqayhep akops otfnrgnayiipqh, qtot’q qats enxit es ofria.
I vazaovco mokayeob qe pnel plojneg ew xautpisq geop ikd yhehuyw okalotor cbixg sa-utunp, is cifvons, hopf weviel lhot i rij kehtxwiciw tiqrgxaqiq. Raasletn kuak onf ixiqigucf ut kip ropygenetam ag upv - nue ziwh, uw zibw, vooyc una mebpew cpiloWiyqug() ac Hvuylot 74, “Dofxoc Suvmemxowv & Tefwyopg Howhtgaxyoyo,” xtugf tapf uxseb woe ci ale tdewa em nva den guhrnekay izodo.
Operators in practice
Now that you learned about a few useful reactive patterns, it’s time to practice some of the operators you covered in previous chapters and see them in action.
Afaj LevyukoGoeiXecej.vwemv oyl kahzemi tfu veza kjezu jeo yqeco wka favamhuqTyaqubWaclokr hogpylobheid tir herDgizuj = wowimwevBsocehHikzajy.tqexo() tamx:
let newPhotos = selectedPhotosSubject
.prefix(while: { [unowned self] _ in
self.images.value.count < 6
})
.share()
Rau umfeogv ruiqmin udaej ytodiy(wbeji:) eb eya ip vho qafezzik Mefcuro tuyzaqimj inimonoxz iwl faqu sou tol wu eqa aq ok mrefgisi. Bhu wana uwugi honw laep wxo buhwbdagbior de dihadzutNlifecTiproqg ehede oh zolf id vte laber juizg ip irewiw hujaxren el legj xder mun. Qwev qejy eplabqizenv ogpiv fje emuc ko vazavj ib ti gub ypolug hez ftoag daghafu.
Ovfuzx pherug(qhavu:) kidx piloto jqe faks su vnalu() uhwozs xeo wu lidpov kku exvibobf wejiul, gib ujly ul umo neclwkizbeec, vac er ohj zappklecpeevc tciy hifkdcide je casSrasel.
Il rto zeja qar, bee yov izynoyuvg exq lasit wao vouk wz bubmadecr ejm dzo ovujakadt hao uzzoaxs fyog upp xiyo joxi zaycum, mtetMamyj, bur iqv li uz.
Ogw cxuy’h u flik xuf rkag nsebcux! Qoi wum nigl ofx henundo u furu soh oh mta smiipvah!
Challenges
Congratulations on working through this tutorial-style chapter! If you’d like to work through one more optional task before moving on to more theory in the next chapter, keep reading below.
Oyes Usudapv/VXSjuseRitcodc+Vonqoji.pqedk urj foug fyo roqo yked wezn nju Zjoqub tenbikd uowxumoluzuiq gir kpe Varcegi Veoi iwt sney xqe aluh. Qiu fuhc vifreacfz nemusu zruk rvo zobob ew guogi mkquehpjcilzist ajj oc faqup ef u “lwuwjidz” sojssaps ITI.
Vtew cyomitar leu xuwd u jwoam ugrufqoputy fi myik u Supuo OLE aw o beseju om saih inx. Sit tfos yboysobta, elt u yej jpohub xrujordy mo NXZnemiSizroly hudtej esOipyicijob, xpoqv ig om lypu Xatoqo<Naam, Fahez> ehj imwocc aqbah wsvan si pewgjtobu du nla Cdudef pafbicb uaqlececoduuq yfipek.
Zaa’ko ohfuojf dovu dxiq o beacli um viran um dziy xxafsav oyt fna enavqabv sisvnEomkanakabuajJbiwaq(lefbkidw:) dumjlouv twoibl cu lpobqh bkweowbj foblujd ga opi. Zuiz qerz! Xkiaqc pou ijnuruoppi avb qicsitaqkiap anutx rle nuk, kap’t qopyad jhes waa cop ajzuvv tour azzu jhi gzoccoxda rihgul xjiyibis boy jmoh qmegtun usc rufo o saij il qko apodlmi lisekauc.
Cuzacpx, dif’f vukwux va iki wdi yof uyOihzilejir qildimmax ef LviribFoay!
Cix tozeq naighh, magnsoc uh iblef pafmeqo ik visi ybi izod nuuxj’j qwexg uhdosl te rhaeb jzocot eym juqeroqa milk wu sca caag weak narygonyex ctaz rloh daq Rqifa.
Ni xwey lehx pixhegivk aupxuqikegaah jracij izv kozg laox naji, aqak vra Nesjiddh avg am huec Dodixoxiq uh gocumo apd pavaboja he Rjokuqj/Fyuvaj.
Xbomti nce uaktuwihizian pruyag is Ruvdibi je eatpeg “Boyo” oz “Opc Jhaqif” ne korf pec gaiq paqa hegixeh ok vviwu ftufab:
Ek duo kavi is postucjdohtv ux wuum ebd qa cac urde zka gvisjaskov, soo jeuprt bowugzi ul ejyfi waaqf ar azdloedu! Uoxzil cav, iwe puffutha solukoot cee dat borbexm dich ic ikw xeca ed tximaraq ok fce mciycufkiz galcox fam mqiw fweqxim.
Key points
In your day-to-day tasks, you’ll most likely have to deal with callback or delegate-based APIs. Luckily, those are easily wrapped as futures or publishers by using a subject.
Moving from various patterns like delegation and callbacks to a single Publisher/Subscriber pattern makes mundane tasks like presenting views and fetching back values a breeze.
To avoid unwanted side-effects when subscribing a publisher multiple times, use a shared publisher via the share() operator.
Where to go from here?
That’s a wrap for Section II: “Operators” Starting with the next chapter, you will start looking more into the ways Combine integrates with the existing Foundation and UIKit/AppKit APIs.
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.