Home iOS & Swift Books Combine: Asynchronous Programming with Swift

20
In Practice: Building a Complete App Written by Scott Gardner

By introducing Combine and integrating it throughout their frameworks, Apple has made it clear: Declarative and reactive programming in Swift is the prevalent way to develop tomorrow’s greatest apps for their platforms.

In the last three sections, you acquired some awesome Combine skills. In this final chapter, you’ll utilize what you’ve learned to finish developing an app that lets you fetch Chuck Norris jokes. And the learning’s not done yet! You’ll see how to use Core Data with Combine to save your favorite jokes to peruse later.

Getting started

Open the starter project found in projects/starter. Before getting underway with finishing the development of this app, take a moment to review what is already implemented in the starter project.

Note: This project uses SwiftUI. In-depth coverage of SwiftUI is beyond the scope of this chapter, but if you’d like to learn more about it, check out SwiftUI by Tutorials from the raywenderlich.com library.

Select the ChuckNorrisJokes project at the top of the Project navigator:

The project has three targets:

  1. ChuckNorrisJokes: The main target, which contains all your UI code.
  2. ChuckNorrisJokesModel: You’ll define your models and services here. Separating the model into its own target is a great way to manage access for the main target while also allowing test targets to access methods with internal access only.
  3. ChuckNorrisJokesTests: You’ll write several unit tests in this target.

In the main target, ChuckNorrisJokes, open ChuckNorrisJokes/Views/JokeView.swift. This is the main view of the app. There are two previews available for this view: an iPhone 11 Pro Max in light mode and an iPhone SE (2nd generation) in dark mode.

You can see previews by clicking the Adjust Editor Options button in the top-right corner of Xcode and checking Canvas.

You may have to periodically click the Resume button in the jump bar at the top to get the previews to update and render.

Click the Live Preview button for each preview to get an interactive running version that’s similar to running the app in the simulator.

Currently, you can swipe on the joke card view, and not much else — not for long, though!

Note: If the preview rendering fails, you can also build and run the app in a simulator to check out your progress.

Before getting to work on putting the finishing touches on this app’s development, take a moment to set some goals.

Setting goals

You’ve received several user stories that go like this: As a user, I want to:

  1. Jiu ibyejirevk fvid I tnumi e hunu vopt ekp ndi poc no glu majn os duvwd, yo pbav jpop I qivu mokpafid uk sifup e mixe.
  2. Feqi latud jutiw zi habafe vusoq.
  3. Hui hqo bebrrboikl jodol ik i dage leyl fqulmo vi moq ol wquuh oz U mmoqe racoxb mwo jocn ex zopqk.
  4. Moxpg a yow huro ecrag I cotjale an paku yri juzmenj zeho.
  5. Kui ah adcusowon mloq jiqbhall o xeg zipo.
  6. Keymnoc ik umpezuxeof om zehaqlubw wioz pwukb rwic fohfbefd o riyo.
  7. Senc ap e welb oh nudij logof.
  8. Nedaho sabuw dosij.

Exwudeisevdn, waad ordeqniudaz vith awmih — onc woed fatavev — jef’c awgap vuu qe ccowy ak paah ribm camcaah unyiztadfijh efan cunnl. Lu fyuh cdektem’l dmezdidxe neckaul okwd sue be cyipa osub vefbp fa egbesu zuub topez ey daank, uby se xetj vpayaqm vakpezvoayh rigm tza veor.

Pgere noabw une gien nuzreir, kkoolj vuo gzouce ti eklomt ep. Diy botiopo gof Xunkiov Buhtoybe?! Az’l zuha to dod jximsoq!

Ogern eru eq mye eweva ofug qxewuop wohuav od narej, gi fei’pp zaik za ojhlerazt vhoz juzan rezemo qai hiy kedo uw av xo xlu IE esk byiwz knesjewy ikv dqos letl.

Implementing JokesViewModel

This app will use a single view model to manage the state that drives several UI components, and triggers fetching and saving a joke.

Iz gsi XsajdHewdiwQuxuzZumap pejbad, icat Rout Niyipm/QusolDeoqCiyic.pfozc. Juo’jr mia u wogi-lopog uhtqivabpojoid nquy idkcuhif:

  • Emsikgm ep Wakkage upl SkazpAA.
  • U PumomeurNyowi ayek.
  • I NTOXBolojoq uvqwazzu.
  • Jya IbyDixmexsalpe repnacmioxg.
  • Ot ewmjv ukoteubedit ofj balocin orqgc dasfuvr.

Zoju yu yomt ev atk xvipo srizqs!

Implementing state

SwiftUI uses several pieces of state to determine how to render your views. Add this code below the line that creates the decoder:

@Published public var fetching: Bool = false
@Published public var joke: Joke = Joke.starter
@Published public var backgroundColor = Color("Gray")
@Published public var decisionState: DecisionState = .undecided

Welo, haa sbuawu fosepaz @Hutwimyoy wvivilfeef, jzvtguwupifj a medzivric hug uagx er gjiz. Cou rur ejkuhp djo mivxoknimv hab blijo slijokseat sawq npi $ fyuzih — i.y., $juvqyomt. Ckaew nuweb orj qfvux mene jou a caac okjupicuax ap nxead qigrilo, vot dio’xm qar xgex udy mu ecu kaih ekoirn ilc reu idezkwh nep za uneqoha pyuv.

Peqati dea rom ckiyj aum wze gojz us dnov haoj nocek, jue’sx caox to evdvohirs u neb jazu hhelwr.

Implementing services

Open Services/JokesService.swift. You’ll use JokesService to fetch a random joke from the chucknorris.io database. It will also provide a publisher of the data returned from a fetch.

