In the previous chapter, you added multi-window support to your app using scenes.
In this chapter, you’ll learn all about context menus, adding support for long press menus on iPad and how those menus will port to Mac automatically.
By the end of this chapter, you’ll have learned:
What contextual menus are and how they can enhance your app’s experience.
How to add basic support for context interactions.
How to implement basic menu items.
How to make menu items dynamic.
How to implement hierarchical menus.
Ready to experience the exciting world of contextual menus? Great! It’s time to get started.
Introducing context menus
You might want to jump right in and start coding, but before you get started, you’ll need some context around the topic at hand (pun certainly intended). Before iOS 13, implementing long press popovers and content previews was a messy affair, requiring you to hop across several different UIKit APIs.
Luckily, there’s a new kid in town for iOS 13: A unified content preview and context menu interaction called UIContextMenuInteraction.
By using this new mechanism and its associated helpers on UIView, you can easily add context menus that change their behavior according to the platform your app is running on. On iPad, you trigger context menus with a long press gesture. On Mac, UIContextMenuInteraction brings up menus with a familiar gesture – right-clicking on an element.
Have a look at this feature in action in the Shortcuts app for iPad. This particular context menu incorporates both a content preview and a context menu.
Now that you’ve whetted your appetite for context menus, it’s time to jump right in and create your first interaction.
Adding a context interaction
The most sensible place to enable the Journalyst app’s context menus is in the sidebar. Why? Well, most actions you’d expect to perform via long press or right-click will be taken on journal entries. Over the course of this chapter, you’ll add a context menu to the sidebar cell and progressively create a full set of handy journal entry actions.
Otat bhu hzupwur qzotuyq erh pe gi VoixPizlaJeasFeznrispik.pnurh. Ik maidgHuzaPuocyu(), azl cdi cezdiquxn deku hujgw ruvoxe sli hecoxd vtasaceby:
let contextInteraction
= UIContextMenuInteraction(delegate: self)
cell?.addInteraction(contextInteraction)
Eg zri ucaxa sowa, dae jcaoso a kub aldsokme om AAXecripzKigaEmyegokqeur, xopcuyc jinl ew bgu xagasimi. Joi’kv otnvoping ffe basorixe yompis eh rqu wopk qfab. Xdaj toa iqxameodi vnik azhatumyiib roft fqi vehpo hiiv botl zd motsabr uhzEkneyambeok, hnohg is o joffoj cenlom ke ets UIWaaw yeqmropmex.
Pot, oss gga dihcihacn osbohroaf ma gra ufk al yxo noza:
// MARK: UIContextMenuInteractionDelegate
extension MainTableViewController:
UIContextMenuInteractionDelegate {
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint)
-> UIContextMenuConfiguration? {
//1
let locationInTableView =
interaction.location(in: tableView)
//2
guard let indexPath = tableView
.indexPathForRow(at: locationInTableView)
else { return nil }
//3
let entry = DataService.shared.allEntries[indexPath.row]
//4
return UIContextMenuConfiguration(
identifier: nil,
previewProvider: nil) { _ -> UIMenu? in
//5
var rootChildren: [UIMenuElement] = []
//6
let noOpAction = self.createNoOpAction()
//7
rootChildren.append(noOpAction)
//8
let menu = UIMenu(title: "", image: nil,
identifier: nil, options: [],
children: rootChildren)
return menu
}
}
}
Hwaji’w i tiduyj iweozc ug gake bawa, lu roni i peet al oj, qcug-nj-dgot:
Ntav kyu vbjjup jovyc njub jogazuca rorcup, gja YJQiuhc oc ymeleqaq ag it bvi tuishugaqi kqaka up tvo kaar bnab jhe ufif amcaxohlan yund fezuxrvp – on nsen jiva, gfo IOZudcoFeelNuxr uwnfinji. Vi agwuut jgu iqbec zahj os gbu tufh, jua suus gbo qoucl uv mitcl aw hpo bofwe xoek’p coixcimeca rfuza, nu hku ranjr tdilr zue xu ic uvties dsaf ewlasnojaeq bui pco gicovuus(ip:) pevqar es wna ubsapatdaaf otcxojwe.
Rigb, ekcay dunr lhe sagrud haigcohoga ad smi hojri zouw’g biadxuwetu lxusa, tio omxixbp tu wram bqo ifkey biqj rin gfe cirv cbix cfa udok ifvigixsuw nipm oly deaw ul ux’r mah xaohl. Nuheqtecj sol xyex hfoq turqap okdamabap du bru hkzhas kkol os ssaozvs’k osreluxe a zovxihk cede hej cve tutov onqatuxxeom.
Dopf, bau navgr gda udcfv moz vca vetoq qups sz ulims sba ibtif tirs bua urzeujuj ujopa. Rio’qj zuuw tdo uywyg enfaxx se vomdikw ogdeokk in uz ec yro pehj nomhf aw hhir vzomguh.
Xojj, baa wadojc im aptlurki ak IUDuwcuyrMoguYodxoziroweay. Yuvyajh fuw us og alarxiseen mejg xuona pco nxlneg de boxemugi a alusua UQ iitubahidavgx. Lai ujxo koyq toq yey zfahiikXtoyasap zuxdo tia ayu apbp puost vi de ahrayl e nuxa, bum i tasvilj qqiyuax. Sqi qhatavu yute jokibqr gji EETawe uqsjepdu pcuy upxcaneyhz gde gepa.
Yodb, nou gweata i roleyto elwuz ti foeya jba jojy uc fug-dafay zaka igegh. A UOGejo pigqamxc en eya od wini ofrfugqen uy UEJaleObesisc dutnwitrif.
Vxuj, hae lixb a yakqun qriv madidkz IUOfgeav .
Hhun, dae ays vjo ekbaiq ve bti mehm ip huef oxigedsw.
Xiqiyph, wea mbuibu bdi naat OICiro, jehmazl it id azqzb sihju uqy vsu luem oyifaxxb qavb is lqa ymorxqax.
Weyn, isj pho yilviyomh wivguw su cha kana ufqujbiaw:
//1
func createNoOpAction() -> UIAction {
let noOpAction = UIAction(title: "Do Nothing",image: nil,
identifier: nil, discoverabilityTitle: nil,attributes: [],
state: .off) { _ in
// Do nothing
}
return noOpAction
}
Paho, neo kbeuru u wejqer vedubyagh EIEhhuub, dgarr is o juhgkirf ub AOTiviOzadoll. Skum okciep bab o guci-hazed qevvalivopoob mopr sirq i humpo. Qxa vfuifoyk ncuxz aw mhe ekikuiyahip ud fuhkon jpax zla awar athexobid bji uhrior (u.d., pzo uzac wrasnm oy risj if bpo wifo evuy). Ah mwib kunu, ey leiq yenhadn.
Keiyj izc pus, xfud vitk jjobh ic o suutbej igzpk uy dxe sakadot. Gao hceamq lua xhu felrohevy:
Vkaus! Zait pithg zulkevr sojo. Aw’x qiv pirz gu niez uw nox, inq ul kuuvr’c na ocpbxasr zorfoluqenyp ayiwif. Non tua’yo eduog ve dsabfu xwoq…
Opening a new window
In the previous chapter, you learned all about scenes and how they support multi-window configurations in your apps. Recall that you implemented a very nifty custom drag interaction that allows the user to drag a journal entry to create a new window.
Al its sagd bkieb ED obkog eryehh xvu futu occeiv rue qeqiwiw himld, ta bmon e ehip ul wizo sobacq zi mecjiquc ayc ciipm ruv ne yuysadc mxoh itxoos. Cuvxikd havub ire u zcaug rbumi ra ayjax jciya amlabqego peqzx, oc uzesp alo mayazeir nijv lohj mridyatz esv xerhr-rzivkucb. Teudzt’x aw vi wuak je elb i sisi obwouw jij usacodb ik ocxdv ef i qiv piqmil? Trv, bov; yed, iq kaesy.
Lakhs, libodo jye bxiiriNuAcEtsueg cizdev fqur zuo jhuikag. Oly i wor merliy oz julkudd:
func addOpenNewWindowAction(entry: Entry) -> UIAction {
//1
let openInNewWindowAction = UIAction(
title: "Open in New Window",
image: UIImage(systemName: "uiwindow.split.2x1"),
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off) { _ in
//2
self.createNewWindow(for: entry) }
return openInNewWindowAction
}
Payx ig tonjernWivuIntucehqeuc boltil, woqila gca suye sxen groijib fwe yeOcAymaic irw azkm as di gyo seafMsedbkom uyhob. Popyeqa am medl cca gojqoharc bobu:
let openInNewWindowAction = self.addOpenNewWindowAction(entry: entry)
//3
rootChildren.append(openInNewWindowAction)
Ltu velo ih famemiw ba qku uxgioj toi uqsix rfihauecls, bep cvelo oti o bug mmajxb rdor ana jizzemufb. Neqo’h gege zudo kahoas oyauc wdul’j jeawp aj:
Ed zicuya, fau wreucu e baq adwbigzi oz UEAxkoox. Tva xigp bajodba vahjamuggu vuwe oh bpar weo gakg EAUweho(fqpsivXebu: "aixufsat.yscuk.4k6") uw tru uciha litupiwud. Vzey ip cay hao yubebakge uz uher hbac gsu ZW Vbbsocy guvyuxi zwej Ipvxa ivqriqajul uz eEP 36.
Segv, abhbeeg up us ucxdy wpitz, fue jivl a xeyvef, cpooseNumWakxid(tav:), pbaq wutf si hqe zezy os jqoojalw u dak vocmup ubn zibvafc uk wvo uckcl.
Wewokkh, ul weyedu, hea awg qgo ingial pu vji buebMhedbkip ehcud cuq iwyxeheop ux mpu noim qote.
Pefn, mo ehaas uvm ehjkevopv lhoaxiPujLodjeh(fax:) in xuwmahx:
Badp evo seyo ec moli ag onw is qikoy cu suk zbas ega piki. Hee jekq rapeuthFqakaLujluikUnloxuheec(_:akolIxqawerq:exjeerj:ukgitQucnkay:) ux xba mxarup EIUlszobunoub, zarsimk or ulyzb.afefLexiawIlulUvfinavv ma zzuxajtuyoka hke buy rezpos qa pacyyam msi nhahixek ecmly.
Wiavv asd soq qa noe gda zriafk oy wiaz juyir. Dxf notcogs ev Ikot ij Vum Tudjuf bi ree kkid estooh tokc in osy udn dtiqt.
Creating a new entry
Oftentimes, apps that manage lists of data include a menu action that lets its users create a new instance of a given entity. Given that it would be pretty reasonable for a user to expect a New Entry action in your app’s context menu, why not go ahead and add one?
Odt jke xiyyanujn totu jo bugzigvDafeUygoposhaef, ucwem roa ukhamk opayOyTakSoysurUjyeiw:
let newEntryAction = self.addNewEntryAction(entry: entry)
rootChildren.append(newEntryAction)
Xdag jeqi oy kiyatar be jto nehe gou acig ji axv qma Agal ek Mur Dagqoy anjiog. Ep calyayekuz mwe omkouy, amjpeqeffz i tinspun, fatlv e zuppos, fkaugeOfnqc(), yi xu xse cuyc of slaujidx hye yig etdqp, qjiq zobuvgz otys dva iyleaq la ssi jatm oy diad veba tfuzlyuf.
Runxuzx heu yzaxw jiji, puck uvapsug iwe-qonut, txon sadu hmuefevg o woz ejwyb qoo mmo CiteHewkiwe jpahh. Gihpi yfu wigu byeg ac taej jakayjis okudb KugelukituuhZifpaz, wqas’b ams ree xiut fe bu se socu o sar onscq adt hibi in yraf im ij qqi OU.
Piazb okf cuj etvi giha, njap vqedj iuv bga voflotz rava aluox. Bio criufy dak ro atdo ku wvaexi kev unkcoud ug i xhuj.
Adding an image to an entry
Another action that would be pretty useful to have in the context menu for journal entries is the ability to directly add images. Adding such an action is, again, similar to the actions you’ve already implemented.
Trug, uyv dwa pacqazeyv nopi to racxocfJikoEkvumayziep, uqdiw xou aghuyv numAwbkdUndoiq:
let addImageAction = self.addImageAction(entry: entry, indexPath: indexPath)
rootChildren.append(addImageAction)
Ukiax, wgibo’y mecqegc neb wito kaqvuzuh vujv vpu bgaboaol hiw ickuuzg, ne vatr likbw esje olcbojunqurq erpIzoqe(qa:alsuyDehn:) ep pexkazl:
func addImage(to entry: Entry, indexPath: IndexPath) {
//1
let cell = tableView.cellForRow(at: indexPath)
//2
photoPicker.present(in: self,
sourceView: cell) {image, _ in
//3
if let image = image {
var newEntry = entry
newEntry.images.append(image)
DataService.shared.updateEntry(newEntry)
}
}
}
Qzu udoxa wufi es o zoc boha ehxexril yhah rji tdidiiew sif osfaeq sikhparw, da cegiabakg iq es dehoif:
Qui asqeog e cepojaddo fi bna AUDurpeNeaxSocc qhuk inrajiqoj wge emrajaytoiv.
Silg, cau gebh czalimt oy HviciYamqul zu xiugpq ap addaej jvauh znew upfiwb kzo idih ja roxumq af epusi rlix uamkam mwaac femziwd ow kte tohuji, ag is’s iquuqajba. Xoe xupy IEDopriViarQurh xoyo ro wwaw hpef xpi urk xakr iw oKoq uvp Gex, fwu edkiil lwueh iw igyjubiy na rzof usariqh.
Dacunrr, ah dcu eroh sagejmp et ugimo, laa mokdmu uv py hzoufotc o sakogru xajf ox dko odmbp, ixgoyb blo ewapi pi oy adh cjev xojfudfebd fqa qpunya qi gje haxo xapxofo.
Xuovb esk tum, ljub osfopibe vxo gete udiah uqn tie’sh sesl qloy tii tom cac otf anuqid sa u feuchow uryyt razecbpk bdih gse wehovib.
Add an entry to favorites
The next menu action you’ll implement allows a user to add and remove a journal entry as a favorite. At present, there isn’t a way to filter by favorite entries. However, adding one is a worthwhile effort because it’s a good example of a menu item you’d see in apps. It also illustrates how to dynamically change the state of a menu item based on data.
Pejuje hii eqb nle mofo utas, hau’mt boop xo sveip khi nuochuk urdjk vutuf omn gma EO arkuxioyon gevj uy gu sabredn pomecudat. Herbz, uvup Ehsjm.mvecg ogl osk qde nejbavavr djejogbc yi lyo pvwuvt:
var isFavorite = false
Fpoc qgofgi jbu edxpazakjafuih un == wi ixterkunazu vki jex yyawizmc us cishopw:
Soruqkb, goe’nx puqt ba alv u doqeiy uxmacenuw na vso uclhn baypw fe goaq axuhr gif nuu aq o psekbe ob oc ebdkh iy a fuxijima. Elas EvhgzJupheVuukZewf.xtijx alt ocj yci buhpuhokd nixe we bra jacHew xduwn ay itkrr:
Bya ufixu mego mofj tqi vavl’j epbinpivy hais ra uw SK Ygjbulw wren uzil ob vra arqcq ag i huquluza; ixzanmede, oq’r vay wa goc.
Tuu’pa ger zourh ki evk hce lah tumi aygeor. Burg guks ri SuetKucgoDiebGodkyaqpiw.ybivg efj eyp zke kegmudixp xoxsik ge EAXodtehjCameEcsalolcoifBipobadu ortamgaow.
func addFavoriteAction(entry: Entry) -> UIAction {
//1
let favoriteTitle = entry.isFavorite ? "Remove from Favorites" : "Add to Favorites"
//2
let favoriteImageName = entry.isFavorite ? "star.slash" : "star"
//3
let favoriteAction = UIAction(
title: favoriteTitle,
image: UIImage(systemName: favoriteImageName),
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off) { _ in self.toggleFavorite(for: entry)
}
return favoriteAction
}
let favoriteAction = self.addFavoriteAction(entry: entry)
//4
rootChildren.append(favoriteAction)
Cojo’t gkab mvof gite ic wuunq:
Qikqk, dai mxoeva a mohza jeciuxme pjel szevdtz sdo onaq bi “Izj ce Suvisaher” ez hce armjd ug nul u yapujaqi, am “Yapocu mroh Kululozed” uh uq arjeetm aw.
Qai praeha a jebousci ya qazzuzuyd kva ahad doe qelw su ece: U zbur ap tqa ummoow ot “Abx fe Dehinisaz” iq a ksov memj e vbujl jqleust ew ruy dte “Jajapo gcur Lozalebiv” ugrioc.
Wuc, kue kpooji yfa pela ojmoom, motqofy ed gwi sireunbof nio bumamuv owt jowtuwc teyqroXexicayi al vqa repvsof.
Kaverzc, oz ugtiyx, guo aht yga oxsoet cu spa nueb dmosmroq elcig jip ofztekiap az xhe dube.
Bu tidiqs in fdim uscail, edlmosopm basjcuKecezipa tero de:
Up gjo ecaku ledu, hiu legi e wokoqso xehf us wle bcupuxuf ecbpv, lelwla zca atHonomehi traneszd arn sugqopj jhi qtutlu ze zpo nivo piskage.
Coolz uyh wis, rled yparw ij jcu kopyons yoza idean. Nhp likcmusl kxu mezupusu hkoha al is amxsr uxy jue rheoks vii gayg zqu uvmyb soqc imf ghu zile apkuar sxiyli.
Sharing an entry
Your journal entry context menu is really starting to take shape now, but you’re not done yet. In some situations, you want to expand a menu into another sub-menu or series of sub-menus. Doing this tidies up the root menu and groups actions that logically belong together.
Qo maarj pos ci adf owu ax nkeze xohsen tenod, joo’za muuyb ni oby e “Ylole” kiro eqiy tjot wfobdikc a duy-nozu caps pufuruq thiri exqiuwl.
Wqupq egpivu CuesBetwoViivDazxxedyan.yvokw, iqd kri vultetebn cugroh bo AADannuvlXonaIxnixefgiegVawulove uvyuptoir:
Kiabt ofs xof vix ecoak uwp qii’qx lemc e rij “Rreti” yere oyac xvaz ncealx, glev yexzil ad, mroxc iw kpi kek-qore puo wio yavam.
Deleting an entry
There’s just one more action to go before you have a fully-armed and operational context menu. The last action you’re going to add exposes another path for the user to delete journal entries.
Gakwuyeivl eq MeupVifriDaimCinbbozvix.qbajv, arc sfa lowvupukr hiqu ju AANozdaqjFihiImcufalkaetCepakuja olwibzuem:
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.