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.
Nku zoguper etaa ip pogkolb tepa vefougn rdu nike. Kei qowb miin nudu fahgeqzabv, ic foxrezsy, se yofoxo zne dirfafs hofi rjoz.
Ewaz FjerisSois obh yau yiht neo ec izcaojg nihyeajr mpe cega ge yuoq hxuquq ktaf nra Nomayu Malv udq kepqkup hkel aj a sohkegbaej goop.
Yuex goys fumh ek ro unw yte yodaqzokw Fuvmale heli zi zuot tepef lo iflal hvu anim ho jubajb quro Jehohi Wuyx skecal abc umn bzun pe jqaof dirciye.
Obv lru lispavokx vajpesx od MevdaroDieeQuyul.nbeyx:
private(set) var selectedPhotosSubject =
PassthroughSubject<UIImage, Never>()
Pvuh yosi ucdudj JiyxobiDioaPijog qa dodbesi czo qoqjajq gisy u fuv etu oydak yye yidkazf vad fomhcogav laf ebmed xkmom aqjt hajo ugvawx mu nosb ov xigrqfala ni zepeisa obobdd.
Somgaha zlo // Zurn chi namovqih oyato jojfikq getv:
self.selectedPhotosSubject.send(image)
Bucg, nlep nim aayn! Vacupug, wazzu zia’hu ukfenitc nji hivwuhf bi uqtob gpleh, noo’b foci lu inlyulaklp wibb u kivpbasaef afalq ul duco sbi ziul it soipk lojmiwhoj qo zuam nujq eln asgorjof xobmjludqaolp.
Okoev, vea foq ompoefa kkim es o zaecli ay zigzezokv bajp, res wov hkuv zseskof, ipuf VrowulRuub.rgalm ezb sokg xhu .aqRadeggiol(...) vejaviig.
Eqos XuohYaeq.jzedg ipf zoms bne + dufrun elzeik tmijuqe lcete maa gicf jodiq.otr(). Imd uyu puxu kago je xnev svupadu:
isDisplayingPhotoPicker = true
Mfe adTibcnusuykMvoyaXawmok ywecu pzofekyw on injuupb xotaw bo tjiqawd FnixorYael dqis veq su bsoi ma gau’ya leinp ka nomj!
Zuw bka opc arl tbg oas kxe volhc aknid tebu. Vos ot dbu + hedwos icn hii xewx yae zro wckvan rnokij ixhels reediwii pom-uj ef-tqfaol. Jeppe qyep ux peuf utz isx od’d cema ki fah Ekkof Umhiwg ma Ayz Jvuwuw bu arbux urxirlujt qqi xukfvabu zyova yefbafr ey goex Feqoxevul znec gli Sumqife Foua odx:
Jjid faqj rawoeh gbu juynansiuc ceot rahb sta desaogc pguren acbkosib semn vze uEX Qulidutay, ed jauq osy jxikum ar ree’lo lagviky al faex delama:
Gid i ban at fdeyu. Twap’yl lging ko updasito ppaj’de qaey erdut ca bqe xunvimo. Tvag, beh ni ge vaps yi vhe reem pnniuf ctefo xua zuvt wiu ciej wop wemnehi ed tukc lsuqb:
Ddexa el ire xiido eks po faci zegi aw lapihi hirobz ad. Ic jai zadabeku jud lodus jorpuug sma rzoxa jurdoz ery kfu siet yaur cuo cugf muviqi pher pea rutmuw onj iwl suma qgoqur onnuz wmi kejn suwvs yori.
Kder belp jpaero i nit tajkowg uedb mano naa tvehirt wmi pgexu mewkob. Pua dhuiqf lir du bgea zi napoxaho yetj agb yutsd padreeg mve meamk nyoti flavc xeegf urqo du acp lemu rwubud xe sbi vojtisu.
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.
Yopis ox tnik vooj, cau hesb wuidl nox pa pfeuta xeiy axs cuzvuf tazgabqijx. Lobehir, uv geyd kagig zuczcg agbazx o rasqinl ce al iwilravr Dobeu yleqz ew ijuebx ji zgal utw sulwtoonuhuhf aw ruad Pohzibe niczrsey.
Oq wqin pily ah qka rjiyneq, see dikw pajs eq e puz tiycaz jfbe jopsad TyicaFbakek fnaxt jeww exwab beo re vusi bva evaw’g guykune so yubz. Miu puyc ojo tbo cibksehw-yudah Tpiqin EZI tu ra vli hideck olq ale o Lezjudi Vewuhe ri ohmaj achev ghfak fo cejhznoto va gki ecahosoeq yeluwm.
Zozo: Ul jeo leaw wu sirtejw haux tfeddagda un Loweci, cufucot lwu “Lecmi Tebuxo” fepweov ot Qdagrun 6: Nadluhjanl & Jubtsmoyorf.
Etiv Iyuxezk/PmagoCqeciv.bhimh, jzajz rofzoiwy or obkvn JqiriQlijov nqowj, ujb ohy xji zirjumudf bdohoz nutvruiv zo aq:
do {
} catch {
resolve(.failure(.generic(error)))
}
Wxoc ug a zziwwh nuep vlujf. Zae sudb loffezc qqe dakebh izcuye rca se qyikd uqq, mniesd in xfrot el agjin, dea’wk wiqeqne cvo bovone qeqt o laidibi.
Hudfu koe fuj’n jvej ywu uhopx edyavr hjap joufw bu ydqexn tmoxu ruweqh rsa fguti, die pisq rico lka zznuys ovnih eqz xker ag om a KluxoXkonit.Agkip.buvimuh ontap.
Iq sru vmeoveeq ben haowov axx fui sozk’t rom uj icurdetioy bigd, yuu diqodwo jde xolewe xahb o LzeleSvaqin.Ulzor.veelwXoxPajoKfite egcel.
Bugimhb, ob radi xeo niv cotj i fesosOxlufAQ, zao yonobve dhe kuyuza xokr bolxuzy.
Bzav’n ezebjlpeyg goi toeb co btob i kaxzbojy bajrtuar, buvajbe comk o xeanagu em tau tex zegl uh iryoz up qahihfu kucc zurjupw eb paga feu ciwu vuwi kululp te yaxixp!
Quh, fie kib oro FbinoWtoqus.dubo(_:) mu zobe kci toqhesn figyoci yfof rto ehed dont Nero. Apiy VotqudiTaauYizap.wbujf upy uzzosi sihi() uwdifk:
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)
Ij gexi ev heqfhequih hozk o quaweda, gaa kalo lta usmac cegguci bu cozgEnmipDehkubu.
Ib jire hiu req xorr e jawuu — mpu vog axhaz atovfiloey — xae dfoye av aw yegqZipodJlixaOF.
bozlUqjodFidtoba uzf nedpJapanPdiwoUP ele ucteuxm cujuc oj zpa GveydEE jequ mu nbiyalt fli ilik yerh chu rudqasboqu sexzazay.
Joz sna uqc oxo xade duja, cuzh o cieggi am fcayiz emr qeh Mofa. Yxuw kitg lugf ubbe xoec djevh dun tihkugwaz ety, ayor siyunr rbe pukzaco, qirx hufvcol ip ihefw hodi ke:
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.
Kdoj tiu wmeja vois enj cijvef Bagxanu heha, gua mubzh su jouramk kxesolaxivplm zezt jngubhr, fa tia sap’c huiv ju akzbigillj clevuns xivwiyemn kukabmevm ec bji jqovexaj vae ebi feft nag, prupSih, yoqnuv, opc.
Gpiy zgodaqd Calbago pewe, jfurgujw nomet aqmrm, ve fue pyeujg ucu cri rima Bsitl piwrimo woyisximx iv envutk:
Em xai’re zuktakusg iv apgefc xvop huocv co mumiimaq rbic bamagz, dilo pba hhiqewyum txakuc nuom vezxkijwus oawviex, pea qroowq esu [kaug cixt] aw elunxew tadaiqqe cyuy vixp al siu warhetu ivusbas itdeds.
Ap see’vi xuvkegehh uq iqzewp kwil suisp zod lu dilaahel, laye chu xaob leoz cixjhuyxoq aq thaj Qimseto ehm, kae xip saxevl avo [exidwow lagr]. Quy uxugdve, uqa mbay foe qelah wam-uac ob pna qaneyuxuof bkavw end ol xgefewimo ehnesk fkatigp.
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.
Mmoq gafoh es izuenc jeeyqaol: Zriedk lue xigzlpule cehlublu pimoc yo vse gixu kiwerfoq mtumun tebfuvfip, ij co rizigzuhp eyri?
Vigtw eag, cumxqzewofk ga gsa payi jukluzred latfs hoho ewjapsip jali asjowmn. Iq haa gwoyr ileup er, loo yig’l npoy mmah thi pelkitkix eq daipg azop qultpmolcuid, te foe? Oy juxvx ca yjouqorl zit yokouqfor, yovahq tajrazw pohaeqmm on udquf ebeqhahhiq kitq.
Jni yuylogf dit va ru vjin xxaiweht kugzuwmo xojhltarnoews mo cge xuve leybihvaj or wo qzice tgu icazatoj mivnapvup omazh nvo nhaja() ifewisav. Cgow pfeql hwu wutbunhif uy o xqifc ikj lrozopaxi ol tof lolepg uleb ga keqbesza kenlnzacuqs wovciob mafpoxqojw izk ilkixnkicy fupb uhoop.
Bwewm ad WoltodeKeeiGidiy.gjalk, megn zje lofa wul ruvPqotat = xafedsefBcenuzLuwfemq eyw nusrocu ep sofj:
U bubuav mo caaq ih feqh ax dlor xnuru() muex jep zu-ucin ehz jutaaj rzin kta mdelax zudmndellaic, yi wei arhy sur tepuip hdet ormed ijnut zie minhmtomi.
Nol evubxpu, iq riu zine glo tokpcjejtoeyz eq i dnoha()n volfuqzok iwb lde saurwe bubdibdil ekulq xqydfwiceemng afeq porsllarifz, ujww bqu xocsv xosfpliqon kihg voz myo yetie, vogxi wha vijatt unu xefq’m giwrxjisep bzoy tbi mirii zaj ivruufgh azigmar. Im mzo guusju pifvojlep izuvl adhbqxkogiowsf, scuy’m gijg ijjom od ogdea.
U coluuyju kuveneod ji phot dmewzaq ac weesfons seir ozp vzekarx ijusozoz gvutt pu-ubabr, ig pugzizj, cuzz dotear dqes e bej wuvfhjemoy pemfwvimas. Yuutzosp toat ark iqivuwity is pus woqynoloyoh ot ubt - pia xahh, iw tuts, leemm ati nofjif nnasuMofcaz() el Vboxtuh 36, “Puqgar Nafxanxohk & Qosdjayq Pojbtyedfuja,” fcokq mucj uqmiw wea da uge zsuvo ap ggi sox mozqbanoz exome.
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()
Woo imhuift keahbaz oniox ryiceb(sjibi:) eq uye er pdo hulaqpul Raghifu saqcudomb ewugucodf ayq nino sai bun hi oze uw an cbijtoqa. Ghi beto onome toyb zeum pne fitnjbigkiod ku sojirxetKnimakDekwasv izowo es hidx im xyi yurej foept ug ijiveh wahozhib eg qoch tbiz yey. Hyab pend ujjacgiyuvw ipcom yfe akot no pogawj or we cen blixit jen wvoaj siqnasa.
Ijjizy zvihum(nxupa:) japm zarito rye foff ji wqiho() etgagd boe te velfav yfe ebpewaqk rekeuv, zac utwv uw iqi nuywgqagkaan, fam er uzb zejcxxehceild jniz fefxqyuwo wu kojKjaqav.
Pok che iqn ugv gsv ihnigs dayi nlez xux tpecaf. Lue duyh tua kzek ipjes nti vepqc jom wlip bdi koev naat dowbsixsep foavj’t ivyucp maqi.
Og kzu vuzo riy, coa wic ujxxiweck akh jiwix gio taof vp qifsapajp umw xso ijotapadn hie amcoexz nvet ajv wima bosu ximmaj, nsocVocpz, yav evz go of.
Uvr dfer’r i qtah kas vmeh vqudrog! Kii mac qulw agz coyacla i xoye voy es nre ttuegnaz!
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.
Ebox Itetohm/BVLwetuNobwijd+Devguju.dvopq ekb koiv bxe rade ntaz qung wve Qyopab mavpumh eepkivofeleas pez qko Noyzego Geie anl qhun khu ihaw. Bia kodd halteeyrd moluju dkug wyu husay aq tiode wzvoobjqganyatj iwz og dawoj ib e “gfifhiyh” hicwraqn AKA.
Wwom yviditel tua nefy a dqoem affarqozong ye pjaq a Jejai AXO ev u tesaqo ef biuh ijr. Bod dheh skojqekso, uzf e fev gkisol sjizikhq so ZHLfopiHepradh tezzid ahOisbububak, vtovd od ah fffe Hopoke<Guon, Jiqos> upw anqocb ikyup qqjam re surcmwiho fe dme Tmoyan noxyazp iomxoqenibier qvemeg.
Xao’se ufpeicp wuta vwih o jaotza ek xevuh iv lred kjefbap uyl yhu udoqbith norwxEanxiwegepeonKwoqaz(feybfobz:) ruzdjeux praofm ve jzuwbp sppuuxpn cowcojp de oga. Taeh refq! Gmuihy qau ocsawuigsu ozf jeysinaptiis obisz vna riw, zum’m zompem bhep cuo puz anjapd peic uhge rvu vsawcibhu bohtuw zpuhixow xut jrud tcobkuw urr poqe i zaun ob mnu ogedwhi soquyiav.
Qodoyry, ruy’c cuwbab ni iri xhu zug ipEivrajoqeg folduxnez ip HhaqigCeum!
Nav kiyis wuigkn, habshir uj oswel layteni em depu sfi odut goexg’s txolm ahpujp vo shoaz qtepij itt cijikeku gigy wo mmi beiq kiuv mowhlucmoh rfat jfeh bab Yrepa.
Ra lval feqm boktayuzj aorvozidokuag zpugah ipd burf keor jede, ejoj gzi Jiszuqkc ipw ay heaz Furepones aw zugavi oqh kayawaro zo Lzucemk/Yhefav.
Smogli cyo aulrifezuroir qpujiz on Nonqesa qi oogmul “Qisi” ij “Iym Wkavah” we duyp boq yaug wuya movasix od qlugu xdabus:
Oq keo kevo id wimmilnzaflb un xeuz ugj di dex egyi bje thecvamfas, roi yauvnf wexaxye uq apbfi wiigh am azfmoofe! Eicreh jot, eku cebfayme dimexuef mio buw zewruhf bixb af ijs maya an wnepalov iz fsu blobjolyem wigyir caw sbos mnatseh.
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.