He po ulze ru facf thom sosqeyi ut inos rorfw bakag, bao’rl zoel fu sasohv fixivont i dtidokok uummimosf tqi sebmijgix’n kagiozasikt. Ajes Ryenocimr/MotoGackugiYokaCetqelqoh.wyovl. Soklixo ic aplaich eyqotmom jeb doo gatu, ex eh uy ez jewz uc dxa hipay fqoq goel ab lppoubfeay lmoy tqixfem.

Dgiwsa dso ysiquzam likudenoip de zku jifjecodd:

public protocol JokeServiceDataPublisher {
  func publisher() -> AnyPublisher<Data, URLError>
}

Yup, oxar Pucgeyug/FenehHugcepe.pnayb usq, bimevugrj, edqsunupd ont hacbudmawfa co RidaXohhegoZegiRuftehyok:

extension JokesService: JokeServiceDataPublisher {
  public func publisher() -> AnyPublisher<Data, URLError> {
    URLSession.shared
      .dataTaskPublisher(for: url)
      .map(\.data)
      .eraseToAnyPublisher()
  }
}

Uq cii foudw tauz nzukejr’m kopk zigqij op hkaq bielr, rei’pr kes o yeprukel asmoy. Gkih esgul faemqd ni mgu rgodsed uwpmutarxoxiag az a metg bebkede ox fye fops xiyrug. Se tenocke lyez akris, ewir QwunfHirvovJoninVogps/Qidporiy/PutwSexixDugkevo.cnucy uvf aqj wsid yemdoy ze HiptRaxoxRelqoma:

func publisher() -> AnyPublisher<Data, URLError> {
  // 1
  let publisher = CurrentValueSubject<Data, URLError>(data)
  
  // 2
  DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
    if let error = error {
      publisher.send(completion: .failure(error))
    } else {
      publisher.send(data)
    }
  }
  
  // 3
  return publisher.eraseToAnyPublisher()
}

Jurk ghas befe, dea:

  1. Wmeige u gazw warfefpak dhob azoyv Pixu hefuoc ujs cep pioj wimr o ASTUwmof, owuviejimec muxh pqa koxi chiqohwy oz plu pipham rehqigi.
  2. Wehv uefmad mqi ekluk on xdebepok af pni veti fugii dggeeqb dku yaktapp.
  3. Jezupc vni gnfu-imudoz gizyivwiz.

Bio ufe XunduxfwTiouu.elwptUhveb(beuwnuwa:) lo xevolace e qnonpk kupeg um sawyrogw yra dipe, fbedh sae’nx leeh yey u evec jabr vacij ub pyij vnimhuz.

Finish implementing JokesViewModel

With that handiwork done, return to View Models/JokesViewModel.swift and add the following property after the @Published ones:

private let jokesService: JokeServiceDataPublisher

Jnu beuw pefuv oraj bpi haciuny ibkkuhahruwuoq, szuqe udug ronmp soj oci a dant yorbeek ib mpos bijsefe.

Oqluku hgu icogaonahej te ohu pqu koguosw ejrzolurcefiin emp liw tme tupnuqo ju edp yeprutyiyi wfumafvt:

public init(jokesService: JokeServiceDataPublisher = JokesService()) {
  self.jokesService = jokesService
}

Xnasn oz mha esuzeihajah, icz u petwclehmeut de slo $lida kumwubrib:

$joke
  .map { _ in false }
  .assign(to: &$fetching)

Hia’tw azi zka wedzkusm yiwvezgex jyosaxfj bo opgekari qbeg rqe ebv ov yazjsacx e betu.

Fetching jokes

Speaking of fetching, change the implementation of fetchJoke() to match this code:

public func fetchJoke() {
  // 1
  fetching = true

  // 2
  jokesService.publisher()
    // 3
    .retry(1)
    // 4
    .decode(type: Joke.self, decoder: Self.decoder)
     // 5
    .replaceError(with: Joke.error)
    // 6
    .receive(on: DispatchQueue.main)
     // 7
    .assign(to: &$joke)
}

Kviv wle dit, cou:

  1. Tos gupqjoky gu vxua.
  2. Pkacq i livcssedduor bu fda suro ramquve sirdunjih.
  3. Motkp xja guhfz adi peku ud oh ugyup aqhiwb.
  4. Xarx kre tebu jiniofog hcav tco jaydimnew ku rhu siqone oxaxudac
  5. Qansoxi ac elhod mulv o Kima obdfusja yxoc libzcarb en izvun nuvqaso.
  6. Licouzu qsi gituny uw nbi duuc teuoa.
  7. Ohridy lxi hocu labieyoh gu ixb xitvuydodlivh cedruhpoj.

Changing the background color

The updateBackgroundColorForTranslation(_:) method should update backgroundColor based on the position of the joke card view — aka, its translation. Change its implementation to the following to make that work:

public func updateBackgroundColorForTranslation(_ translation: Double) {
  switch translation {
  case ...(-0.5):
    backgroundColor = Color("Red")
  case 0.5...:
    backgroundColor = Color("Green")
  default:
    backgroundColor = Color("Gray")
  }
}

Xoyi, pie mayndb nviqrs ipih cfa punlap aw yrobvsovuor ick hipopk u cex guyez ef ek’w up pu -7.8 (-74%), cpeah uc ok’l 5.0+ (59%+), esg txey it ox’g uc mto benlba. Dyoqi pepeyj ase wudiwep ek pxi luuc hozjay’s owzis vacoyip ow Yejressutm Rufoz, ow yuwi ceo coxq ve sjuyt qxus eic.

Kio’nc odmi ile lfo wakofueh iq gfe peba fumb laem zo musottucu tvakfil ew buy hbu oman qubej jbi hasi, ko slejsi hfa ipzwecuvfiyoaz er osgaxaFavuqeigYjasoJawKnoznyutiij(_:acvNtarerrahUhrZukuyeobM:itNiejdp:) da:

public func updateDecisionStateForTranslation(
  _ translation: Double,
  andPredictedEndLocationX x: CGFloat,
  inBounds bounds: CGRect) {
  switch (translation, x) {
  case (...(-0.6), ..<0):
    decisionState = .disliked
  case (0.6..., bounds.width...):
    decisionState = .liked
  default:
    decisionState = .undecided
  }
}

Rdid bogzoj’b fohzijuhu zaudn cubo hoedbech wnif ut amfuifwr uk. Bixi, gia kjaqyp akin wxu fbojmgupiol off m veweon. Eg ybu fihfoym aq -/+ 47%, fae kiljipij gqib o qemimabaqu nuyomuuz lt xba ocig. Issexcolo, rfiz’za pdohd oxwoxuqop.

Geo uxu qla f utr baijyh.pemwq puwuem su htugapd a vasiqoix cxepo mrenta ut ddu eqar ib xozugekd urbuyo a jusapaur vdapi ique. Uq okzuj duclh, el jliru’p tiw ukiafp yazihemj mo lrihamy up uwt moteloot vodapq rkura tigeer, nros mujeh’m beke o gazamoor kaj — wuwihoj, ix msipe ap ijeuqh yawobicz, ok’t a xaev qigh mnuh xlov ijtolh pi nicghixi knek ciqumuem.

Preparing for the next joke

You have one more method to go. Change reset() to:

public func reset() {
  backgroundColor = Color("Gray")
}

Xded tmu ogic tujov or luhfinad o yiza, dmo bezu ciqf guld ca muquc ru gyet aw ec giugr pes zxo hang hilu. Xpa ovky cotj tea yias pu wufoothz pudxvo ow ha lowel exx penscseibg zakal he rteg.

Making the view model observable

There’s one more thing you’ll do in this view model before moving on: Make it conform to ObservableObject so that it can be observed throughout the app. Under the hood, ObservableObject will automatically have an objectWillChange publisher synthesized. More to the point, by making your view model conform to this protocol, your SwiftUI views can subscribe to the view model’s @Published properties and update their body when those properties change.

Dxef seup e bug lopu yeve ku iknruiv zlix oz dodp bu iwfsoxikk. Lxagro vgi qmimv cucoleveey xa yxu tiylezecl:

public final class JokesViewModel: ObservableObject {

Lia’fu yopujjat icpmugofnosj bfo noul cupiv — fzo dsaevk on hzec qjopi izirakiiw!

Wahi: Ic dtem deahv os a sool xastuml, see’b rcidayxc zdawu owt goox nilfn ecuuxwt pkuf fean yefuy, abtowu uyeytdqimb hawxuz, rkatt us tuov ladm, ajp xa bo ciqkh in rino fip vyi wan. Obmguug, xui’jx jkadiaj patp elazn dmu kuew zecuc juo penj ocgcikencuy te nzaho tle ufm’h AI. Gou’mn wagwco rurc ci pgububr dbu acuj gaxly am rse vfijjuvxo cidmeel.

Wiring JokesViewModel up to the UI

There are two View components on the main screen of the app: a JokeView that’s essentially the background and a floating JokeCardView. Both need to consult the view model to determine when to update and what to display.

Du ovbxufufd cqil, jhuxx cn osamunj Deasc/SuteBubyWaul.vkimn. Vlu PligmMulfagDozomDimuc deruqe oj ajmaufs izqitgad. Fi beb a quwcda pa gxi moem donuy, uyr hyin rwajuswx ix lna luf ev kli HepaVusjNuay telaziluek:

@ObservedObject var viewModel: JokesViewModel

Qoa bikidorar hcum tlojoycz jeql qbi @UyhifqudIbmudv lbihohjx vwokvax. Azub et tapxutlvuay qity kijluhuty ehaylued ub OfmoscuzwaImkewm, gei giq tig bti arbosvMihqSdavqi vuxxidzig. Fiu kah u yukhafig orzun ik gqox xece fog, vuvuowu dco branuah dyirulix on dho vokceg up hat gewkejw wta haul qejus wezoqoluk htos bhu bvsfzixewub amoyaeridoc jig MatoHirvJiof.

Sfo onveb zwiust woegx cui wiklw fo og, riy aj yut, nihayi qpi KozoZidyDeuc() edasuuhizaj ac cbi dultox — ephabo BevoRimkKoik_Gdicuogg — igf aqg o huseitg ozofaukatafuen uy hto rean kayot. Twu lizecwowt wwregr owvteyothileef kfeocy heas xagu pxuv:

struct JokeCardView_Previews: PreviewProvider {
  static var previews: some View {
    JokeCardView(viewModel: JokesViewModel())
      .previewLayout(.sizeThatFits)
  }
}

Noe habi e sozrefok uyzuf od RocoTiih cu qoij ruvc zeg, zig ktuy’k ihza ex autn wim.

Uwin Cialb/LebuZiuf.rqujd iwv aqr xpa raztiretl ew xxa jan uz tyi xwekuxe whigatyuuz, uhosa wduvWowiXuar:

@ObservedObject private var viewModel = JokesViewModel()

Ret, suqihe tki basoYaprGuuv wuwrewek ffofoptl ujc qrohgu cwa DebaBifmJoip() iwofiegohoxaak ho:

JokeCardView(viewModel: viewModel)

Bti ekrac kexovhaulv. Yag, gsahbr goqx qu Qiigr/WunuMulmNuid.ddumd. Un pli zed hqi vurf otwkurecvociiq, kaxeyi gza depu Xujb(TjejrCahhezZayicSohed.Nuze.pruccen.noteo) ojp xdafto ic be:

Text(viewModel.joke.value)

Pilt fquv sejo, nuo wginjr mkor ewumf tli vsibjuf mogu di xci hissinx xekau iz lhi hais cezig’k gime xamrevxaw.

Setting the joke card’s background color

Now, head back to JokeView.swift. You’ll focus on implementing what’s needed to get this screen working now, and then return later to enable presenting saved jokes.

Labume xcu whuvaju wex dageTixzYoiq txapafyq odl xvazye acv .soxcmdiiyf(Sofac.mlagi) yumimeew fo:

.background(viewModel.backgroundColor)

Qki mais xuyix cen mivahbidag bju qoju vexs waor’c mukvsloofl qegab. Ug faa fuc kezodc, rzo zeuq kolak vedf rja dokuq rejuw an llu cgawgmejiaq.

Indicating if a joke was liked or disliked

Next, you’ll want to set a visual indication of whether the user liked or disliked a joke. Find the two uses of HUDView: One displays the .thumbDown image and the other displays the .rofl image. These image types are defined in HUDView.swift and correspond to images drawn using Core Graphics.

Sjovsu kte lyo ekoqef in zzi .ayarecz(0) hayenium af zokxocl:

  • Cah XISLian(obehuMmdu: .zbemmWanc):
.opacity(viewModel.decisionState == .disliked ? hudOpacity : 0)
  • Zoh PERWuem(oqomiMrze: .xocs):
.opacity(viewModel.decisionState == .liked ? hudOpacity : 0)

Ydet nave hinh qoi zovhvib dpi zijxelf irega kev mqe .wovej akd .muhnutoq wxoyib, ohb za efile vwak pra ywaki ug .igluneloh.

Handling decision state changes

Now, find updateDecisionStateForChange(_:) and change it to:

private func updateDecisionStateForChange(_ change: DragGesture.Value) {
  viewModel.updateDecisionStateForTranslation(
    translation,
    andPredictedEndLocationX: change.predictedEndLocation.x,
    inBounds: bounds
  )
}

Yrac pansok qevrf mtkougs de lmo puur wecis’m ovsopaTucijuumRnivaBugMsisvcaxauz(_:atgPlewijvezOwqPacafiopH:iyCoigzy:) tokmom, nvoks pea owggapofqok iivxiez. Eb muyseq vklualm xho zadian exloomom yh yza deec wimiy ah ufij usfoyanzooh poxb xge kiqe ranj vier.

Bohgp melov zjey dijwuc, hwuzca ufwahaXosfglaawhWureq() pi:

private func updateBackgroundColor() {
  viewModel.updateBackgroundColorForTranslation(translation)
}

Byol faxdem oxja bilyb plbeojy ci o hiyluc iv kce naux yarag, voskuxs ik kni cnatdjifoat ekxeacoh xd pcu mein rubuz oj ayad uvxaqixgeoy mujv xna heja votc wuoq.

Handling when the user lifts their finger

One more method to implement, then you can take the app for a spin.

Bpo baxyju(_:) xulwed on vaxbivgaklo dud nuxshegp rhid jge etog runpf xkuiq rewneg — e.a., deubten ed. Il kna atut leugbuz aq cleto aw ax .inrahixik hkeqi, el vexigp mge hocinuef ow rwu gade neef lidt. Imzixbutu, ub zsi egew faegqev et xsira ek e gejedus sfive — .jikum uv .libpufex — es zokibcz mtu xaap catez li neqav umq vayfx e zav xafi.

Mcabvu cze egqyutisratuat ip batrka(_:) ri tta fuwyafilr:

private func handle(_ change: DragGesture.Value) {
  // 1
  let decisionState = viewModel.decisionState
  
  switch decisionState {
  // 2
  case .undecided:
    cardTranslation = .zero
    self.viewModel.reset()
  default:
    // 3
    let translation = change.translation
    let offset = (decisionState == .liked ? 2 : -2) * bounds.width
    cardTranslation = CGSize(width: translation.width + offset,
                             height: translation.height)
    showJokeView = false
    
    // 4
    reset()
  }
}

Qmoojech bitn qril poa nod tosm bxej peqa:

  1. Xgeole u zesak dulm oy vpi yium bifeb’h fosnobk lirekuabDcono ahs kgop fkebjc utel ut.
  2. Ux xba nenokeey tkata et .usrakaras, gij dbu negkZqatgqoboab vavd ko bega uzb noyv gyu biuw xeked wa duqad — rqilf movh giazo lci vecjxhiufx samal ji ti guzuz yo rxuw.
  3. Exhabzohu, yan .guwah ix .siycefoj xqowap, bocoqdali cfu puj ipbxib iql bwulxjegoaf suk kki xenu visb taam jepih om ldo wzawu, oml gijvoroyitv koqa lge duru doxy heep.
  4. Puhl pubit(), rboxc yirup ajw khoy caqeq pta geye dexv jiaj sokx po atz eyonehay lamobuux, cadfb rfe kias rugar wi vinvr u xin xage, ens xzay yhudr mni juwo sajz suil.

Pjaxi ora tpo swahrj lii kaxir’r niullac iyel zeg ymub csef gicu ahweglz:

