19.
Complex Interfaces
Written by Bill Morefield
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:
HStack(alignment: .bottom) {
DepartureTimeView(flight: flight)
Spacer()
ArrivalTimeView(flight: flight)
}
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)
}
This method takes two Dates and returns the number of minutes between them.
- The
dateComponents(_:from:to:)method returns the differences between two dates in the requested units, in this case, minutes. - If something went wrong and the
minuteproperty doesn’t exist, then return zero minutes. - Return the absolute value of the number of minutes. The absolute value returns only the magnitude ignoring the sign, always resulting in a positive value.
Now you case use this method to get the progress of the flight as floating point value. Add the following code after the minutesBetween(_:and:) method:
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)
}
}
}
There’s a lot here, and it’s somewhat repetitive:
- You put the current
Dateinto a variable as you’ll refer to it often in this method. - The first case covers departing flights. The case for arriving flights works the same, but with the local and other times swapped.
- If the
localTimefor the departing flight is after now, then it’s not departed yet, meaning the fraction is zero. - If the
otherEndTimeparameter for the departing flight is before now, then the flight arrived, meaning the fraction is one. - If neither is true, then the flight is somewhere in the air. This code uses the
minutesBetween(_:and:)method to get the minutes between now and the flight’s departure time in minutes. - The
flightTimeparameter gives the total length of the flight. You calculate the fraction as the value calculated in the last step, divided by the flight’s length. - You return the value as a CGFloat to make it easier to work with drawings. For more on this, look in Chapter 17: Drawing & Custom Graphics.
The case for arriving flights works much the same as described except the roles of the local and remote times swapped.
With a method to calculate the flight location, you’ll now add a graphical representation in the next section.
Adding inline drawings
In this section you’ll add a view to show the flight progress. Add the following view after the ArrivalTimeView view:
struct FlightProgressView: View {
var flight: FlightInformation
var progress: CGFloat
var body: some View {
// 1
GeometryReader { proxy in
Image(systemName: "airplane")
.resizable()
// 2
.offset(x: proxy.size.width * progress)
.frame(width: 20, height: 20)
.foregroundColor(flight.statusColor)
// 3
}.padding([.trailing], 20)
}
}
If you need a refresher on drawing, see Chapter 17: Drawing & Custom Graphics. The specifics for this view:
- The
GeometryReadercauses the view to fill the space. It also provides aGeometryProxyyou will use to get the width of the view. - You get the view’s width using the
sizeproperty on theGeometryProxy. Multiplying this value by the fraction of the flight gives an offset to reflect the flight’s progress. - The offset in step two ignores that the offset controls the left side of the image. Setting the offset to the far edge pushes the image outside the view. You add a 20 point padding to the view’s trailing edge, providing a space for the image.
Now use the new view. In the HStack within FlightCardView replace the Spacer with a call to the function:
FlightProgressView(
flight: flight,
progress: flightTimeFraction(
flight: flight
)
)
Build and run the app, and you’ll see the progress indicator added to each flight.
Now that you have the underlying grid in place, you’ll let the caller specify the view inside the grid.
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.
Recall the initial code for this list below:
ForEach(flights) { flight in
FlightCardView(flight: flight)
}
You passed FlightCardView to the closure of the ForEach loop. ForEach uses a ViewBuilder to create a parameter for the view-producing closure. You’ll now move the timeline to a separate view and update it to take a passed view through the closure instead of hard—coding it.
Create a new SwiftUI view named GenericTimeline in the Timeline group. First, update the struct definition to the following:
struct GenericTimeline<Content>: View where Content: View
This change allows GenericTimeline to accept View values as dependencies. With that, update the contents of GenericTimeline to the following:
// 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)
}
}
}
}
Here’s what you’ve added:
- This new
GenericTimelineview will take a list ofFlightInformationvalues and a closure that instructs how this view should useFlightInformationto display a view. - In order to make use of
content, you need to use the@ViewBuilderfunction builder. You’ll create a custom initializer that applies the function builder. - Your new
bodyproperty displays a list of generic views that are constructed using thecontentview builder.
Next, update the preview to include the new changes you’ve made:
GenericTimeline(
flights: FlightData.generateTestFlights(
date: Date()
)
) { flight in
FlightCardView(flight: flight)
}
Now that GenericTimeline is set up, navigate to TimelineView.swift. Find the following block of code that creates the list of views:
ScrollView {
VStack {
ForEach(flights) { flight in
FlightCardView(flight: flight)
}
}
}
Replace the above code with the following:
GenericTimeline(flights: flights) { flight in
FlightCardView(flight: flight)
}
Take a moment to appreciate what you’ve created here. Instead of hard coding the list-view behaviour in TimelineView, you’re now using a generic view that does this for you.
Run the app and verify the timeline looks as before. While there’s no change in appearance, you’ve gained a more flexible way to choose the view to show for each flight.
While this change makes it easier to specify different views, it still relies on the FlightInformation structure preventing reuse in other projects. In the next section, you’ll address that limitation.
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.
First, change the declaration of the view to:
struct GenericTimeline<Content, T>: View where Content: View {
You’re saying here that you want to use a generic type in the struct. Instead of specifying Int, FlightInformation or another type, you can now specify T. You can now change the other references to FlightInformation into the generic type T instead. Change the declaration of the flights property to:
var events: [T]
You’re also changing the name to reflect this value no longer ties only to flights but also works with any event. You also need to change the type for the parameter passed into the closure. Change the definition of the Content property to:
let content: (T) -> Content
You’ll also need to change the custom initializer to use T instead of the FlightInformation type. You also need to change the flights property to events. Change the init() method to:
init(
events: [T],
@ViewBuilder content: @escaping (T) -> Content
) {
self.events = events
self.content = content
}
Now you need to change references in the view to flights to events. First change the preview to use the new parameter name:
GenericTimeline(
events: FlightData.generateTestFlights(
date: Date()
)
) { flight in
FlightCardView(flight: flight)
}
There’s a hidden problem lurking in the view that results from using a generic. Change the view to:
ScrollView {
VStack {
ForEach(events) { flight in
content(flight)
}
}
}
You’ll see an error: Referencing initializer ‘init(_:content:)’ on ‘ForEach’ requires that ‘T’ conform to ‘Identifiable’. Generics add flexibility, but this is the cost of that flexibility. There’s no way for SwiftUI to know that the type you later specify will implement the Identifiable protocol required by ForEach. To work around this, change the code to:
ScrollView {
VStack {
ForEach(events.indices) { index in
content(events[index])
}
}
}
Instead of iterating over the collection items, you iterate over the collection’s indices, which ForEach happily accepts. You reference the individual elements of the collection using the index.
Now back in TimelineView.swift change the parameter on GenericTimeline from flights to events:
GenericTimeline(events: flights) { flight in
You’re done. Generics let you pivot from a specific reference to the generic represented by T in this case. Swift handles the rest. Run the app to see that your timeline still works.
Right now, your timeline isn’t that much of a timeline. Let’s change that. You’ll also learn about another feature of Swift used in SwiftUI — KeyPaths.
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.
Back in Chapter 14: Lists you used a KeyPath in the following code:
ForEach(flightDates, id: \.hashValue) { date in
When using ForEach with a collection of objects that don’t implement Identifiable, you pass in a KeyPath to the id parameter. The KeyPath provides SwiftUI with a property that identifies each element uniquely.
Here \.hashValue is a KeyPath telling SwiftUI that the hashValue property on the object uniquely identifies it.
Since your timeline takes a generic type, meaning you could pass in any object, you need a way to let the view know the property on the object that contains the time information. That’s the perfect use for a KeyPath.
First in GenericTimeline.swift add the following property after content:
let timeProperty: KeyPath<T, Date>
Declaring a KeyPath takes two parameters. The first is the type of object for it. In this case, you use the same T generic type you added in the previous section. The second parameter tells Swift that the parameter the KeyPath points to will be of type Date.
You also need to update the init method to add the new property:
init(
events: [T],
timeProperty: KeyPath<T, Date>,
@ViewBuilder content: @escaping (T) -> Content
) {
self.events = events
self.content = content
self.timeProperty = timeProperty
}
Next, update the preview to pass in the new parameter. Add the following code after the events parameter:
timeProperty: \.localTime
This KeyPath tells SwiftUI to use the localTime property of the FlightInformation objects to determine each object’s time. Now that you can specify a KeyPath, you can use it.
Now that you can indicate the time property, you can change the view to look bit more like a timeline. Add the following code after the init method:
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
}
This method takes the events and sorts them in ascending by the property specified using the KeyPath:
- The method first sorts the objects using the KeyPath. The $0 syntax within the
sortedmethod’s closure indicates one of the objects under evaluation. To access a property of it defined using a KeyPath, you use the[keyPath: timeProperty]syntax. - The first element should be the earliest. If there is no first element — the array is empty — then return the earliest possible hour.
- You then get the hour component of the first element and returns it. You use a similar syntax as in step one to get the time property using
firstFlight[keyPath: timeProperty].
Now add a similar method after this one to get the latest hour in the events:
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
}
This method does the same thing, except it sorts from latest to earliest, so the first element will be the hour of the latest event. You add an hour since you will use an open range in the loop. For no events, it returns the latest possible hour.
Next add a method to get the events within a specified hour:
func eventsInHour(_ hour: Int) -> [T] {
return events
.filter {
let flightHour =
Calendar.current.component(
.hour,
from: $0[keyPath: timeProperty]
)
return flightHour == hour
}
}
Like the other two methods, this one uses the KeyPath to filter only flights where the hour component of the time matches that passed into the method.
Add one more method:
func hourString(_ hour: Int) -> String {
let tcmp = DateComponents(hour: hour)
if let time = Calendar.current.date(from: tcmp) {
return shortTimeFormatter.string(from: time)
}
return "Unknown"
}
This one takes a passed hour and creates a string displaying the time at that hour.
Now you’ll update the view using these new methods. Change the body for the GenericTimeline to:
ScrollView {
VStack(alignment: .leading) {
// 1
ForEach(earliestHour..<latestHour) { hour in
// 2
let hourFlights = eventsInHour(hour)
// 3
Text(hourString(hour))
.font(.title2)
// 4
ForEach(hourFlights.indices) { index in
content(hourFlights[index])
}
}
}
}
You’ve added a few more features to the timeline. Here are the new items:
- You now loop through the hours of events using the
earliestHourandearliestHourproperties. - For each hour, you use the
eventsInHourmethod to get only the events taking place in that hour. - Each hour shows a header with the time using the
hourStringmethod. - You now only loop through the
hourFlightsindices since you’re splitting the overall events into hours.
With a generic timeline done, you can now use it in your view.
Using the timeline
First let’s give a nicer appearance to the cards. Open FlightCardView.swift and add the following at the end of the VStack:
.padding()
.background(
Color.gray.opacity(0.3)
)
.clipShape(
RoundedRectangle(cornerRadius: 20)
)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke()
)
Back in TimelineView.swift, update the GenericTimeline to:
GenericTimeline(
events: flights,
timeProperty: \.localTime) { flight in
FlightCardView(flight: flight)
}
Run the app to see your improved timeline.
That’s the power of SwiftUI, Swift, KeyPaths and generics. In this section, you’ve built a timeline and encapsulated it so you can pass any object and display the results. Great work!
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.
To work with UIViews and UIViewControllers in SwiftUI, you must create types that conform to the UIViewRepresentable and UIViewControllerRepresentable protocols. SwiftUI will manage these views’ life cycle, so you only need to create and configure the views. The underlying frameworks will take care of the rest.
Create a new Swift file — not SwiftUI view — named FlightMapView.swift in the Timeline group.
Replace the contents of FlightMapView.swift with:
import SwiftUI
import MapKit
struct FlightMapView: UIViewRepresentable {
var startCoordinate: CLLocationCoordinate2D
var endCoordinate: CLLocationCoordinate2D
var progress: CGFloat
}
This code imports the MapKit UIKit for this file. You next create the type that will wrap the MKMapView. SwiftUI includes several protocols that allow integration to views, view controllers and other app framework components. You pass in a starting coordinate and ending coordinate to display on the map along with a progress fraction. This fraction indicates how much of the path to draw.
There are two methods in the UIViewControllerRepresentable protocol you will need to implement: makeUIViewController(context:), and updateUIViewController(_:context:). You’ll create those now.
Add the following code to the struct below the progress parameter:
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
SwiftUI will call makeUIViewController(context:) once when it is ready to display the view. Here, you create a MKMapView programmatically and return it using the Swift feature that treats a single-line method as an implicit return. Any UIKit ViewController would work here; there are similar protocols for AppKit, WatchKit and other views and view controllers on the appropriate platform.
Now add this code to the end of the struct to implement the second method:
func updateUIView(_ view: MKMapView, context: Context) {
// 1
let startPoint = MKMapPoint(startCoordinate)
let endPoint = MKMapPoint(endCoordinate)
// 2
let minXPoint = min(startPoint.x, endPoint.x)
let minYPoint = min(startPoint.y, endPoint.y)
let maxXPoint = max(startPoint.x, endPoint.x)
let maxYPoint = max(startPoint.y, endPoint.y)
// 3
let mapRect = MKMapRect(
x: minXPoint,
y: minYPoint,
width: maxXPoint - minXPoint,
height: maxYPoint - minYPoint
)
// 4
let padding = UIEdgeInsets(
top: 10.0,
left: 10.0,
bottom: 10.0,
right: 10.0
)
// 5
view.setVisibleMapRect(
mapRect,
edgePadding: padding,
animated: true
)
// 6
view.mapType = .mutedStandard
view.isScrollEnabled = false
}
SwiftUI calls updateUIViewController(_:context:) when it wants you to update the presented view controller’s configuration. Much of the setup you would typically do in viewDidLoad() in a UIKit view will go into this method. For the moment, you define the map to show.
- When you project the curved surface of the Earth onto a flat surface, such as a device screen, some distortion occurs. You convert the start and end coordinates on the globe to
MKMapPointvalues in the flattened map. Using MKMapPoints dramatically simplifies the calculations to follow. - Next, you determine the minimum and maximum
xandyvalues among these points. - You create a
MKMapRectfrom those minimum and maximum values. The resulting rectangle covers the space between the two points along the rectangle’s edge. - Next, you create a
UIEdgeInsetsstruct with all sides set to an inset of ten points. - You use the
setVisibleMapRect(_:edgePadding:animated:)method to set the map’s viewable area. This method uses the rectangle calculated in step three as the area to show. TheedgePaddingadds the padding that you set up in step four, so the airports’ locations are not directly at the edge of the view and, therefore, easier to see. - You set the type of map and do not allow the user to scroll the map.
Since you created a Swift file and not a SwiftUI view, you didn’t get a preview by default. To fix that, at the bottom of the file, add the following code:
struct MapView_Previews: PreviewProvider {
static var previews: some View {
FlightMapView(
startCoordinate: CLLocationCoordinate2D(
latitude: 35.655, longitude: -83.4411
),
endCoordinate: CLLocationCoordinate2D(
latitude: 36.0840, longitude: -115.1537
),
progress: 0.67
)
.frame(width: 300, height: 300)
}
}
Note: A wrapped view often does not show in the static preview. You’ll likely need to use Live Preview to view the map.
Now that you have a map, you’ll add an overlay to it to show each airport along with the progress for active flights. In the next section, you’ll learn how to handle delegates when wrapping non-SwiftUI components.
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.
This class acts as a transition or bridge between the data inside SwiftUI and the external framework. You can see context passed in as the second parameter in the updateUIViewController(_:context:) method. Add the following code for the new class at the top of FlightMapView.swift, outside the struct:
class MapCoordinator: NSObject {
var mapView: FlightMapView
var fraction: CGFloat
init(
_ mapView: FlightMapView,
progress: CGFloat = 0.0
) {
self.mapView = mapView
self.fraction = progress
}
}
You’re creating the class and a custom initializer to pass in the flight information to the class. This Coordinator will allow you to connect the delegate. It’s also where you could connect a data source for something like a UITableView along with a place to deal with user events.
You need to tell SwiftUI about the Coordinator class. Add the following code to the FlightMapView struct after makeUIView(context:):
func makeCoordinator() -> MapCoordinator {
MapCoordinator(self, progress: progress)
}
This method creates the coordinator and returns it to the SwiftUI framework to pass in where necessary. SwiftUI will call makeCoordinator() before makeUIViewController(context:) so it’s available during the creation and configuration of your non-SwiftUI components.
You can now implement the overlays that need a delegate. In updateUIView(_:context:) add the following code to the top of the method:
let startOverlay = MKCircle(
center: startCoordinate,
radius: 10000.0
)
let endOverlay = MKCircle(
center: endCoordinate,
radius: 10000.0
)
let flightPath = MKGeodesicPolyline(
coordinates: [startCoordinate, endCoordinate],
count: 2
)
view.addOverlays([startOverlay, endOverlay, flightPath])
You create three overlays. The first and second are circles located at the start and end coordinates. You next create a MKGeodesicPolyline connecting the start and end coordinates. An MKGeodesicPolyline creates a shape that follows the contours of the Earth along the shortest path between points. As mentioned earlier, the movement from the Earth’s curved surface to the flat map distorts shapes. An MKGeodesicPolyline reflects the shortest path over the Earth. It often appears curved when shown on a flat map. It also provides a good representation of the route a plane would take flying between two points.
If you’re familiar with MKMapView, then you know you need implement the delegate for the overlays to show. Add the following class extension after the current Coordinator class definition:
extension MapCoordinator: MKMapViewDelegate {
func mapView(
_ mapView: MKMapView,
rendererFor overlay: MKOverlay
) -> MKOverlayRenderer {
if overlay is MKCircle {
let renderer = MKCircleRenderer(overlay: overlay)
renderer.fillColor = UIColor.black
renderer.strokeColor = UIColor.black
return renderer
}
if overlay is MKGeodesicPolyline {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = UIColor(
red: 0.0,
green: 0.0,
blue: 1.0,
alpha: 0.3
)
renderer.lineWidth = 3.0
renderer.strokeStart = 0.0
renderer.strokeEnd = fraction
return renderer
}
return MKOverlayRenderer()
}
}
This extension handles the overlays. For the MKCircle, it merely colors the circles black. For the MKGeodesicPolyline, it strokes the line with a mostly transparent blue color. It sets the strokeEnd property on the renderer using the fraction property passed into the MapCoordinator class. This ending location lets it reflect the partial distance of flights that are in progress. Note that this class and control know nothing about SwiftUI. The code you’ve used here works as it does in UIKit.
Now that you’ve implemented a MKMapViewDelegate, you can set it for the MKMapView. Update makeUIView(context:) to:
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView(frame: .zero)
view.delegate = context.coordinator
return view
}
Now you can add the new view to the app. Open FlightCardView.swift and add the following code at the end of the VStack:
FlightMapView(
startCoordinate: flight.startingAirportLocation,
endCoordinate: flight.endingAirportLocation,
progress: flightTimeFraction(
flight: flight
)
)
.frame(width: 300, height: 300)
Build and run the app. Tap on the Flight Timeline button, and you’ll see the new timeline in action:
It doesn’t take a lot of work to integrate pre-existing Apple frameworks into your SwiftUI app. Over time, you’ll likely move more of your app’s functionality to SwiftUI when possible. The ability to integrate SwiftUI in your legacy apps gives you a neat way to begin using SwiftUI, without having to start from scratch.
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
Controllerclass 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
Controllerinside your SwiftUI view and place other framework code within theControllerclass. - You can use a
ViewBuilderto pass views into another view when doing iterations. - Generics let your views work without hard-coding specific types.
- A KeyPath provides a way to define a property on an object without invoking the property.