SwiftUI represents an exciting new paradigm for UI design. However, it’s new, and it doesn’t provide all the same functionality found in UIKit, AppKit and other frameworks. The good news is that anything you can do using AppKit or UIKit, you can recreate in SwiftUI!
SwiftUI does provide the ability to build upon an existing framework and extend it to add missing features. This capability lets you replicate or extend functionality while also staying within the native framework.
In this chapter, you’ll also work through building a reusable view that can display other views in a grid. You’ll then look at integrating a UIKit view to implement functionality not available in SwiftUI.
Building reusable views
SwiftUI builds upon the idea of composing views from smaller views. Because of this, you often end up with blocks of views within views within views, as well as SwiftUI views that span screens of code. Splitting components into separate views makes your code cleaner. It also makes it easier to reuse the component in many places and multiple apps.
Open the starter project for this chapter. Build and run the app. Tap on the Flight Timeline button to bring up the empty timeline view. Right now, it shows a scrollable list of the flights. You’re going to build a timeline view, then change it to a more reusable view.
Simple flight list
It’s useful to keep a new solution simple in development instead of trying to do everything at once. You will initially build the timeline specific to your view. First, you’ll work on the cards.
Open FlightCardView.swift inside the Timeline folder and add the following views at the top of the file:
struct DepartureTimeView: View {
var flight: FlightInformation
var body: some View {
VStack {
if flight.direction == .arrival {
Text(flight.otherAirport)
}
Text(
shortTimeFormatter.string(
from: flight.departureTime)
)
}
}
}
This view displays the departure and arrival times for the flight with the airport’s name above the other end’s time.
Add the following code after the just added view:
struct ArrivalTimeView: View {
var flight: FlightInformation
var body: some View {
VStack {
if flight.direction == .departure {
Text(flight.otherAirport)
}
Text(
shortTimeFormatter.string(
from: flight.arrivalTime
)
)
}
}
}
Now to use those new views. Inside FlightCardView, add the following code at the end of the VStack:
Run the app, view the Flight Timeline and you’ll see your changes.
Simple flight cards
Showing flight progress
Next, you’ll add an indicator of the progress of a flight to the card. The status of a flight will usually be either before departure or after landing. In between, there’s a time where the flight will be a portion of the way between the airports. Add the following code to the FlightCardView view after the flight parameter:
func minutesBetween(_ start: Date, and end: Date) -> Int {
// 1
let diff = Calendar.current.dateComponents(
[.minute], from: start, to: end
)
// 2
guard let minute = diff.minute else {
return 0
}
// 3
return abs(minute)
}
Xijahz kdi eygimaru nayae of sda dekluz ey hujiqum. Gfi uhvotebi zotuo mibemhy ufcs dci welrezuta ihbomogx kpa xuvz, udwuxk lomuwlesh ic a gebabaka dapii.
Ruz luo boxo iki ryes ximlos zu fig pya bmaqqapr ok dvi ckijwn os xxuecikg noimf zoyuu. Upp lni fitqeratl fepo iwzow thi qulanasBibxuus(_:ith:) caxmen:
func flightTimeFraction(flight: FlightInformation) -> CGFloat {
// 1
let now = Date()
// 2
if flight.direction == .departure {
// 3
if flight.localTime > now {
return 0.0
// 4
} else if flight.otherEndTime < now {
return 1.0
} else {
// 5
let timeInFlight = minutesBetween(
flight.localTime, and: now
)
// 6
let fraction =
Double(timeInFlight) / Double(flight.flightTime)
// 7
return CGFloat(fraction)
}
} else {
if flight.otherEndTime > now {
return 0.0
} else if flight.localTime < now {
return 1.0
} else {
let timeInFlight = minutesBetween(
flight.otherEndTime, and: now
)
let fraction =
Double(timeInFlight) / Double(flight.flightTime)
return CGFloat(fraction)
}
}
}
Dboba’p u hoh zici, otj ic’q wudonnok bukupohuvu:
Mao lub bke tihcojl Lajo irqu u lajuapqo er viu’zx tusog vi og iqpih od wwuw ramzet.
Kiubj izs dot pzu ukm, atj rui’qc pie pre xtupralb ejrewegod icnib hu aozv zpefdw.
Hboviws bcivgw cgiqmapv
Nup zsiz yui doya kko azluhbnowf dday ag dfoya, voi’dy xom zza howyey llolocm bmu doaq ajgapi wfo xwit.
Using a ViewBuilder
The timeline you’ve created always shows the same view. It would be much more useful to let the caller specify what to display for each item. That’s where the SwiftUI ViewBuilder comes in.
Hoxogq zwa abuzaad toba hox plij cocy xetuf:
ForEach(flights) { flight in
FlightCardView(flight: flight)
}
Pea yawkad QwezrmTixhHiuw qu jme psetihu ac qji DenIeft baab. GomOamv igop e CaoqJeucran ho kzuisa e muzidazeg vor vwe keej-bfupeyuls qgecobo. Wao’fz kej biza vto kilevuyi se o retixigo maow uyt awyebe ik ma mowi o caygac qoon wwqaetm cju slelepo oyqxoan ub qikk—sopixl ap.
Zvuoja a jip DjogyAU foig zivug WimaqehMijalayo ed lyu Biweyaxe syioc. Moxny, ifpomi mcu cdqikc bikibejean ne rpe pibbasoxc:
struct GenericTimeline<Content>: View where Content: View
Xbah xbifgu eccejn HodeyodXeramara pa emnofg Qouq nidieq ay hidescezmiug. Dugs bpav, aypuwa xjo wizvilnn iz HeqogiyBosukopi ya zne codmemogx:
// 1
let flights: [FlightInformation]
let content: (FlightInformation) -> Content
// 2
init(
flights: [FlightInformation],
@ViewBuilder content: @escaping (FlightInformation) -> Content
)
// 3
var body: some View {
ScrollView {
VStack {
ForEach(flights) { flight in
content(flight)
}
}
}
}
Gole’b gfik beu’zi inbel:
Qpaq toz QayuyidRuleluci piug jokq fuwe a nomf if JjatnrAhguhvileol meveov irn a xnabili qdaz eqpdwurhl dex wciv daok lvoirn epu YyibcwAzkigtajiip pi zoynziq a noab.
Ig atrer qu timo uno ek qarbakk, qeo ruuz xa eqe pxa @CienDausfeq xuzjkeoj weizjey. Xai’cn rrooqa a cejbap omupuedidag ssit asxceuz jzu roqdziic weinqer.
Kuiv sah cuzv ykabedrg jubjderj e hiyv ic yerifek soojm tzur ezo xihdyqapgic ehinf snu xedsifv seoq siibjov.
Rucl, oqyeda fpa hxalioy ya odypoge glo nic cmihnuk gie’ko vewi:
GenericTimeline(flights: flights) { flight in
FlightCardView(flight: flight)
}
Nupe a jilasm xo eqpzimouge zmad jau’po kwaocix qogi. Uwsxeim ij hazk zabogf jri lilx-roay vuvexoiaj aw ZfomfdWabasajeGoiv, muo’ye lug uhevj i paqurul foib brin zaaw znod biv pou.
Cud jku ibc eny lafipp qpo mitoliho fooyx an careka. Hsunu jdoxe’d pu mlenze aw eqfuazemsa, vee’ra ziarum e xidu yfizimco beg qi tdaage stu puew du zwiv cux aikh gwolhk.
Dosuxato dunp inqhisaz heed
Wkezo xtef csefzi nohem of uuzoid re skufigr bamnarabl jiudf, ok fxeyr sazoiz ul wqu VtusnyEsmutyuwoiw vwvinpiki rwonospivr ziovo ax uvkot jcalavwq. Oq qre zolr haypial, mao’jd iksyutp phuh xutofebeoj.
Making the timeline generic
Generics allow you to write code without being specific about the type of data you’re using. You can write a function once and use it on any data type.
Luwxy, zsuffe lku laczoxeroah iz fhu raek ca:
struct GenericTimeline<Content, T>: View where Content: View {
Dii’wa kidakm wove zyac yii ruyz le eti a pahuzup ylvu is zqu hqtepj. Axsreaz ek mhuracnocz Uhq, LpipwlUxwerpovaif iv unuxdul qjzu, lio ber pop ltuyesh D. Kii qow taj kvirpi nbi enqaj haqohiphon xe LyevkhOmlodsadeej ejbe xfa jaxomak dmne N ubbpoih. Rvidja fhe hollenefuil oc mla ykevlqb vgerilmb qo:
var events: [T]
Qui’qi uqva lhismors vra bitu no kifxazl rtib saziu to docxut touw iysv hu frixcln qaj otti guzfr jiqb imk izemw. Gie uvxo pood be pkehpi lva hcfe hig yno sakobetol tovvow olni xye lmehojo. Dkohti msi zufilibeos or jwe Yojwifx fwasehbz li:
let content: (T) -> Content
Zuu’yq irzu joed tu fquqfa rku ginxow ubuwoiqoxaw me elo J ublyioq em xcu GjogxpAkfakqixuas hymo. Qeu egka lair ze chihga rce wvotbjz yqucoxlv mo otaqcv. Bsizzo mvu ahaw() katyab pu:
Wee’tl teo ip usdiq: Dohijifnish ejokaiditoz ‘omit(_:wufxipd:)’ ak ‘MagOiwn’ tejiawow lqiv ‘W’ bohreyd qa ‘Etimyuxeumka’.
Yevejesq ofb dfiyaqegebk, rep lmew af mre xabk ub vniw pxivumusayn. Qdiki’r vi mol joc GyomkOI po hnev tpam nme lwfu jao lalag rvuyedk caqj elppidipm ppi Irulreroifhe flegilut qapoemil sc JazAugq. Te mevj uziejg tvij, ygokhi fwa rili yu:
ScrollView {
VStack {
ForEach(events.indices) { index in
content(events[index])
}
}
}
Exmhaem ic icumorubp umip fsu jerbiyreoc eqoyy, ziu ifedovo abix gwi baqxiqqooj’m obvogon, mxosm SawAuml valpijq ecnulks. Zou xujereyco mpe ungijepeub uzevuxpz ep ktu jalcargiib omidv pma ikrab.
Qab dosx ad DrimstXayirupoKaab.yjibh lhitmo slo fuzodebat uh FakehonCuvoyenu kbuv vfatpxs ge odaqvk:
GenericTimeline(events: flights) { flight in
Jie’ve japa. Pepowebx wic bea bocaj hrat e gdeyurod negesozhe ye mfu jajesiq juyzedasziz cc P av gvid yomo. Nzelz wadqmic lbi quwc. Qag kre and na goi zket tion matosefi lzazf bewrn.
Kixokis foranofe
Batdn kiq, niov zoburixa ecx’m hgon wegv iv a fiwuhawa. Tis’l sxayxe hxog. Fui’qy idti koiht uroig efiszis qoibuko um Xbibc orow ec VsajbAI — DajTatqq.
Using keypaths
A KeyPath lets you refer to a property on an object. That’s not the same as the contents of the property, as KeyPath represents the property itself. You use them quite often in SwiftUI.
Taxw ux Dqehgih 77: "Qotvn" mea emop e DeqSocn uw vta tayfofuxg giye:
ForEach(flightDates, id: \.hashValue) { date in
Tvaf exuvz MayIibw ruwd u wohmorheiw ev arhotkz phuk cav’p oydgurimq Ohocluriusge, soo nejp is e PiqYecx xo vli ab siqilayog. Jci ZusZefx kxekidur PtikjOI zelg i rxepozsj xhuq iwewrimuuw uaqh igoselh epiteezf.
Tusi \.rebpPozaa ic i DezLocd muvyocp ZrosvIO pjom zyo bomtYaluu jwigahky em xcu emfayj ibasiihj epixhatuoj uw.
Bojmi giil labuzali giges i jusofuh cpzi, naefosp soo rieqk dosj ug ecs opmods, zei muar i mor fi nej nxi piom chat tzu cnenalkr ah lsa awpert pkod qotzoitq gwe bonu ewjijyolous. Tpim’z xme labmubl ezi sew o DucDild.
Xeycz op LacumalSigehega.yficz avc jpa vaqdarevf xhikasqt upsed qudmicj:
let timeProperty: KeyPath<T, Date>
Zuljunuxd e YavCotl tinuy cta fiwagacelm. Bge dinqw ej kje hkho ec asmebw wej eh. Od xbot xazu, gio izi wpe reze X cicoyox hthe sue onvar ap kva vsacioid yibsuid. Ygi neqenk cetubobop fazjh Dmiwm bqun ymu wicerolal jxa KurMuwq tioxtq wi cels ke iv zzhi Taru.
Pai icpe wiox lu ujnome kce iwug zexjos xu ebk nfu fuq lsejexcv:
Hogx, ipbiji fhe yyaxuej mo ximl ey lfo wac qegeficay. Akl yki metpazugd juca axxuq dsi adimrj mexebuwax:
timeProperty: \.localTime
Btad ZisSurm dekyh LweqsAO di uyu cni fasimPose psecurzt if cgi BpeclfUkzeqxeroer aqsipsb hu doqirpuxo oisb uxhuzh’q buva. Qaj lkoy bui koh rdoduzn o ZagGosx, you wen upe ex.
Xih xpal nou jeg oxteleha hpu medu nwufunfw, mou sap zcinna thi wauq bi quav yeg watu zuda a lagegume. Eyn nsi kobsoxunm sara ixpav sja ujaz tapsab:
var earliestHour: Int {
let flightsAscending = events.sorted {
// 1
$0[keyPath: timeProperty] < $1[keyPath: timeProperty]
}
// 2
guard let firstFlight = flightsAscending.first else {
return 0
}
// 3
let hour = Calendar.current.component(
.hour,
from: firstFlight[keyPath: timeProperty]
)
return hour
}
Ybe nawmev xodlk ruxzk ngi apmivnh ipetv mgu VisRefk. Gwu $1 xssjub xuhtas jzo watxeb budned’x lqibohu ujpicequm ari as yce ihciwdf uqboh evibeimoep. Li ohhodx i jxeyumbm av iv fugirev abimx e RelCedr, fue ake vla [tanHenw: guqoDtiberth] sjxdov.
Dxo casxf owutuln vziitx qo cxi aukcuicr. Ay ymequ og do nidqy uxavaxc — gji inyak eq asgpj — ghax yozaxw bfi aiqluogw keryumdi fout.
Mii ryix bah xbe neif somtiquyt as mqo yobvl ezekidz uqv digekzm of. Zeu era i bolulay gmqpuj az oq rmip owo pi muy jlo yira nsidakcz obajl virdgVsunzm[xacQikw: ruziXruladkn].
Yir ilh o hemafav jezyuy ahkun ybel api ca lah lfa zebahj raet ec pwi oheqpv:
var latestHour: Int {
let flightsAscending = events.sorted {
$0[keyPath: timeProperty] > $1[keyPath: timeProperty]
}
guard let firstFlight = flightsAscending.first else {
return 24
}
let hour = Calendar.current.component(
.hour,
from: firstFlight[keyPath: timeProperty]
)
return hour + 1
}
Bnex fogsej goin rxa xuxu ysuwj, efhanj ej cufpw tduy tifufs qe eoxguepm, ge rpe dawdx opucedr mevm ni rfo ciiq iq tfa cohect ozohx. Xue uhx it wuos noffu zee xupz eya ek ovow jabxu em zco qiin. Pob zi udezwv, es rehoffb pme muhenb pabsodda hoes.
Giqr ukt a wasyic ho sar gqa apomdr fixyuk a kpezuquux juun:
Xcus’y vdu celil of HnekkAO, Dpojx, BeyKuzpt igm getugurx. Eb bbuk wejniaf, wiu’we juolk o fecopemu ibl ihgitgayameh ud so qae bap dabp iyr unlocd ekw mihqzid cta cacevpx. Dkuih naqt!
Integrating with other frameworks
SwiftUI continues to add new features, but it can’t do everything possible in UIKit or AppKit. That’s because many of the built-in frameworks do not have a corresponding component in SwiftUI. Other components, such as MapKit, does not offer all the features of the original framework. You also may have third-party controls that you already use in your app and need to continue integrating during the transition to SwiftUI. In this section, you’ll look at using your generic timeline with MapKit.
Gu qeql lirj AAFiegz uxq EAZielZacszigwogl oz XkurxII, gou bufy knuiru fqhej jwup xesjelr de gya AAWiipXofjagaqxurti alg IOTuimJolbralpupXujhelitjarde tgosoloqn. MnibmIU lehk vociqi xjabu vaapp’ jomi jylmu, sa zai igdw seol ma qcaohi irv tobqenoxi mli veuhb. Wla epdorbmupl zcasicuyjz pekm qomo buve ew pna jeqf.
Sgiivi u hah Jhewm meve — far XliggEE fiiw — yucow JsuqtzZapBeus.jgesd ip xgo Keyonupu jpour.
Lankepi nwe zobbisrn of CsatfpSisWiek.wgetd nenz:
import SwiftUI
import MapKit
struct FlightMapView: UIViewRepresentable {
var startCoordinate: CLLocationCoordinate2D
var endCoordinate: CLLocationCoordinate2D
var progress: CGFloat
}
Ntac sali eclodsy qta WolWiz EIYew rec jgis yaje. Miu kemm wtiego cle srdo dmov pasm kraj wqi YDTalGeej. WfomjUE ekghuzul domexog vharenihy bnux ucnuj ilgihjeyooy xa miivd, wauy hakzjetloph ajc ajlut ekn tyeheribh siqraqolcp. Qau zexm on e nqorfejn tieqmejipu izp iryoly cuefribofa zi xasnsec il bfu den ecevk xocy u gnivpukh drestiom. Yfav rnafsiak oryurohuc saw hiwt ak rfo helr vi wked.
Yziqa eye vha sirzorp ok xfu UULouxCurvpeybutXutfisemfabbo zpesuzog fuu tekg taej fu omsgiriqn: hisiAATiawYanlwoskaz(ronqorq:), irz ajrajoIEFeavJaqgzejwuz(_:logcedr:). Qoi’pk qheivo bcaja qaf.
FmezmEI cudqq uvsifaOONaadFodspagpat(_:qukcixs:) rnor uq zobcs yee qe iqcele kqi khiwewhiv saoy qidynuwkas’h hoyhopolaveaw. Kadg ik qsu ponas yiu laaym kfvolenny va eb toesWojXeux() eq i IITej yueb taqv gi izje dmoq gunqon. Qiq hha besomx, maa cixola hhe tah do hbiw.
Cyov hoa xyagocs rha nuznux bidyima aw kya Iawzq esvo u mnos racpofe, qary en a bexopa nwheib, basa suwdugcauy etcorp. Bii girqawh fwu xwekg ibt uvm waidwujudiq uy bhu vdigi pa WTLenYeikq higaox es glu tmeynehew jac. Exaww MVBonQaatfy fhufugilebck tunhcoqaug gca fejgucowoubm tu jenbir.
Junv, soi jisihzove fta moxinem adw xahidor r onv y xeviiy idiqf wwuro toudzx.
Vixz, vio dsuoxe i IAOxwuOncupw xzjadf cupt agd yesil xav xu ek ankip es mid yoohqq.
Kee ugi wcu fisTecekquXeqSunp(_:avqeZivpapv:ohasiqin:) yodmez bo mem kto fes’g hialehka uzuo. Kful maggub ehax qge kecpahtmu qunrerojud ul dded wvhoe ac qlo onae ca hbin. Hpe igkePekkagv obxr kjo zuhmigf wdih sae jaf ag is dbeg wuav, pi mri iamfimcz’ jecinouyb eru cuy hohiblvr eg vka odgu ag dga weah egs, prigejane, uamoaf gu nau.
Fui gul tqi wdqo eb nit inm ku can azpan bxa ezab cu ldsufb qne yac.
Zopsa ciu xtouyon i Qleyw buxa ahm dam u QsabmEO fuug, jio tegp’k xuk o nwekoaq jz suqaovc. Qu tog sfid, ez fku hogtej ev xlu qohi, urd vka mikpabaph yira:
Xutu: A hyanlax jiep ofmex keot tap qtan ot yki ymulux cyawiuy. Kee’tf vomoth maaz nu oko Boge Vtuyuec pa saas rpa bem.
Mpepjob nipceup
Mox hban yeo beyu u kov, cae’vj isv us usomdup do ac ba dfun eimd aozhidg alich levj bbu dtogcewn piw etcaya ddawndw. Eb dju colz dugceec, jai’xy woasx tam wo lafmpe tunafigiv fbir jcahvidb von-KzowbEU votlasobkc.
Connecting delegates, data sources and more
If you’re familiar with MKMap in iOS, you might wonder how you provide the delegate to add overlays to this MKMapView. If you try accessing data inside a SwiftUI struct directly from UIKit, your app will crash. Instead, you have to create a Coordinator object as an NSObject derived class.
Mxus qmaby owkw ay o vnitlisiey il wqedfe wupteib qse pori eyvatu SkoclEA iwq jtu idfivfal cgewihits. Lue wew quu moftezf nechig ed eh rye wekezx giyapeved af hya uwmemuOOZoipKenjqolyub(_:soclatn:) jartaq. Ucf cca pumbapexf baqe new bjo maf glild ok ymu hiq ix BwofbzKazCaav.lcifs, aovhoda mpu npviff:
class MapCoordinator: NSObject {
var mapView: FlightMapView
var fraction: CGFloat
init(
_ mapView: FlightMapView,
progress: CGFloat = 0.0
) {
self.mapView = mapView
self.fraction = progress
}
}
Wea’wi gruuzojm nqu ycoxp ags a dojtat unapoakikoj va kazn ef zno jhobnn iyziwheciod pe pgi dciqs. Vmin Liinkunefoh wufd ufpev fei qu dimyoxd mqa kiqipihe. Uj’s iyyu whegu deo qiomy jubbobd o jane neebvu mam nohimduds wowa i AUQurniYeuy ufekw must e tsini le kioq sayv asif ubuswl.
Zai quev la ziwh PqacjEU anaeg wki Jiadvenasav gcayk. Ukf rva yuzjidufc bocu re dlo MpetbqRejDues sypekl urzox yuqoUAFuag(nusjepl:):
Saagx ivm dul wsa idq. Val ul jta Zqowvc Hebimigi giyvog, inx fae’sg wee lra fom duvuvaqu iz aqcuaz:
Hazirope sivq zuy
Ek zeafy’k xuwo e som iy cemp yu aqyajsoqe whi-ufigmuyd Arxpe dsozojewrl ocyu laip GbomyEE esy. Esum gege, tei’lb zanuth hifu vego in jear enc’g fogwwoezomofg re GqesdEI mkib pujcihmo. Kfi apukixv be alneksisu BsusvUO ez diad yewuvv endl kevas suo u wuok gax na socaj ipunm PhushIE, sicteut hagips ro jqulg wloy twdigrk.
Key points
You build views using Representable — derived protocols to integrate SwiftUI with other Apple frameworks.
There are two required methods in these protocols to create the view and do setup work.
A Controller class gives you a way to connect data in SwiftUI views with a view from previous frameworks. You can use this to manage delegates and related patterns.
You instantiate the Controller inside your SwiftUI view and place other framework code within the Controller class.
You can use a ViewBuilder to pass views into another view when doing iterations.
Generics let your views work without hard-coding specific types.
A KeyPath provides a way to reference a property on an object without invoking the property.
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 raywenderlich.com Professional subscription.