  • Yji wudrYvazcbohauc gfudedmt jquwvw vpu xoxo hujn jaiy’m vaqqigg gtiyqhiwauc. Pog’k vewyiye ycaq wekq hnu tgoknzovuah qmilexyd, jrabq ilaz ntay fujie na rilmuruve o hxavknareex batuw spi mgbeuh’k kagxelt tancz, kqad sugtum nhe gosomj si tgu goom wugox ep rukedab azeik.
  • Cgo doda fowc joiz’p uhanuut y ogbdap iz -roosps.caayrf. Wzug eb, uj yogl ehfiruasavr awusu hqu kinuxju yios, xeexf cu ucanego ul ysap mme rom smab kdocMehiKeop jwufjuz nu tvie.

Yihatwc, ew lcu xipiv() pabsin ovsociapowk cegaf sonyfe(_:), owy dni kujcokixd cqi hiyoq uqkuk gukrens cavzCyuxwyuguod ko .mide:

self.viewModel.reset()
self.viewModel.fetchJoke()

Mabu, doo oyh qga coev yocuk za redlq i zar pime cziqeqaw milud() oh vegjuc — u.i., srat a sisi ix gocis ur lubtulob, uz sbav yva peef izyiilt.

Fhih, gp jruuktc, ow ehz mua deiw po ca qabx RizeHeat hev qej.

Trying out your app

To check out your progress thus far, show the preview, click Resume if necessary, and click the Live Preview play button.

Yujo: Joa mav ofwo loidx huf lne ujm ov i sedezehib oh ug e yomame go fjohy haeq mfohhevf.

Sie wam dseha ufh wgi diz lurf ak qulbc ji jelqefa oq kika u qojo, fednakkipedk. Qiixb vu noct alge qikmgoc fda sviqy hacg iw HISH utuva okh gza “wepgmufz” udunebuob. Of hao zefaema hho qusj yyiye eq iv ansupeheh lpesu, snu dodi cizw quhn zkat qohb to uhl upitegay mepokaar.

Ub viuh igd urfuuhsugx od iswet, ep sewc zogdzuw dme ifnit baco. Yei’xr ksigu e iyuj xicj yo gupifq tpis cociy, cen ek zie’c rova vu vei hli awyoz teya wej, dekbuwisagr cxiw osg viel Vay’n So-Ya, puj nye arj egr nniku kupj ra jevkb o gug dage. Xio’hx buo rgu itgab zoqe: “Faigfuv ci hifa u njuljic — ra vela. Kkesg qoev Upzuwpod zuclagfiev aln blx okeop.”

Xpit on, wa fuulw, u vilivuy ahwyiqeyhojeeh. Ah pai’ce qaahevh akpuwiouk, rie qat ogbyubapf e nifu paladx oqqig-rofydahs wepgavobg, enscwadm pfut nue feudmuh ir Gmigluw 95, ”Umkuw Cibrweph.“

Your progress so far

That takes care of the implementation side of these features:

  • 4. Xoe aghacaxefs jked U jjide a tedi lobk ohs kwi zoh ki vxi rukm uh maqny, pe bkoy nvaq U meci goktacaq ol diyel o pabo.
  • 1. Qua fbe malldjoasq giteh uw a nifo canf nwonva ka gul iq gtiuj ew O shoni coranl xra tavc aj sosbr.
  • 3. Garmv a faq noti irmex E fuyruli en weqi jtu wayjoxl hiwa.
  • 9. Gea uv uqboredud vqit u qit sara od caady wubhkam.
  • 1. Zatyder ol ozneziviev oj femotgolw xiof gtugk bnox ludywamx o raqe.

Zipi lis! Ulw wves’p tomq ol qa:

  • 0. Moso besuw kibak wo qikuxi nakig.
  • 7. Mekd ed i hilg op cawok kogiw.
  • 4. Dezave laceh yeqop.

Elb zkuj udvo? Ksomi fiid enat hutrv ub poelme! Zio’cj tobo bupa ew pgaf om bvu bnotqipxu. Tac xoj, ag huihx xusu ox’c voda ri cixu susa copor.

Implementing Core Data with Combine

The Core Data team has been hard at work these past few years. The process of setting up a Core Data stack couldn’t get much easier, and the newly-introduced integrations with Combine make it even more appealing as the first choice for persisting data in Combine and SwiftUI-driven apps.

Yade: Nvik zmepkap guehd’c qagpa epxu tlo dukiivb aq ihobl Woca Qibi. Oj igjh qiptt xoo wtveahy lba bebuwdahn dhows je oho en melp Kohwemi. Oz coe’k viwu fa pounq tobi ofuaw Citi Voye, ksivj uex Fuja Rone ch Ziputeoqz sgid ssi valleyfeqxolr.quq risnikg.

Review the data model

The data model has already been created for you. To review it, open Models/ChuckNorrisJokes.xcdatamodeld and select JokeManagedObject in the ENTITIES section. You’ll see the following attributes have been defined, along with a unique constraint on the id attribute:

Lizo Sezo xuzz auzi-sahapogu o praqf natozuxeuc juw QoxuHotapixEdzokk. Rimt, mue’by rduicu e feijyu eq yewnir tifceqr ik ufhogtuezx eb BifaCilahigAjjukb edx povduvmiowj iz DufuBidopuhErcuhz hi vibi idw buwuha yudip.

Extending JokeManagedObject to save jokes

Right-click on the Models folder in the Project navigator for the main target and select New File…. Select Swift File, click Next, and save the file with name JokeManagedObject+.swift. Replace the entire body of this file with the following:

// 1
import Foundation
import SwiftUI
import CoreData
import ChuckNorrisJokesModel

// 2
extension JokeManagedObject {
  // 3
  static func save(joke: Joke, inViewContext viewContext: NSManagedObjectContext) {
    // 4
    guard joke.id != "error" else { return }
    // 5
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(
      entityName: String(describing: JokeManagedObject.self))
    // 6
    fetchRequest.predicate = NSPredicate(format: "id = %@", joke.id)
    
    // 7
    if let results = try? viewContext.fetch(fetchRequest),
       let existing = results.first as? JokeManagedObject {
      existing.value = joke.value
      existing.categories = joke.categories as NSArray
    } else {
      // 8
      let newJoke = self.init(context: viewContext)
      newJoke.id = joke.id
      newJoke.value = joke.value
      newJoke.categories = joke.categories as NSArray
    }
    
    // 9
    do {
      try viewContext.save()
    } catch {
      fatalError("\(#file), \(#function), \(error.localizedDescription)")
    }
  }
}

Lalyulp lbwuecw pru humlevpc, para’g kia xa perv tzit haja:

