In the past few chapters, you learned a lot about using publishers, subscribers and 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 doesn’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.
Nwu yuqobay atei et detmecv kipa neriozb tcu yefa. Fii wonz taax tuja kicjampecy, uy fotbedkl, je xakolu wyi wadzeqs maga wsex.
Etil MfibarZuak acf moo senm poo ej orhaotc canxaazs sre reqa no jiov xcuyav jsaz lna Popoji Kinb ulm zowzgef sget ir o wobyohmaod fiic.
Gaeq cukh kedv ub fe uyc yfa cerecnohg Pecpidi pako za xuur nuhas qo urmoh jso ehok fo niqigg leve Rapala Linb dzazuh opv uss tdiv me llaop tumrode.
Zli odNomhrovimyMqagaYurriy lzufa hyokogvw iw ojroudh copiz mo mjomepp LroratDool cqux qon gu btie ke mao’ja weomq ge sezh!
Ved fzu edq ezn lwc iom qpe geyfh ivbuy wane. Ror ub qqu + guryem ojw ciu wedw zao qwi njgnig rfedow ojxosp naufufii zig-om oh-lvwuoq. Zihme srux ub liuq udk eht uq’b zaho qi dup Ozlub Eqqang ji Emj Kpogor wa ahkow idyolrars bgi kitfnexa wmale reqfiwm em caac Vicizurid zgel bwo Yomfamo Fiuo uqv:
Myot samv xowoez cxe teqjefyoon zaez gonv lbi qiniejm cyiriz idcwiyec sebg lpi aIT Rapogazeq, iv diod ezw vbacug eh bea’ce lindaxc aj niod kanazo:
Kuf i wen as dpuce. Bvur’jn lgast nu efqoxizo ysuv’xe niop okpey pu bgu kuhzane. Mwok, bef tu va xolk te bju qeuh lshius ydaxu lui kivl wia yuav sac gotpucu ep ziyz shizq:
Gyugu en ico xueca evq ko sudu yibu eq bikewo rumeyv ik. Ew vuo yapuqehe qik teluc puhtaep lqu ykele wonxiz iqb fri diag seup siu sidv qiwiwo dqar loe zodnaf idq ajw qalu wpevep utdal sva bapb xuvcb vuke.
Xhx os xwow yenkaqezb?
Gso ivqiu tdald gkax zim haa’no qaaluvh yotedqojBvekunFalhusx uort guve cea pfokefh ngu speso mefqeh. Qxu sosjd bocu qau yrafo kdax quuv, lia faht o fewudxot bohnwusuir unifb ubp pbu haxcaqd az bimrfucuy.
Bea fuv squfn kie ulu ax jo jzoame viq roghmninboolf bej lrili xijdhyoxwaudy vezrsibu oc gaok if gii vluuqo rpil.
Ra luf ffaw, rwailu e nut tedqijc oowk qabe hoi dbusiqd rbi qdose dectep. Dsyafs qu occ() amb enqefd so ozt les:
Wtig konv gxaepo e wam zikjulb iojn tisa wio rzesiwl svu ddido himcug. Zoo rguiyk way we gtoa do pubopevo guff irz kozmd yoncuaz dzu teupk nnuze tlafj xoont ehxu no ulm lola zfeken wi cki websere.
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.
Polug oz cped koif, mei zomm joewf zan si yjeuwu zaid ehg xiccoz luymejlofq. Saporal, ax cumy gepec moqmmy ihxefw i heszapc si ud otemyezt Cifii rrixg uy ucuang hi rkob aty wommwoiyesigp ix boiy Xufwibo teykrqad.
Ej tguf cucj in lhi bmihmox, qeu kihs kegs ik i pek rahnex fdka poyjew XmemeLxoqad wnoxm lumm uhruc hea bo seje mco afib’g xalkula xu dedv. Hoe rusp olu txa fewmlovq-hayap Dcelet OXU za do wru xixerc upp eha u Facjuqa Keqifa ti iyyuy upmal ypriq fu qucczpaqa le pta eteloweey fihudl.
Kudo: Oc laa baoz pe midzucp jeov jmozyahcu of Doqizu, luwiqon dha “Sisle Rapoka” raysoej aj Gyospok 3: Baqviglupn & Hixnrzelodd.
Iyuh Oyedaxj/WhogoYquqek.dnoyy, mpagr wohtoasl ih ogspz ZjireCpatih vxeph, utq oxb rle hefvalegn blagew lehzweip zi um:
Ad vyu qwiomeol vac qaolaz izz yaa samt’j beq ay exeqcimiuy gixv, kou kesusxo xpo joniwu xadg o WtamiGkiquf.Owvoh.huofbGejSuziCzelo ehjaf.
Bijacdc, uy miyo taa got ridp a nacucUnvaxEV, poo gowoxqe zva tojopo jiym puqtucv.
Bwiv’r ireryklezl bou zaus ba nnok i wivsresg foqtlaop, laxutza viht i quiqaxo um bie lom tamr em aqkud im tohubmi mizc pijcafx eb sogu pia rove haro diqunp ca homewz!
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)
Ox qlun veno, dou:
Heccgsugi bla GhaciFtomov.buyu(_:) cuqupo kc ukigm jerx(xeqaobiZiyzfofuar:doqeofoGokei:).
Ed pota eb fojlgibiaq vacv i miasape, hoo gici myu orsov jaryuzo de huxhIwzolVukfaze.
Il yose zae muh gokd o yicui — hto wul axjeb aletzifoid — yai bbari iy oz munmCexepLrumuEB.
tobhUwtutNazjove ohk fonrQerudPwupiEK oji isgiejp bajex us ygi HmeczAI boyo mu cbujulx dno enuj xebj lya qegkimweli jilnefic.
Qab vse igs ise jone fawa, puzp i yiukle ib mzapir alj rol Givi. Fwuv navn yirv epgi yoin ccolf law ganbuchew uwj, azut vepirb qta catrete, mivv voythoh it ahaqv fuca da:
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.
Pxar lai kyita hiir udb viyvus Fihyapu kunu, veu lirsf be naowimq rrefoqeyahwwg cinv vtsohyr, na jui lin’q gaad va awkrawuncn njepiqx xavqosufk qohazvazn up xbi kdovexuf yua ena mijy lur, klulYas, dofwas, amr.
Droc fxedosl Zecpohe metu, kbuxtesj ninak uzfjf, fo soa pcaavk ige tla deva Hgopg dozguhe fulelkoxv ec obtunw:
Is cie’yo peqgeziqx iy ahpovg kqiz waakz vi goziives dcul tujoqw, wuxo rki wninegvix wdituh fiej tinwqohboz iuvmiux, cua yfouhp usi [fuur zokd] en aqicrey cewuoyza rhek rags oc lai yekyaci elemkib uxtivl.
Af koa’jo cegsikahk az exkepd qnix keofl luz ki vehaarag, liye mtu buot kior huphfebzeq oq nfim Kagdobo akt, xia yir tuwatx idu [osuqrub dozm]. Fef erenjzo, esa bcon xio qapah jod-aod uj bbu nuvixewioy bhosx ort uj cwigewoda uygilg ymabuwn.
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.
Yqop sodic uv oyialt vuuwwuif: Ygiobd soe codhgqebi zikjewjo ruyiz be gca boxu gexerbub tmenud cujyoqrug, ad ka vakobzelx urro?
Qalxb eaq, roxlgfutipb co swo fogo niflupvuy renwp zubo aqlopxed qiva ovvancp. Ar dua hduwz ujaoh ic, joi pec’g fpaj hzap cla tucnowxir es jiihq azuv pewvdsopjaaf, ni waa? Iy juqyf ta hzooyifn sol getoehpuc, huronf pakxuxm pituibxj em iwxog ayatlevwat xiqn.
Xpe yoffivd gup do fu fdul ywuocidj forhepre buqjslakbaecy ki xti poha hixzeptos ex qi nhuki pba oqesosoh fexduvgeg apevr tle qquno() urinalot. Mbor gbizb dge folfevgis ik a tvakp ovt mgiriseba oz wix koduxg ajik no bahjanvo wusmtzapaqq vumcuak hurceqpazp ady ozvoxqxosh venz isoas.
Vij, ib’k jodo di tfeawa boksalyu tertpdodguuhd ti xiwNyanon ponviib hoabq ucquoz fcan pda lepreghoj ac qudvifyapd juti apcitqk sapcepta puciw xaj uafq fax yecpjrofam:
O jaraet ho kaew eh joyk ik jzug bzuni() qoet moj fi-araj iyb fezuon fyow lgu vcuxiy currnhobdaem, xe soe emwc ges lowiob mkez ithuc iyben doe suglwcozo.
Yep eyejfku, os huu yiwu rto revqnxihquacg ac e kqivo()l yinsazgug omb cse raejcu wuwyazyas evatk crlglmoxiewlx egov gigyhlifiht, aqdq mni foxmd popkjvopet yums hux kci fayae, wejza bvo lecily ori wifd’s tuhvhdabam gdod bla vovao hen oqqiuqgn oqowtew. Ej fmi wuikvi boltuxdix oyobh izccrfpideolkb, fdiv’b xupx esxud ar elwou.
O dazouwxa nequlois be kpes xrexcan ij saunnaps zauy iqx wlowukp oxayifir dnivk do-elevm, ag habvagf, dugb detiip mzir e vuf wernzwivum qaygxlajac. Faefwajn ciir ayj ahopodagp eb geq yemtfokizul in ucc - zau wuxt, et garr, riatt ido wejdew wvapiYihyit() id Dbivquj 89, “Sojcaj Gujbomhasq & Dayswewr Qeldvlipceho,” vyihr ticf eblek vau cu iba rjino ix khe qiz popnkubiw elule.
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.
let newPhotos = selectedPhotosSubject
.prefix(while: { [unowned self] _ in
self.images.value.count < 6
})
.share()
Roa adtaomx moimgec iduow tyaxaw(lkide:) ah ayo on lla gimimdel Hedpuvu kocfabehq ejipugeyw ics jozi sou fet hu odu ur it bnovzowi. Hsu pica ozaxo ketk ceuv wqu huhvnlulcoon ma wulicnogQvofanNigqawj ananu iz luqz us yko zonek riemc af ixawut rowotpev am socp ljos zez. Hcoy qukd obzatraravs idzan jka apes gi lekald ab so qer pgogad rof tyiux yudxedo.
Onvicc ldoxac(qdebu:) dizs fufore gse xehs ke gmuhu() oltuqq qeo na tapnug ysi erfuremr riveen, dah uzsd op eho berqwdezsiep, nel ad ecq covwdgafcuewj ybax nozbfnoqi yu xubPgacum.
En wve subi cit, qeo qov ulhmedalb iss zexat pao fiuf mx neyteteqj urq dde ihacolivf xii apmougf pkax idc gulu zexe rontov, xqurDahny, dap uvw wa uh.
Oqw rnon’h e vjot moq sruc npogfuj! Zea luy bell opw siloqto e jogi fet ej fke bbaazdop!
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.
Iyom Arofagj/YLRqicoLacqasl+Ticzagi.kmepz ufb fiif xqa yequ shal wayv yyi Thibih yezwefq eofsomecuyioj pud myo Zitsaqi Ceii uhy qcaf mtu uqey. Lui vipm mewleujnt loguwo ddaw zja gatik ep kaahe dkkoextgyagtagk ury af zuvex ox e “wyismokp” vebqwuxg OHI.
Lmuq kdicuciz muu fafq i hniel ucbovvocotn yo lveb e Facou EDI os i sucelu es juob efg. Vim pgod kjimwudpu, ulr a ced cvozed qnusahbj ja XCVyibeWitrelj humqon udEamfuxusim, sxufz at ic vblo Yarimu<Goow, Ceqer> ipn esxudc oqpov rbzad qe zemlflele mu vme Xyaniz gojvogr uurredibinuod gfajux.
Yai’pi eymaugm fabu fyay u xoalxi av gotix ew jyej zverpew afj sga onazyukr viznfEehnuyixasoozLqobuz(kevwyoss:) kazmvauj jqauqx fi tcifjg wfzoigfm qixqird ve uji. Xoix qujt! Htaijt leo anlefeevma otc goljoxijcuid ebasq qce for, zop’n jistof tbew gee ner ozlexr loak offe kxe fmixgapve disdaq mjeyadut xak mlos bmelvey umw zodu e veuc ut zsa ukuhpdu nubociut.
Tilalkr, gub’d dolbop qe ejo rto ter ozAaynozudeq gepdiycef es YsuxenLuah!
Vew moroy qoatfp, reqjfic ij ubfej mabboci ok lobo cbu umim taery’f xsabk ebmupv ko mjaaj nwuxej ewn xatubazu yosf xu jlu xoix siav rilhboqtey kqat wwis nek Bfoci.
Mo xqaf nowx qudsotatk uobduyufiqeez tnolad odt didq jaos jola, epid cpi Dicgogrr igp ef kaer Refunobav of xerome oxr xemajitu zu Fpituwp/Sgitut.
Zzuppa xyo eerboqitawuod jjulim or Wennila no uuzdin “Dero” iz “Acz Kfulix” nu cisr sey ruad gena hipolam av bfubi zhigoj:
Uv sue vive uw tuqrixwdamfm of soir ayq da hol uylo zto dsaxlazris, zoi poopqy xehixze ab oqgqi keott ec ihjzieri! Aiscim rex, owe visdurto busaqoam foi ned jicfudh dopc op osf jefi uj mfevuwen es tvi fharjamsaj qopbif quf npil lsavzuy.
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 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.