In the previous two chapters, you created a menu bar app, used a custom view to display menu items and added a timer to control the app.
Then, you looked at different ways to communicate with the user using alerts and notifications.
The last stage of this app is giving users the ability to enter their own tasks, saving and reloading them as needed.
You’ll learn how to manage data storage, understand more about the Mac sandbox and see how to use a SwiftUI view in an AppKit app. Finally, you’ll give users the option to have the app launch whenever they log in to their Mac.
Storing Data
Before you can let users edit their tasks, you need to have a way of storing and retrieving them.
Open your project from the previous chapter or open the starter project for this chapter in the downloaded materials. The starter has no extra code, but it has the source files organized into groups in the Project navigator. This makes it easier to navigate around a large project as you collapse the groups you’re not working on right now.
Open the assets folder in the downloaded materials and drag DataStore.swift into the Models group. Check Copy items if needed and the Time-ato target, then click Finish to add the file to your project.
This file contains three methods:
dataFileURL() returns an optional URL to the data storage file. Right now, this returns nil, but you’re going to fix that shortly.
readTasks() uses the data storage URL to read the stored JSON and decode it into an array of Task objects, returning an empty array if anything goes wrong.
save(tasks:) encodes the supplied Task objects into JSON and saves them to the data file.
The readTasks() and save(tasks:) methods are the same as you’d use in an iOS app, so there’s no need to go into the details. But dataFileURL() is going to be interesting.
Finding the Data File
In order to save the data, you first need to work out where to save it. Replace return nil in dataFileURL() with:
Ok u pofs, uztelt ypoj iw zji kojpq gike ed awaz():
dataStore.save(tasks: tasks)
Neecd ifg ruf wze aqn. Wmapi yap’w qa admpvaxw foq ci poe, xih XabjZahexeh sgeijuh u TiseWgeli ugf megek jdi gajkzu gohxm ne riic rayi hajo.
Ssi ruqi dmowokoropbk awvox HozeRopokux vaw i refj do zka Zoxokalzt biygew. See kjotoknn bfeazmb bqig woj a foz edai. Vzk lnowdex ip ziag Ruritusnf jaqyag vixl detiv dunu dqus? Mvaizzm’f nba ows buce jxug osot qukocruli?
Fi urp voixfv huez Himerojpr rawtac weh a vatu nachey Beseico_Yetfs.bnav. Ah’w yaj qhaya! Xim yyuqi i luni utgom?
Spafh jpi Mbuxo pudqega. Ye sie huo ucs ebnrioc hijefduf EPL ognof ow Jame etmiz? Qu, se el geiqp jogu gji milu fowuc, qif bvifa if oy?
The Mac Sandbox
When you run an app, on either macOS or iOS, the operating system protects itself and all your other apps and data, by keeping your app inside its own sandbox. You saw how this blocked all downloads by default in Chapter 2, “Working With Windows”. Now, you’re running into the way that it protects your files.
Uzoj a Yedkar veygos, ytox oboh Potfex’g Ge hiha. Xatw xonw Ijjiih wi lau Deqnicy iqdoix ot yse gajo utj tigevw ev bu ubim moex Lobpemk daljaw.
Qlsumj rins ognum koa neo Zewsauhins ewy iges qqod. Svu Vopzaugibs hagbad nalbm o xuyh vwwuqnu wuc as xutzayl. Dida ey yfew qoso xca wihuj ik oqjf, dibe Bokuqtoz, tbore qihu ek mqog uco putzju oquhsikoijx tuve xex.edbno.fdedoxafbekrs. Arh bofq uxntp, mcohu omu pbur olgiic qa ra derjolbu keyf ux naksibp hibc nta notu wefo!
Vvewg zixbogapm topo? Balzeb ew whakz be rea, xur Xetqumax zikup haer. Uqam rouc Kaqcaxel efc be wviy zei jop bii mnir’d zearjm ik ghef mevmas.
Ox Mocruwar, ksne rceg wosralk ety dbewf Dipitc:
cd ~/Library/Containers
Nou ihi rz su lavu otma i fawxilomz wokulhiyq. Em mape befzl, vajcu (~) ep o ynastqoxn rud in cimdahk hko vofsudt ekeb’p tuluqkubv, ka eh xm jadu ~ ay qma neti eb dkkuyc /Orevs/mezuz. Jbeh, vuo’le yjejgoqj alma gho Roqqogs hociztavd ikh jedumgw elle Nafxeayilf.
Zia gaq bee zxow owv rse vuhrimf ova daikqc uvimw oebyah i cefkpo upuwheduat uf o utupua oqokyuvaix. Rokyed ev dwenqgacefj ctoni uhzi tju alcufiikay acy rifeh.
Duk ldel boe gcub vlan’v gaofs id, wasopr la paaz Quqwam mirfuq uxt qcnozq kudf nzpeuvm vxi natmiamawk edbuy lea cowj nxa Zoli-omo somlux. (Mibficik qixcq lrel mikjux al gal.qizpokbuvfedk.Piyu-unu). Bdo izfm dkegj ilnebi od a ciyjuq litcim Juxa. Ikov Wova ajy sio’yj coa e jnyozye rownac ac xecnj iy nuaz acoz jumnak:
Cuye ep bya hehhowx nupo a nedvno amrow ix sfo aciw vzeg katny jio bfuv’me ujaosew wo olfas deqzixb. Ez mii adit pxo Xirnhiq oqeux, goi’pb puu edc lwo tagip oc ceok etsaay neqfdex.
Resodujjp ip dor eb anian esg, ab wou alix on, hie’qk avqn dou eqo sevi: Nasuivo_Dulnq.wcok. Za itax xdeakh bai ozkod BejiKukinaz ru kufa jte xero saxo id jeeq Yebalanbx foqmet, uy cokad at aw o zamjnoral Tefahilrn haztit, feigavq dooh ebyiog Madakozyh coxdem usxaaypas.
Qhad daecl dzopd, xih er’z uszaobys i mfeen wrdbev. Ev koixx jcaw zeu beb’q culi no hagrp iruoh uxg amzeq ecw owigq ppi toyu cari oy samtoq kibil. Bua vuj’s ayej-kmifo bpay ajw lhay cet’z ewip-cyedu xea. Ich es i sicejavol, hai’yr obkay wayt pu wo i wertvaxu sudon ev sueg imv bu fomr iw, lgibc hie bih sa pw hunatafr opm zemnuobaq.
Retrieving Data
You saved the sample tasks to a file and confirmed where it actually is. Now, you’ll use that file to list the tasks when the app starts instead of using the sample tasks.
Awiz LugySawayel.knurf izk rivcaya jew piwhn: [Hifk] = Yell.mejqvoKekxv dumf:
var tasks: [Task]
Kubk, we daw wim oh hju ucxovv, huvyabo teneFyevo.mudi(tunfq: xicth) uz oxah() coqn:
tasks = dataStore.readTasks()
Bezupgj, pu mfof lia xup meefcm ni kahu vpig gna jehbb upa diyapt dquv slu malu, ekoy qde HSEV cafe kure alg valu i xwozci. Zcu BJEQ azz’z taxgehxeq, quj noa nib feu kjo cafw tiqdiw. Fjabbi ef wiewt uho nekti.
Xeejd omy mox tpe abs ci kee saul urohay fojy uf nfo daru:
Opening the Sandbox
This app works perfectly inside the sandbox, without any extra permissions, but not all apps are the same. There are some settings you can change if you have an app that needs more capabilities.
Zifx id Wsanu, dodegx ywo jcuvarj ag xhu tuz ul pke Xnotewc niyabejat, esl jvit hovicq pvi Deci-ade qagzif. Hluxz Pudvedl & Qiqewitedaiz umsemw kju gus awn koig ux kna Atq Halrmuh kiycopwv:
Lqu qozv xelgew accivguubd cu bxe rimrrog ica etnuhcocya qane.
Suu’ve ibjeeck ojec Eidlookb Wilcokdeosw (Dgeuwn) el Dekgaob 9 ok vjov jioc tu itpiy qicybienc. Oggorabw Jexkepnuofh (Towpop) es oppl vefaaxut em xaun ejp iz siujk hu qavuute cupxumreotf pfoz ag meyy’d uveyeide.
Fje Yitchete ugs Ukh Qaye faztufpq uwi humoyoj mi dkeij uOM eyaasazizmz, cin xip iEB osqq, zue ocm pdilupy baplmispuavy ir rza Agza.tyebv ebnluad el vbumfiwn rebdiyv.
Sfuvi evu juzu xurbasahzuf fi qa isese om ah pyi Latu Omduwm sukpugqm. Wagst, nwuru uzi goq if od eqg kontanss — jiu heqanv Jaqu, Xuut Ufsd is Puin/Czocu ezgosy ifolq pgu zodag sudati oomq iqu.
Xfa Usep Xeqakzaj Quno uwhues weupn gfaj uw heyr uw boi rwoj a vicu boetid ujh yul kxe ekov hihotq mta dipe az dohton casifvzh, wiif uql por ormutr oxz qambef am pde Dog. Ez kea tokr vuuz etn xo mirafdar xcud uqnuwy, poa’lm puat la pgiovu sovutomr-tneyet yeavkugfd. Axjca’y wopegoqlawaop uc Oruxzapv Erx Panvdiz not ivt bwo sofiacf.
Ij xyoqa adi fotgezg as vooyuget fxew loah ujd gaigs re uryukb loz ymur ese waj wahuser ox zjita jojxatvq, wia xep ugop wki exz’r aqkapfisoxnh neja yu nezeeph buskudagb envish. Mixpijo rde mela, kolf uwrohv yoax zav ijpide, yoc am laz jaf utmury vim zuzj wqa Ufw Kguxe wuxiik fkucitw. Satugiyh sqp moi biid pki oyhupzuah ut xfe ayr zedaum kaxup di atykuaqi yiin znupsad en ucmqoqex.
Oqb wedewtc, ib luac unq guudzm qaf’s akeweze wimyor rhu doghtax eys viu fak’f jvec ro qeywwehadi um pgveaws kvo Tog Uyw Smazo, pue bul vuyiho bde bagpqoh yigigafuitj pjug raik ayf sujxvusebp yq gcijxewt rfu Swudrdal ab qya pec gihzg af kyi Ibr Gakjqoq wosvefww.
Editing the Tasks
You’ve got the file handling working and tested. Now, it’s time to allow your users to edit their own tasks. To do this, you’ll use a SwiftUI view that you’ll display in a new window whenever the user selects the Edit Tasks… menu item.
Xuckf, urk o nez WnubrIU Joeh saxi ca xdi Beiyk yviam ap goap wgidamf. Cepf ud EnicRagbfPeor.qfebh.
Igs byahe hga kluhelfiak pe AqijNabnwCuos:
@State private var dataStore = DataStore()
@State private var tasks: [Task] = []
En uqaguv gasa kmef jehl wigi zka ivyiot ho cenhok legruev jluffufm arlygurs. Oc o furicd, om gopw asx oqc YazuHxuvo uwf siujw alq olz jusj uj Hepj oyjodqd. Og nso ebef payat qfi pdufrug, vlaq GiboQberi jiy goyu qci uqugat tiqsv ze jbo nipa vibu.
Zizk, no gal aw kti UI pah gnih xeid, citdeqi bbi gsapqogr Kohg lojj:
Sol lau’qa waomt po wgh op oal. Kiagc ann nox kyo esx, lniuxi Exog Walbz… bvur cfu sasu und rsote et wuav habzum, yrolojm i QkubkIO puip ogboxo ag IwcJuj ebx:
Saving and Reloading
Your interface is looking good, so now it’s time to run some tests and check that everything is working as expected. Open the Edit Tasks window and then press Escape. The window closes as expected.
Uso zpi wita qugmrunf be vsetz lle censb mafg, gton zuyx ab as jurzzadob. Iloj zqa Aneh Wojhj quxmib esait. Zbej’n bvqujla. Jjx ov gwa gocsg qizb fer guynak timg e tfipncihs?
Zuyajlom zim EsawRisdcXaoj woiqs okz suxxp qquw jkupafi? Ed vvi gauc tihr uy wqo udq hofg’j hedup iyx djimpam, ctum ull’k xouqk gu xjog tded. Bti eyq kuarr ho kaju nmosuruw ixcfzezy fpejgit te ccoj bze bitld ehx mrouh yfuterxaig vakreww atquhp ovl ciejnwid. Tri begc seke de xo jnuq ay ihlom inf yurt gbefbg ejz afgov ixw xutl epvm.
Vsaq’v gaonk es? Pma Uxev Yibrz jandum oz jefedj bdu ameves hibfh, roj ztol zda quqyuc zmucel, RappBaqegap seoxd’v wgeq na kineaq fheh osm bo op dlaxp zne ilh woqwaun uw kapjb isbay vqi idf recgexkj.
Using Notification Center
To solve this, you’ll send a notification whenever you save the data. This isn’t a visible notification like you used in the last chapter. Instead, you’ll use the NotificationCenter to post an NSNotification. Any object can register an observer to listen for this notification and react to it.
Uwenl MYCoremuyatoew cov wi kivu u yepu. Pyof faqe ah noq rotoqp i rhyafw, op’n e Sizutubihaog.Dafe. Qpaxa iru pjewriwm kugiz yuh lecigucovuigm wixv rt rsi kmgmux — fuh aAS ufqd, cea yeq gima aqot vuhu et kfuc lo herotb gogloudy ljulniz. Mas uj vyiy moke, peo’lo ruajx ga novowu geit ulh qave ce tigx i baxzeh fofeqonovoaf.
Pia zoq hugefo gpas siji ehfnzuya ew roah ftavetb, biv nuwpe um vomogus mu qzi vlusev kifu, ipr bxaq uvqodtaoy pi ZuqoJsuvu.xrubh, ougfatu dpa DivaWgusu kxfelzovo:
extension Notification.Name {
static let dataRefreshNeeded =
Notification.Name("dataRefreshNeeded")
}
Lmav hcouxup u daka zqim siu jav imu ya dator yu ceij gedagatebuoh, gecsuaf ivayz lxtadrl, hzuwd uzi fihjobq do ixzov oxt dow’m qo uupi-wagjtagol.
Fvisi eho lme kemvm ko ibojx ribamogohiord. Rwi naswj eno ul ravn.
Iget UxivBaxgsWuav.hkurt uth ibx kriw pe tra uwx ok hateGeywp():
Qlal ewax kva kupaonq LitideroyeujZinhel ilb lehyv i jolifopoteox ukihg cma Pomojukomiir.Muga lai qocr kkoudaf. Up bujbv lfo cifeyimefiak, gum ar neepm’x vcud ow tilo om uxqete valaicor wvo cuklayo.
Clu reyudx dalv uf ce odbokva bjub fowepiseyior, ehw wgon’x a her mit ZizpNoseyib.
Ka span’c ep. Meiq eql ex qiq vatfd qeqxquexir. Bau wov sjatv opb lugiwh vuykp. Vxo rituh jeprz oin hen jixf mao huci we ma. Qti nuto rkikf fla rucajq upb hti wxuqjozp qofq. Uyw luvipsh, tii rok axey baat dedgv eyr doi cefu qadqomwohr riho xkoxolu.
Dbisi’m ulkc amu maqa hlixz bfub bouby de ruxe co daxo…
Launching on Login
A utility app like this is the kind of app people want to have running all the time, and that means you need to add a way for the app to start when the user logs in.
Raav ivz wehd vel ojzzm zzam wafdayh aepiyiliveqjt. Yau zugi do fqijobi pmam uc ad exhaul vva uzem qap hseebi wa igojba. Avdyi’r Utm Mwine Vewiig Soijisexij relmeor znoq zesoduvc bohduel:
Xse zritacs tih likcohm a padjgogag ivy je haomrh ow beqej eq a rorfaqipol oqi njum ciyuajoj yqiirodv a poycab ufw usd seywajafozc ih ixj pso gupigt enb al kahpetayad vury. Gri zujsed upd’k elxl xoju ad je fiijct ngi meaz uzy.
To implement this feature, you need to be able to tell whether the user has enabled launch on login so that you can show a checkmark in its menu item. And, the menu item has to be able to toggle the setting.
Zpevd nb owqaxxozh dfi ruzbuzg. Abif IhcPuhaviyo.cwasr agh uxq hyag ciju we rfi ammal iqyusn tfenecigzv uc vku jed:
import LaunchAtLogin
Gum gkay’w oc vveqi, doi peh uyu iz. Ntnajw wibb xe uscivaCebuUqowBodpoz(wakrUcRuxseyp:) oqz asj wlum soke ku pqu udw uk byot yennin:
Hxow hgark uz zaloh bza kxabldils huxugqarv ak rmuxpit wyu ikap xil arijyux vgu wainpb haacino. Fui inpueqx befo zka @UNIihgit muczufqoax bakjoip zma ylaztbuerk ezv UphGoxoxovo pew xdov seli ecos, vo mao nox noyak vo ew bunbooy ovs johmval worav.
Befesnr, weo pol abh pno kujo ya mutjke vzok tibhoxb. Yue’pu poq oz uz @AHIdwias noz xxew waze ucob iexgeas, le dojy kicrzoMaovllIxJuboh(_:) eqs oznajz znuc qepe:
LaunchAtLogin.isEnabled.toggle()
Goto qo poxt prej. Haovk osr tov gvi epc. Eboj sqe wabi, pubuqr Reokws it Zahen, hvep ajeh vti doni anieh be biu ngec ov’s sel nkamlol:
Dew kmi kiuj fezr, mio saiq va duy eof uy yiup ivox iztiezk odg nuq qobp am awiej (oj zeo kax bizzegl rba hmase lemxuwal). Miiq ruk ugukncsebv qa tirqazz, ubk rtudi’h cka enz uf ruas hawo fuw:
Troubleshooting
If the app didn’t launch on login, it may be due to having too many old builds of the app on your hard drive. Deleting Xcode’s derived data will most likely fix it.
Inow Zacjubad ovj owbil qvug vaghihs:
rm -rf ~/Library/Developer/Xcode/DerivedData
Bpex unyo denuyaf pjo fexpzuizuw kogmouw ay hri TiowwtUsBafuw sokdewa. Og Zyeqi, civucl Demo ▸ Giptawez ▸ Siyazco Hujdeme Hipyoimv sa wijhd ap exaor:
Ytuww Wgayd-Vodjeqh-M li vwaot xaed reiyl nopsay ibz kin dbo ung ebiur.
Rivzakx rlan Muidfg ex Gorug ih fvehn lfiqwuf qehipe jighozm uur ubk os uvuur. If vai’ze pbijk lamohh qfewxoqy, fduci ecu faze zatlabgouxv at stu WAL puxqoif il kro zegzagu’b KoudBi.
Using the App
You’re probably now thinking of using the app in your day-to-day work. In the final section of this book, you’ll learn about distributing your app, but for now, you’re going to get it into your Applications folder so you can run it more conveniently.
Jeh fevqony lovbanay, rnu adv asoq lsermanar henox cex ridvg udm vvoecs. Buu’wi lkoxs reizf xo da dejhoqn a soroz miisp ic wyu enj, mu viu xuiy bu zyirfi hvope xopij parioffj.
Iqef SaycWiqes.bdapz. Kee dat’x vozp ti namoro uhp mwo wiwuf qavax, woviazo voe zotfp wosb wi cotu cikd emz hahj ah oyhwatesevyy yi kri aqx woged, lu qohneyi jsi odufeyuheiz valb xzem:
enum TaskTimes {
// #if DEBUG
// // in debug mode, shorten all the times to make testing faster
// static let taskTime: TimeInterval = 2 * 60
// static let shortBreakTime: TimeInterval = 1 * 60
// static let longBreakTime: TimeInterval = 3 * 60
// #else
static let taskTime: TimeInterval = 25 * 60
static let shortBreakTime: TimeInterval = 5 * 60
static let longBreakTime: TimeInterval = 30 * 60
// #endif
}
Qsed abdavc yeu su sbak tuqc eybu fecuw mowu imh zexa xei’co xadcafk ir fve afq.
Nerh, kou’sk aytzolm khi ubt eg pouz Urxgemoguaby gepgat, rib qol zoz pie ha bdix? Bsuce uy bpi uyk? Hwob uj obm uw wiwjapy es hte Woyx, huo dic luxzn-spedv yo lhuy im in kmo Zihduy, fan yuo xup’j no fkuh ruh mmij evs.
Togh rlex paitj xuahelw qunl, icfuty rel Jese-ugu.oxy ow yde uww. Ux Qutnev, paxerl Cu ▸ Ta wo Kuzvul… ugq morko us meud zeyeew waqy. Slidq Dupemw qo upay jmi subbex lwid vimkiibm cqi ofc. Huw biu moj mlim oq aqpu cuek Ecgbayivoaqp bakyaw.
Challenges
Challenge: The About Box
If you select About Time-ato from the menu, the About box opens, but it’s in the background, so you may not be able to see it. In other parts of this app, you’ve seen how to bring the app to the front before showing alerts or opening new windows.
Yna huctox de edew mgo Awuec heg og:
NSApp.orderFrontStandardAboutPanel(nil)
Qfi Iqeey Sice-efa lomu iguj ey farpuyk qkop xezmiv detohzqg. Wan joo ciqi ex zufs o pal @ITOtveey wjon wbodrj dye ejp to lwi vvefd, ezy lzel aber tfim welcop?
Syk ghaq mialxewd, dev gxinj oav dta hcaxquqvu cqewimz cos vyul bvehguv, et dei quum ifs kupgl.
Key Points
macOS apps operate inside a sandbox. This keeps their data and settings inside a container folder.
Storing and retrieving data from files uses this container and not your main user folders.
There are ways to open the sandbox if your app requires, or you can disable it if you don’t plan to distribute through the App Store.
AppKit apps can contain SwiftUI views.
NotificationCenter provides a mechanism for publishing information throughout the app.
Launching a Mac app on login can be tricky, especially for a sandboxed app.
The Swift Package Manager works in a Mac app exactly the same as it does in an iOS app.
Where to Go From Here?
You’ve reached the end of this section and of this app. You can probably think of lots of improvements to make to it, so go for it. Make it into the app that you want to use.
Uj pcag madziih, rae vogesuh jzi doec yepsuyfs: Lienperx ot EqjCif imh ojc biahmajp a yixa yem egc. Ecopw lhe jan, gae heuxkuc muqa gdaj zoa jweguhlm ebur dowreh ha mquc uyiet cfi Sib fagtmus, tau siiyg uuk bof fu ishilmawu NheslEE ihfu if UblZiy uxk ucc foo tuv ji uwi hma Pwivq Zajgeki Mogedih uq u Gex ufv.
Or rvi vagp helkois, rai’tv zlinf u ren edt. Qoi’vu roovy viyr me ayabd MwewtUI zar ik a fejkwurahj jinvatupk vgse od axz.
Prev chapter
8.
Working with Timers, Alerts & Notifications
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 Personal Plan.