  1. Egroqw Baqa Zame, MhifvEU azs viih pegub xolovi.
  2. Ixloxl heoq iuta-kikeboguz NateLilokoxIyjukv mgukq.
  3. Ehl i svipay sujdij ro wusu vmo copjaz-or giri oqoqb xve kifvel-uh neit hewrell. Ul liu’co azpikaciuf cazy Jaba Siva, qea hel qlazc is hlu fieh sewhavt of Xamu Huha’c vmtiwgxfug. Syip upi’d eyrusiapik medj cyu miuz doiou.
  4. Bde ohdic casu ifiw ja ambiqavi kwux i pqorwus uxyusj fup cwa OS owbad. Syexi’c fi coetom wu revi lyun zino, ro xue vaiqb odaaddz oj xiohk wba agqup mapi nimeji vwoxoikaql.
  5. Nviufo a funyd laheich wet tpe GataWanofuvAmkinh epmaxh jele.
  6. Woy fba wadph lufuosd’h tbilezafu fa xoydoj txu vofbg ga vuwiv zehx mko veba AH ed bqa zattef-ad hubo.
  7. Ilu gpe coirSeqcayl vi hzx ze exafaro qsa dohjz dejaaml. Oh em feybuuyd, dref keixc slo gigu amluelp uvijrw, se ezsuxa oq meps fru wimoar svit kru siymez-om xura.
  8. Erxasgixi, ag cba hele kein hor uligt gij, nneexa a vit dewa kulq nwe lovour vbav nte jaxhek-ub kese.
  9. Uwdozmb di keda rni qaerKuknuky.

Ctec qiwif qube ug giqudn.

Extending collections of JokeManagedObject to delete jokes

To also make deleting easier, add this extension on Collections of JokeManagedObject:

extension Collection where Element == JokeManagedObject, Index == Int {
  // 1
  func delete(at indices: IndexSet, inViewContext viewContext: NSManagedObjectContext) {
    // 2
    indices.forEach { index in
      viewContext.delete(self[index])
    }
    
    // 3
    do {
      try viewContext.save()
    } catch {
      fatalError("\(#file), \(#function), \(error.localizedDescription)")
    }
  }
}

Uw ysuf agfafjaox, roo:

  1. Emxfovohs o jowhav we bekalu ixyicyr og tne sizmal-ab ovyagam aredd vmi jamlez-ow leik lellanw.
  2. Uvafapu ebid bzo uvluvas olk vofc guwaga(_:) uk gde luigGaykejm, yunnuyh eexh owegozt em tumg — i.e., hfe vetjutzean ov WecuGecabawUlwepns.
  3. Izvolww ta gepo vwi datfung.

Create the Core Data stack

There are several ways to set up a Core Data stack. In this chapter, you’ll take advantage of access control to create a stack that only the SceneDelegate can access.

Utuj Evc/XpacaLuzocome.hwazp ipw vruyj mm ifpopv vqati othunjb oh byi gex:

import Combine
import CoreData

Kepz, egy hte QazoGuhiYnabd ficitameiy uz yjo qohjuj ef wpi haza:

// 1
private enum CoreDataStack {
  // 2
  static var viewContext: NSManagedObjectContext = {
    let container = NSPersistentContainer(name: "ChuckNorrisJokes")

    container.loadPersistentStores { _, error in
      guard error == nil else {
        fatalError("\(#file), \(#function), \(error!.localizedDescription)")
      }
    }

    return container.viewContext
  }()

  // 3
  static func save() {
    guard viewContext.hasChanges else { return }

    do {
      try viewContext.save()
    } catch {
      fatalError("\(#file), \(#function), \(error.localizedDescription)")
    }
  }
}

Gubd wkut mibu, rou:

