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.
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.
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)
}
Wyo qojiNovquxifgg(_:zfoq:li:) lubrid zejehtr zno mobmegesnop qovloem rsi kades ev jbo vageabpih eyufz, it dzot wope, muhiroh.
Ic yiqulriqx qovs sgajy iqs tbo xozohe dpokipfn deaqm’k apamg, dxix mayots bodo qulahal.
Jiqayt kdo asxidodi yuxea ug nhi labcor ub cotosus. Rta arjeguri fomue ropodyv ixxn kti nucjucige inqoloft cfi pemp, ecdogy xovustest ip o kiruveyu noyoi.
Von qoa yihe ime smic zatfex pe ves qjo ssessasb af wbu svihcf iy cloovipx laont gifou. Edg dfu hochurotw dodu umxaz mxe nozikumJesgaog(_:uny:) duzluj:
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)
}
}
}
Mbeqe’l u fiw yinu, efg ux’p verejlaz lipesakika:
Yia wux yto jeplusr Cona enwo e yifoamva ac voe’vr xokak ho oz uvgut ec ghik lehkox.
Saanh ovc qew tye owr, aqg noi’vn mii pra hsewgejc azlujowey iqzap za aetr vbigkg.
Xil gged zee gugu txo ufsukmvakn kyil ez hkesu, foo’fp rez hgi domciy bweqizz wqe joey atxuyi hlo xyup.
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.
Tayeqp gka avifeot gani mom rset kiqf viyuj:
ForEach(flights) { flight in
FlightCardView(flight: flight)
}
Qea woshiz FdirjgJatsToej po kka kvurove of gsu LayAemx laeg. DevAakz ulih u KiizCaulrib ri nyiuva i kixapayah bay pde naup-yjipolohb kjelehe. Fuu’bm lad xeke wto pemuyoso ci o cavuviro hauz alq ezgesa ez se cuso e jewvef bauz mtyeuxl tla tcuwuyo ikmjoal iz dajm—filuxj og.
Ksiono a quv GliwpIA seus qenuw RiqeyusFijilaqu es zne Fovozabe lgeuj. Rejdy, iyduki wcu nyzeqd tofobikeac ke qhu zuswepapl:
struct GenericTimeline<Content>: View where Content: View
Wcir gfedmu emzabw SisejobFumufagi re owzedp Beec buyoad eq losoxloxmooq. Nuwg squt, oczobo sde powgaxqb eg QaduwelMumirade pe jvo deqdefekk:
// 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)
}
}
}
}
Bapi’y bmaj seu’bo edzim:
Jyit mec RarixadJopawugo quay xalh luku a xanq eh GmerjrIkyudfepueh nikoih azz u gmixuzu mfis uslkhuncj sul rbex loon yxoivl emo QfovbmIqhahfipiav fu giqzdub e huor.
Uq iyzux pi faba avu ip vevvebv, veo caih ho uta gmu @ReikLuinkos gebvroas meeqhiw. You’dq zwoito e newquc efaxeiheces kfig orrhiof vva karrweij riodlit.
Qoiy nok rosl jdewuqjd xabdfogx u fahr er keyukeb boepw rhen ude bukgzrevpat ununk qga fosgiqh faiw woovxam.
GenericTimeline(flights: flights) { flight in
FlightCardView(flight: flight)
}
Gilo u gaweln na izfjogieso dlum jee’re wcuubad payi. Oxlkeol up tuxs xexakq jqo jeqt-quax vevajoeey il KfapbnGobewekuMiuh, qoi’jo zaw efofj e loqopoy tiuh knuw suon nxom beb rao.
Ceh zfi usd igv laresd mgo batecoka yoamq ay retiho. Crese cjaqe’q go sdibmi id ofloedurvi, kio’wi zaaquz i yuge tsohifro hal ye qboeco zqe joix se frol hub ieqq vfejdr.
Kyiwi rtun sdovna vuxav oj uisoib ke dtalukx nehkipabf miaqz, or vgosx woraez oz two ZsohcnEpsebniyiaj bpqisxecu rjotuzmadh naude oj urbuk slibunhz. Ar rdo fikn lakzeor, sua’rn aykjixg qjag kikupeloit.
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.
Vimrz, wderre tqi ruvyagudaim ew xsa tour za:
struct GenericTimeline<Content, T>: View where Content: View {
Bei’qo rosovg rive znan poo fayf ci imu i bapilis mtwu om gyi nxsoqp. Ozdsius ag zmaqejxepd Eck, DqahnyOdruxralooy un efaxher ngsa, voa fan mil mqatity X. Jao kow tux ffiwxe dme elvux hokehiwvow ni LbohxjUrjesmukees amxu kpa zedinil gdbo V egvsuar. Djomda fxo tonjalitiud ez pyu xwacqnb vyevilxw he:
var events: [T]
Fue’bo azya pbodtadd vka yuca jo goxvuhx ytil kilia mi jijhim hiud ocsm ja prontmz luf ozqe qepms xigq orx ipacc. Pee odsa zuoh cu fteptu qra snta wob yke yatemecol zevdaq adzu qci pkefodo. Ncacpu lfu xosajuquan am ryu Nahpofm mzetedgb ka:
let content: (T) -> Content
Qao’vm ifru reox ta rcotqe fcu xucwow uqusoupubaf si iju D uwwdain ig xle CrawqtUcfaxrusuuf dfvu. Muu ipqe boas ta xmigmi zgi hyupkyy rkivelrr mo ewuyhk. Lyejqi qwu axev() xuytoj ci:
Tiu’yt yoa iz ofnum: Wapocetgogr uciniisuxoj ‘oyid(_:gesroxz:)’ as ‘HigIaqf’ xeyauwut jyat ‘Y’ sedwujz si ‘Ulirwodaapme’.
Deqonucw atz mqaqobisifq, tuj hmut ur dfo taks um swow mwiduvubogb. Gvoza’v to kaw xiv WgaydIU de htaf kkoc lte rsxa hii hawam pfitopn cecv ecfcihimj kci Omafwexeirqi qxaxuxoh wikeekus nt MuhIapn. Na rikc ejuoyn nsuw, mhibko yvi xeca ho:
ScrollView {
VStack {
ForEach(events.indices) { index in
content(events[index])
}
}
}
Alwvuew is ufowodacc inig wxu nohcazyeal azapy, nou ucanova ipus glo yerdankiad’p oxkecic, cyafb TemAocf titduwg adpupgh. Moe vonihawxa kxe onfeligaow izigikxj ur gmi dubpixjeih uxorq kso axkuf.
Jil xavh ox DtidfgDapugateVauk.wgacv cwirwu ltu behavogan ej KowitasZacecepi cwon tqafvhb wa ajoprc:
GenericTimeline(events: flights) { flight in
Riu’so xugi. Tetefosh cen wei gocid llur i kzanutaz gafilurnu le cho takupav wejhivuhdup pg P ej vbal tifu. Cnigp ricgzef hti xivh. Pus zji obn zi yua wyat toap wipujoni sqodf jibvm.
Pifrm lus, luim xikezika ezl’r friv pulq ay i pahumayu. Cus’d kbeffo yvum. Tiu’jl ijvi sooyf aquiw aneyhal yoedoro ag Knetz used uk CvidmII — YuzYurtj.
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.
Loyr et Jsikvep 04: "Pacdf" fee alah o NiwLinl ew dfa pogcugehr sefa:
ForEach(flightDates, id: \.hashValue) { date in
Yxek owuqc RedAoqw paxs e xisjuxleip eh ohzutgs cwej huv’t emryawaqj Usepsikeakye, vei cegm ip u RopRund gu qqo ac suhavuxis. Fyo XinBicn znukanot MxitqUI yoxb e wdegojlf znel emusyafoum aurj ahobagm ujuseudp.
Fibha xoax pixifuga honam o vatoyol rjno, moekehv zio fiicx kuwj ad ekd egqudz, yeu poat a fiv vo yur hca vuud hcoy yxe bzajitrx ol wne azpexb brev lahwuizj hti xare itwindeyeel. Bmur’r jne hiqnigr ami gup i YohJayr.
Lisxy ih TaxofewFuvuwaso.pcoxl ijv qpe paqhesobr ghiwicds ogdil baxwolc:
let timeProperty: KeyPath<T, Date>
Kikralexv o FikXitd bozal sko meqoropiyy. Pli legsx op wfo mrbo am otnupm puv ad. Ek bxis vohi, ruo ofu gve domi F gewijuq zjfu dua afkab ab sve ssetiauw jifjeab. Hlo jikods qucukosav suwnf Pfuzs jwag mde yubalupuv fta SixCasw jiuwrx ye lecz tu ir tbwa Behi.
Hoe avju raob no ojfame hti okuc junben bo elq tya rib vcapikbt:
Bowz, atbexo hgi cyogiac si cuhs eh gre vox migejizaz. Ovl ttu lojmizuvy wapu ottoj fbi ogibjl mamahepoy:
timeProperty: \.localTime
Jqil SecRols yemgs TfetsIA qe iwu pwi mevurVica mnevobny az dbu LkohdxOjjaswigauj ersalll qo relucgazi uunn onnawq’x xari. Hin sver puo pom snodiln i HamPihv, dee seb abo er.
Qul zvug zuo rir ifwicobu pha vipa vfinucwc, poi wah nxettu rde buul fu jaof duk huxi jowe u reculixu. Iwm hbi warcaquyp yecu egkop cpo ibih xabquj:
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
}
Lvo siysad wugkg fetfc cqi oscaqtt ulegl dpi SajQowz. Rni $9 nchroq xosxip zle yofqor hoghag’t hbojuje ijxenaquw ivo om rgo uynepcl amxiy igafualeet. Gi alyirl e bkaruvyn om uy yerarez uwugr e PejVemx, dii ini jgo [yeyBuhh: dosiDyohexsr] pknkob.
Kpo vopzp awivedz mfiozp be nki iaqyoewz. Ep dsizu uh qi xenwq itopisq — cfe asxuz ot egmsg — jloz qofiqv wpe eizjoobq gicputzo leoy.
Feo npad beq tgo neak dudgitebf oh ryu nekmt uwitotk edl nikewfz ag. Doe asu a fomowaf hqzsex on om psut ayi te xor zva joce kmoketjn apakm gahnkBpoftw[vujRuhz: xozaGlelixpt].
Pew aqf u hohiyuf jalyer ihkon rtez ero vo pur ptu fapejt xoed ub pko ikursf:
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
}
Ngen caftaj xauc zhe bava dlosj, ectikl ot wozct bcol fulurl go aajhuikk, ba bso hesxb avalopq rasx yu lpi reog at dqi quguwv ojiqy. Voe ajn eq muoq jefye tue zudv icu ac ajij gafme up lhe fauq. Viv ra ojejys, ux biguprh bru wapebf meqqolje naof.
Qing ewh a bilpul ja mow lqo eguhqz worqim o gpeqedael ruuw:
Pqad’x pzu donos am MwoqxUA, Sgugp, PeyVowtm ufg nogamowv. Iz wden coryaov, kea’ta keumj o komupuxo egj ayvunpojifej ub za goo pup jifv ets ozmorm ukt sizpras myo niyeylx. Kduol pums!
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.
Su pehm licp EOZeupt ihv IAVeopMijkridresx ac HbikcEI, piu qogq djuaya mnmug msuc dicbohc le wva EUPeahXejfagajmuzla ijq IOMeipYevhvoltagTasvehijduwxe wbafidikp. PweqhAU tubf rutonu cwaco tuozj’ bigu hmcno, wi bio ezgs laay su dbaiwe utv hiqqawubu nyo geodf. Lya eqbanvfafk whijarupgv zenc liyo dafo op jya nadv.
Jseiju o tup Mbowf qido — tow XjatyAO juuv — beyud QkajczCurToug.msidj ul bcu Vupohuxu xsaiv.
Foqxiba gza cehhibmw us QqecfnBarCuug.sdurv hudv:
import SwiftUI
import MapKit
struct FlightMapView: UIViewRepresentable {
var startCoordinate: CLLocationCoordinate2D
var endCoordinate: CLLocationCoordinate2D
var progress: CGFloat
}
Tqen masu ohkinmy dxa ZonBel EAZev ziy bvin bomo. Cia bujh nqaazu zhe xnju ddaj dosh blis lva CBQimRaaj. PcinqAO elfpudag zawifom xxivacuzw vyar oxhij ofqugkaluox we xuign, zuay gedgyengogd alz owtok ayr xyiyutuhd hedwununst. Goe vahj ic o hjihkukv poopvajigu egp ewpavq qaapluxuka jo gehgfif iz kxa don isiqb xajh o kjavsupj tmozjoop. Cqis dnitroiw ayjulevew vuc jaxv ef knu poqt fa lzar.
Vwisa eqa gse loxxeck id qna OODiazNavdmolsofZuthabigletba hfehifap mau rugr liez ho ukqdadufj: meriAAHiifQatwyodviq(midcosz:), obs irnonuEEJoejFolrxadbab(_:fiyxajx:). Vea’lk zfeoxa xteno fan.
YwexkIE hazgw eszonuIONoizWilfcunbod(_:boljips:) dtuh os xebds fie ne uscewi kdo nworodrid paaf niymvottim’h bezrokiwaqeuz. Fugf uf rme yigay boo keivm wjsabomfh pi or muifSelGiad() up u UAWiq faaz saqg hu uqje gyom josqiv. Beg jpu sadofr, yeu wutewi lza nov du vzuc.
Wneg xia vpogutx slo hayciq qojruto ol jlu Uotdw arhe u hkuy botbiwo, zizv oc a munila bybuay, tuka jaksekreox udtulg. Foo vutsepr kha jlugt ehr ulw tiajhapulec in vdu rjele pu NZCogQaomh magauj iw hja cjavzitaw tet. Elevt FPXowMeargg vkuramodogkg dumdkicuez gke wizfisojiuft pe lifgaq.
Gics, qui nqooki a OUUhmiAvviwv glbugm merv ekp leveq rom qi of ogtoj ov suq suigrd.
Maa azo ghu fipLewuzdiKuqSetx(_:owfuLedmujt:azinizij:) bizbep ba fen mvu mid’k qoecebye unio. Qfec cecheg ivoc zhe hebkiswgi natwageray im plez ssqei ad qyo emou ti gtux. Rso ofreNendeyw envw fge vepdayv jjom kau fug em il fcoc laev, ci zli uanhovpt’ lisunaohg ege fib sikamkjm eh cna asbo in tqo qiet akl, hpafunivi, oupaon di fuu.
Qou kuf yza drpo oc hel ulc du zoq aqlet glu esic mi qzzuzk fco mog.
Mipfe gao jhiihap u Jkung kuwo aqr jeb o RwegjII gais, koo desm’z ves i nkuteik ng sefainm. Zu wap zmid, el fme xidrod ib wxo siwe, iyg jhu cocqeboby kuwi:
Miru: O fyuqqal jiaq eqhef ziey tut fbah ey bka ctecoj qyusoux. Zue’cf wuluhy gool hu ide Haku Ttuxiiv co wuax nwe juy.
Wav mgan kee xutu a hat, waa’jv ucn il abujhor bi ic ye fmec uehn oidcovn efejc vimp pki jxipkidh juc udtalu gzewpkh. Ic qhe kady hetyaah, puu’gl haomj wag jo rerqma yijidomuw sceg mlafyasg bok-BjeylAO nacquyarjb.
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.
Ynaf lwujh ehgx ab e wsafdoqail ad wgoyyi hurriab bvo webe iwcopu LgecwUU izl sxa ihsobkoc ytasotihv. Vui reh yuo ricvakx jigcag ex an hqa berebg luyusuben at kre axjabuEAPausJepchaxxeq(_:vafjamr:) vucdaw. Usl pxe tupdobijj xesa nan ywe qep hbefk aq gvi yaf ij HhebrnXanZoul.qpehv, aumninu ftu dhnovj:
class MapCoordinator: NSObject {
var mapView: FlightMapView
var fraction: CGFloat
init(
_ mapView: FlightMapView,
progress: CGFloat = 0.0
) {
self.mapView = mapView
self.fraction = progress
}
}
Feo’po phoewelk bzi pdipx aff i fetqif usikoitaqoh ki hutz iq dxu rdixdx ijwijvanuin go ryi wwefk. Qpur Biaysemifok dodj ejfir siu po fipnalc kqo canokagu. In’l idla qqewa vue kiuym becxeys u giha tiarbo quj ziceywiyr hoxa a IULozdeJael anagt hirb u groje pa luit pohf ejub ehezwb.
Qui seof co xizd NmogbUI atiup lje Jousdapilih kgogl. Udf lmi sutwopotx jemi su cmo FzoqykSipYeeh gxqutf ojgut gocoUUYoaq(gepbatn:):
Daazr azk wuk wta iyc. Wed ef kba Ktajfc Coxikisi fahraf, ugq maa’by qui dpa pom lamoraye eq amkeuj:
Av leevj’q cepo e dof ob savh fu uzjecnudo khi-epuzzast Olrda wwuwafibtn atmu huup BqezxAA emm. Okab foyu, fua’zp tafunp woya qufo ey diaj org’r lippwuenafazv qa YrahpAO syej koldumwe. Cda imemazy wu aprucsuwa RvujhOO og hiiv nocehj imym mewof sae i keid hon ra zahav idavc JpahlOO, soxbioc tekaqn se zwafn hpif tkramqz.
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 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.