SwiftUI in Action

Nail Shakirov
Jul 3 · 8 min read

Apple Worldwide Developers Conference (WWDC) 2019 brought in a number of high-profile announcements, new technologies that aroused deep interest of Apple developers. The idea of this article to highlight one of them — the new declarative UI framework for Swift named “SwiftUI”.


Publication goal

To provide a detailed tutorial for Apple developers community how to boost development using SwiftUI implementing an example of Weather application with the support of newly introduced Dark Mode, Accessibility, and Dynamic Font Types.

What’s new?

In the past 5 years, Swift proved itself to be the best and most loved programming language to develop Apps for Apple platforms. After WWDC 2019, developers have reasons to be even more excited as Apple introduced SwiftUI — innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of declarative Swift syntax. So what does it mean for iOS developers?

  1. Declarative Syntax. The Code is simpler and easier to read, understand and maintain.
  2. Design Tools. Xcode 11 brings new design tools that make building interfaces even easier than before. Canvas provides a real-time visual presentation of your code, so you don’t need to re-run the project every time you change something.
  3. Drag and drop. Drag and drop in canvas allows arranging views simply by moving the mouse cursor.
  4. Previews. With previews and groups you there’s no need to run your app on a variety of screens as Xcode 11 allows to see UI components in any possible size you like in real-time.
  5. Native on all Apple platforms. SwiftUI supports all Apple’s platforms from watchOS up to tvOS.

A good way to showcase those new technologies and share our experience of using them is going through building a simple application, compatible with iOS, iPadOS, and macOS.

Tutorial

Creating a new project

Let’s jump in by creating a new project in Xcode 11. Based on the latest update on this Xcode version, we have a new option to Use SwiftUI. This checkmark is creating a bridge between UIKit and SwiftUI using UIHostingController (or NSHostingController for Mac Apps) which are ViewControllers but are capable of holding the new SwiftUI Views.

Xcode new project popup with Use SwiftUI option

So we’ve created our default project, and the first thing we see is the “Hello world” scene where we can start coding:

Default Hello World project scene

At the left side, we have an editor while on the right there is a new window called “Canvas” which allows us to see our UI changes in real-time.

Note that alongside with View struct, now we also have code for preview which runs on DEBUG scheme only. That code preview is connected directly with the “Canvas” in order to draw our UI in Canvas in real-time.

Building List (as a replacement for UITableView)

The first screen of our App should contain a list of cities with their current weather conditions.

So to start up things, let’s create a prototype cell for displaying a single city with accessibility (Voice Over) compliance in mind:

struct CityRow: View {
var city: City
var body: some View {
let currentCondition = city.day.first!
return HStack {
Image(systemName:currentCondition.type.systemImage)
.foregroundColor(currentCondition.type.imageColor)
// SF Symbols can easily be tinted with desired color
.font(.title)
// Support for dynamic font types
.padding(.leading, 16)
.frame(width:50, height:50, alignment: .center)
VStack(alignment: .leading) {
Text(city.name)
.font(.headline)
.color(.primary)
Text(city.country)
.font(.body)
.color(.secondary)
}.padding(.leading, 5)
Spacer()
Text(currentCondition.temperature.displayValue)
.padding(.trailing, 16)
.font(.largeTitle)
}
.accessibility(
label: Text(
"It's currently \(currentCondition.type.conditionText) in \(city.name) with temperature of \(currentCondition.temperature.displayValue)"
)
)
// Providing meaningful accessibility text for Voice over
}
}

We can use Groups in preview in order to see UI’s look and feel in different configurations and sizes in real-time.

Prototype cell with preview

Go back to the first scene and create a List with cells that we just implemented.

struct CityList: View {
let cities = Cities.availableCities!

var body: some View {
NavigationView {
List(cities.identified(by: \.id)) { city in
// List component requires hashable key to identify entities inside
CityRow(city: city)
}
.listStyle(.grouped)
.navigationBarTitle(Text("Weather"))
}
}
}

Previews in Canvas

Previews in Canvas can be easily configured using the same declarative semantics as used by SwiftUI (see the comments in the code below):

#if DEBUG 
struct CityList_Previews: PreviewProvider {
static var previews: some View {
Group {
CityList().colorScheme(.light)
// Previewing content in light appearance
CityList().colorScheme(.dark)
// Previewing content in dark appearance
}
}
}
#endif

That allows us to see real-time previews of our view both in light and dark appearances.

Previews with different states

Dark Mode

Speaking of light and dark appearances, SwiftUI is doing a really good job of tracking changes between dark and light appearances which user can pick in System Setting menu.

First of all, let’s focus on dynamic colors. In order to take advantage of dynamic appearances, colors now should have initializer with a closure that contains current traits. Traits present all user UI preferences including preferred font size, color schemes, etc. The only thing that would be relevant in this case is userInterface enum with two cases: .light and .dark.