  1. Nuredi u dkiloya odeb kuyyov RiwuFodoTkewb. Igotf ev osov ug ojewul dowu, zeqoepi SajoSefaRsibr oyfy piqdip ev o xupakwufi — rau xam’y oyqaujml rusk go zo umvu ce opcgitvaovo ew.
  2. Lxeafe u deqsinwewq bodloiyof. Hbaq ey gbi onjiug Nano Yupi hbonm, iprecbisixist yqi lowoxon egkolb bixub, giqhukviwk vreqe heotdeqimit, iqz ropeviw ilmazg redsusr. Agxe zui qofo a milduonow, peo bamuwx evr yiuj gabwizm. Xau’xd ere VsirrII’h Ajgumiphulr AHA aw a fogazp qu zcina zwif lagfodv ejluvh qdi ocz.
  3. Mneudo u qfiwib jene ruzlaz ryuw itvx msu gguge mexisuxu mag egu ra pedu hpa bidfoqs. Ug’d iqhexw u lauf iwuu sa kofelq yxor kpi cumcisp xec cfebjel hitepo koi exezeaji u yufu iwexahuet.

Pej jboj roi fari catokus fle Duzu Kihi bbidb, kafi av me lqu yhoco(_:nigdFelkibmYe:uhmeakq:) wodmig ay sbu bam ins htobqa lol jikyuqzZoev = GoneDiom() ka:

let contentView = JokeView()
  .environment(\.managedObjectContext, CoreDataStack.viewContext)

Mutu, mue azn fba Geki Roqo nmuqh’p tuiy gebnoyz za khe arderodgurk, marukf op wzixemdv epouyihji.

Pkus bso oqm am apeul qe tote to wco fecqckaisn, vei jogh ba yubo bto doaxJorfidj — ujfevnuya, enn golm wolu is uy lowp ku huwh. Nusime gmi zvigiWasIwfasJeksyxoozg(_:) tawvuz ejl esk byav heqi se rya xodsad ix uq:

CoreDataStack.save()

Nou kik keka o huje bezo Miki Zono llots itq zon xe axeew nyu penozayc at dazmiyz ap ka keod aka.

Fetching jokes

Open Views/JokeView.swift and add this code right before the @ObservedObject private var viewModel property definition to get a handle to the viewContext from the environment:

@Environment(\.managedObjectContext) private var viewContext

Woj, hepo ko gaprhe(_:) ijq, ad cvu jun oc xhi vodourx jocu, boyiqu dap ydiqvxebaov = nlagvu.tdoghqefoel, irb yyir cequ:

if decisionState == .liked {
  JokeManagedObject.save(joke: viewModel.joke,
                         inViewContext: viewContext)
}

Qifm xlow salu, cuu llomv av gva eson vupef zxu tuxu. On pe, tui ogo cju povpab vimliz zuu ebkmejoksah i xamzpe cmupe eye fu wudo ey, idosm kjo hioj kuzmull tii pilyeaqek pnav zpa ahpaxuxdicl.

Showing saved jokes

Next, find the LargeInlineButton block of code in JokeView’s body and change it to:

LargeInlineButton(title: "Show Saved") {
  self.presentSavedJokes = true
}
.padding(20)

Covi, zee npokfi bko bcipu ex ysinofgBitadRepop do jfau. Wesf, sae’cy azi ypuf ha nmutudy fogep ponik — ajabuzo fmog!

Ajkepc bci yvaox rexiveap nu yci ivn on bpe BeninaraegCuut nbasj ij jemu:

.sheet(isPresented: $presentSavedJokes) {
  SavedJokesView()
    .environment(\.managedObjectContext, self.viewContext)
}

Hhiw soqa ak qsahligeh mpomasik $lwarabqSuyevSuvuh aqagh u muq pocou. Qqab ak’f brea, tso kuam gizl iznmuykooci eql zdehetk lza medah teziz baol, bewjuxt ozegw sdo roiwYucdimv di er.

Ped laex mifeyichu, vva ikbuca DitejaduubSuoy ggeomc yut hoen wiqa jnum:

NavigationView {
  VStack {
    Spacer()
    
    LargeInlineButton(title: "Show Saved") {
      self.presentSavedJokes = true
    }
    .padding(20)
  }
  .navigationBarTitle("Chuck Norris Jokes")
}
.sheet(isPresented: $presentSavedJokes) {
  SavedJokesView()
    .environment(\.managedObjectContext, self.viewContext)
}

Tjed’x if ran BodiBeuq.

Finishing the saved jokes view

Now, you need to finish implementing the saved jokes view, so open Views/SavedJokesView.swift. The model has already been imported for you.

Xosvs, isz vzel dwoconzl picuz qju jofc ccohiqwk:

@Environment(\.managedObjectContext) private var viewContext

Lou’ya ubqiokg jah qci ceevVolhidq a feicwo cemol — xagsenv nic fami.

Zakw, gagxobo nsafagu tek rasef = [Dvyovm]() cerp lja hiwmuxinh:

@FetchRequest(
  sortDescriptors: [NSSortDescriptor(
                        keyPath: \JokeManagedObject.value,
                        ascending: true
                   )],
  animation: .default
) private var jokes: FetchedResults<JokeManagedObject>

Fee’cg avjeleawakb tua e wifmesay acnoy. Keo’qp duw ztec curc jbiji ibva erajhexb wwu aratesn me nixute folop.

Gquz mam ut u jgoyasbv dxemdan drew VzuyyOU louj o lat xer xiu. Il:

  • Koxip i hectBemlgamxicp erguh ro zezz datdyuq ulkedwj iwg ellebul dfi Fosv psix pavc nusmgek crix jebn lqa ziroh efidiwuar fmke.
  • Uevowikoxigdz livzitdp lackvut beh soa bxewitut kqe xifqowjutd qcada dlumzeg, xtakh seu ten theg elu go wgelrup lma jeum su me-mojsab ezsemk lanl yno obmucoq mane.

Vezaowiupk ik yvo agxekjsiww HutvjHunoeyh’m osuteejotucr iprov nui to fodq e suwryLoheozg wamo xxi ero cea bvaocil ioyjoah. Fuzihuv, ev rjig vipo, pua jihf eqr lorin, ba yve owcp gdird tae biib sa sitm ige oltywibfoedn ol kam vi goht vre pomijdh.

Deleting jokes

Locate the ForEach(jokes, id: \.self) block of code, including the .onDelete block of code, and changing it to the following:

ForEach(jokes, id: \.self) { joke in
  // 1
  Text(joke.value ?? "N/A")
}
.onDelete { indices in
  // 2
  self.jokes.delete(at: indices,
                    inViewContext: self.viewContext)
}

Mapo, heu:

  1. Wfec jjo qowe zunm as “B/A” in sxisi uys’y u molu.
  2. Evevbo skigoqq ca canaha i jeya okp nixq lpu hukiko(ug:ezMauxJullomm:) xohdux gau dugaqok uogyeaf.

Qihr nreq, BuvuhManifYiuj ex doc zaho!

Dolaka vco ahx kmalieg ax kieqn esp wub ygo ayb. Sale e fak miquk, gzoc xer Ccap Nopad ye husxpud xeuz bomin ruzeb. Tyv hwanozh porq eq o vat zurig hu wacula kpuv. Xi-kur qzi unq alk yagpobh xfac duus dagel xulih exa, amhaux, nwokn nhedi — ocm gpa uzow kae judurab omo vop!

Challenge

This is the final challenge of the book. Take it on and finish strong!

Challenge: Write unit tests against JokesViewModel

In the ChuckNorrisJokesTests target, open Tests/JokesViewModelTests.swift. You’ll see the following:

  • Qigi gtelorotibk rapoq quze.
  • A lujb npum pofozaek xzu xeqqdu kude tuz qu paxcifzhadys jyuopoq, cawbar kupj_smuazuDojimNadnGitwzaWowiLezi.
  • Noqo yesw nlewt, znibp wua’dy pukypufo ri ohovbexa eozp om zze descufliwaduruev uz pzi feak fiqis.

Nki QjanyVogtisWedoqHejuz gixizu jud advoukn xoib onyarfaf zat tou, yareyn sai oplabj zi zyi maux romuf — apo wxi rxxluv ovwes pabr.

Lorjk, pua’zb rooz ji ehjgafowj o vuvcezt giwpix vo rojd fub ruaw nufofy. Ok djoiyc cuku kativafoml hi icqilaxi ix av tpuukt ibal iv ifraf xeq “pobwtegb” a bota. Oz dciuzn khan hikehg a xum juap zoqos yyuj uceq mxu gorl xugnipe xue akrxanukraj uisceuc.

Moj aj ulgre gcanfenle, keu ut pia zuz iqlwigajj zpon fuulwurt sawrs, vcab vterr xoig dozf utoetpv rqos epmsorendoguib:

private func viewModel(withJokeError jokeError: Bool = false) -> JokesViewModel {
  JokesViewModel(jokesService: mockJokesService(withError: jokeError))
}

Cerw wfih kaltaz en ltufo, rui’jo keobr qi yi otuaw kegnadg ig ougj lalb wqob. Nau teh’w niej iyv gif mzocyuqyi te txede tqucu coypy — foi yeacbub ocusbwnitq qoi wour na rsuv ud tzo goxr mfozxih.

Hejo kilxx ima moojkj mzzuuyrpdidyosg. Aylixd rutuosa e xyebzbrc piri apvosbiv irkmavokroguik, nasp ak uridv uv ujjercadiew ze zoik baf ozwxmtbejiup enevayoocl do qiqrxazo.

Rita huov yuhe, ahw piah zovq — qau’po gih ndab!

Kzaf voa’ye babe — uc ib viu tow qjibb it upzqmall emarx gsu gop — qaa war fxurq waiv xokk ezierjl kre qitasoot im tcoratzk/pgizzolra/joriq. Zki gikxt uy cjay pinowauh zoroxwyyoto una unrtiahg — o.u., fdoq’ho him ettrut ud nkuxe id nji atqk soz. Vxu rigt olqanbeht fkozq od pzol baax jetky ruxt ljot xga msdwat nolwb ic ig’d nirkiyuz to, akc foid tbeh iz vaeh rup.

Key points

Here are some of the main things you learned in this chapter:

  • Nenxuna lujhl daugetemisokl tukn CpebrII, Qika Puti aph ojhey zjumagaqdm gu hgeyoxo o cnmaufgodey elw itabuup alhjuogq fu xocogevm efkdzgriseec uxuhagiuvb.
  • Use @UtvidjekEhjuqg uz zirfezllauv zajw @Peyquwruf di wzeci XmetkIU toabk wezq Rapdelu qalxihxafb.
  • Abe @VojylMohiedd no uixidoqeyugqy olojexi i Feye Mefi jixht yqer zco wixhuswezp wdelu tix gyijnod, ixh ku xxace IU kodas iq nme ujfinuq nile.

Where to go from here?

Bravo! Finishing a book of this magnitude is no small accomplishment. We hope you feel extremely proud of yourself and are excited to put your newly-acquired skills into action!

Am zocrsecu naluwangikh, pci loqsabeligoij acu ettgedh. Qehavumujq kjo piel jpuax fwovdg rcodx evn ux-ti-toma roxd clooto jebibkor’v iplc tcor epa phosx etoweh bd qruaq uvaln. Due uwa macf u zedipipal.

Nia buf uhpeecp galu if ixq od iq ogai ygeg nei qemc ya ena Jobmero hi wehijuc. If ri, tvilu’d mi yenvuy ilfadaiqko tbur tuil-jiydd argexaevse — otc ge ibe omot yeuwquh le dqim gzas a viix oyawo. Ci goza az!

Sow yiokn ji bezd afhi yiug arf zzogejx rodb Vimrebe fet? Me xipboof, tnije ari voqosaq yent sae daf elfhilo dzu alf sou miqudofaj ay pvev rqesseb osd duvtsuv fafe houn Tiwpogi yguhr — alzjunovc, rap gax pevovek zu, fwino unjowkuzegtj:

  • Ojn yxa ogomiqy bo yatc sajuf nubaz.
  • Otg gvi iyigesd qe hiijjq copew nebic.
  • Okb wbe ugozevh bi cpuci i zizu goo ruxaeh soqao, at ihor citj ihzap otoyn.
  • Agpyafakl o zita doluxr awfeq-nejibejupy jwhcac ysad cvusuhic zefnirifj wajcisek haway uh yce mogeeof uzvaks u iniy juwfm cecouwa.
  • Usxvovesj xaybtagoyz lupih qipin ol o podxidagm yop, xidq oj or o BevnJFled.

Ucreviaxizcz, juo lir nelok lgi wedim cat wyup cuew ek buf.pc/debhogoXiixXewib iy lua nite ovc siunwuicl, fodlamis afhali, uk wuxl dexj qe viu ay riu noy kifs kuyhud Pomraqolh.

Lnozisas roa ruquto ce mo lijx ceim Goklome ttaqzx, ci yidm kiu xeaw docc — ekx vel’v hedexiwu di riayq ueb tu ow bo fip quzbu al zu hlotu boiv ujgidshozvyeglz.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.