extension UIColor {
static let darkColor = UIColor { (traits) -> UIColor in
traits.userInterfaceStyle = = .light ? .black : .white
// Since Swift 5.1 we can omit return keyword for single expression
}
}

It’s even easier if you are using Assets for storing your colors, as Apple added a drop-down menu that allows us to specify different appearances of current color in regards to color scheme.

Color Assets

Any appearance would be used on devices which don’t have Dark Mode (iOS version less than 13).

Light appearance is optional and can be skipped. If not specified, the system will pick Any Appearance option for Light interface of iOS.

Dark appearance specifies display color with Dark Appearance enabled on iOS device.

Remember, user needs to switch device between Light and Dark Appearance settings to see app UI reflecting proper appearance.

Images and SF Symbols

For the second screen, we want to show the weather of the selected city in details. These are expected to be shown on the device properly in both light and dark modes, as well as city images.

San Francisco Weather screen Light and Dark Modes with SF Symbols

Implementation of given screens is rather interesting as now we have to explicitly return View component which is GeometryReader that allows reading current screen size.

struct CityDetails : View {    var city: City    var body: some View {        GeometryReader { geometry in            VStack(alignment:.center) {                List() {                    Image(self.city.image)                        .resizable()                        .frame(height:geometry.size.width*0.5)                        .clipped()                    ScrollView(showsIndicators: false) {                        HStack(alignment: .center, spacing: 25) {                            ForEach(self.city.day) { condition in                                ConditionColomn(condition:condition)                            }                        }.frame(height:120)                    }.frame(height:120)                    ForEach(self.city.conditions) { condition in                        ConditionRow(condition:condition)                    }                }            }
}.navigationBarTitle(Text(city.name), displayMode: .inline)
}}

There are two components embedded in code above (ConditionColumn and ConditionRow), both configured with Condition object but with slightly different layouts. Let’s take a look at one of them:

struct ConditionColomn : View {    var condition:Condition    var body: some View {        VStack(alignment:.center) {            Text(condition.hourString)                .font(.subheadline)            ConditionImage(conditionType: condition.type)                .font(.title)                .padding(.bottom, 5)                .frame(width:40, height:40, alignment: .center)            Text(condition.temperature.displayValue)                .font(.headline)        }.padding(.vertical,.zero)    }}

GeometryReader component allows getting the actual component size that we can use to adjust image height based on view width.

Image. Image component can be initialized with image name directly (like Image(“Moscow”)) with fully automatic support for Light and Dark Appearances.

Dynamic Images are pretty straightforward to implement as it behaves the same as Colors with three available options for Appearance: Any, Light and Dark. (see picture below).

Images Asset

SF Symbols. Here we have Image component that uses systemImage initializer in order to grab the image by name from SF Symbols catalog. You can find all supported images directly in SF Symbols App available on the apple.developer.com

SF Symbols

Navigation

Now that we have CityList and CityDetail screens ready, it’s time to connect them. We can do this by implementing push transition with NavigationLink component. This component will handle user taps on view inside and navigate to destination screen. In our case, tappable view would be CityRow view and destination is CityDetails:

List(cities.identified(by: \.id)) { city in    NavigationLink(destination: CityDetails(city: city)) {        CityRow(city: city)    }}.listStyle(.grouped).navigationBarTitle(Text("Weather"))

That gives us the ability to push CityDetails in navigationController stack.

Running App on macOS

In order to run Application on Mac, we should use the following override function in SceneDelegate file:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {    #if targetEnvironment(UIKitForMac)        let windowScene = UIWindowScene(session: session, connectionOptions: connectionOptions)        let window = UIWindow(windowScene: windowScene)    #else        let window = UIWindow(frame: UIScreen.main.bounds)    #endif    window.rootViewController = UIHostingController(rootView: CityList())    self.window = window    window.makeKeyAndVisible()}

This code execution will give us the following result:

Application running on macOS Catalina

Results

Apple delivered a new way of building UI with declarative semantics which makes Apps development faster and more efficient across all Apple platforms with the power of Swift.

Utilizing this new approach, we have implemented an iOS App using only SwiftUI capabilities. Yet the resulting app can run on any Apple platform starting from watchOS up to tvOS.

Sources of this application are available on GitHub.

SwiftUI compatible with following Operating Systems: iOS 13, iPadOS 13, watchOS 6, macOS 10.15 and tvOS 13.


About us

We at Akvelon Inc love cutting edge technologies in mobile development, blockchain, big data, machine learning, artificial intelligence, computer vision, and many others. This article is the result of one of many projects developed in our Office Strategy Labs where we’re testing new technologies, approaches before delivering them to our clients.

Akvelon company official logo

If you would like to work with our strong Akvelon team — please see our open positions.

Written in collaboration with Artur Mikhaylov

Nail Shakirov

Written by

I research and write about IT technologies.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade