MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 20

What's new in SwiftUI

Categories: SwiftUI 0 comments Watch the video

App & scene definition:

You can now build the whole app with SwiftUI, including what was previously handled by the application delegate and/or window controllers

The code below is a complete SwiftUI app:

@main
struct HelloWorld: App {
    var body: some Scene {
        WindowGroup {
            Text("Hello world!")
        }
    }
}

The API for defining apps is intentionally similar to the one for defining views:

  • the app definition is also a struct
  • it implements the App protocol similar to the View protocol, also with a required body property which is a function builder
  • the struct may have some properties marked with wrappers like @State

The body of an App returns a Scene, which is a new concept: it represents a piece of the app's user interface that can be independently displayed by the platform

The primary type of a Scene is a WindowGroup

A WindowGroup renders as a single full-screen window on iPhone and Watch, on iPad it supports the new multi-window scene system, and on the Mac it creates multiple Mac windows

Another scene type is Settings on the Mac, which creates Mac preferences windows:

#if os(macOS)
    Settings {
        PreferencesList()
    }
#endif

The preferences window is automatically styled as expected and linked to from a shortcut in the app menu

For document-based apps, use DocumentGroup which automatically manages documents:

@main
struct ShapeEditApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: SketchDocument()) { file in
            DocumentView(file.$document)
        }
    }
}

On iOS, it automatically presents a document browser screen

On the Mac, it opens different documents in separate windows and adds a File menu with all standard commands like Open/Save

You can also use the .commands view modifier to define custom menu commands:

.commands {
    CommandMenu("Shape") {
        Button("Add Shape", action: { ... })
            .keyboardShortcut("N")
        ...
    }
}

Use CommandMenu to create complete new menus, and CommandGroup to add single items to system menus like Help:

CommandGroup(after: .help) {
  Button(...)
}

See more about app/scene APIs in "App Essentials in SwiftUI" and "Build Document-Based Apps in SwiftUI"

There's also a new section of multi-platform Xcode project templates that creates SwiftUI apps which build for all platforms from a single codebase

New “Launch Screen” Info.plist key

  • lets you define launch screen elements like background, navigation bar, tab bar
  • a replacement for the launch storyboard for SwiftUI apps that don't otherwise have storyboards

iOS 14 also introduces widgets, which are built exclusively with SwiftUI

See more in "Build SwiftUI Views for Widgets" and "Widgets Code-Along"

Complications for Apple Watch can now also be built using SwiftUI

See more in "Build Complications in SwiftUI" and "Creating Complications for Apple Watch"

Improvements to lists and collections

Outlines – hierarchical lists which display items in a collapsible tree structure:

List(graphics, children: \.children) { … }

Grids – collection view-like, lazy-loading, vertically or horizontally scrolled 2D grids of content:

LazyVGrid(columns: [GridItem(.adaptive(minimum: 176))]) { … }
-> adapts number of columns to view size

LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) { }
-> fixed number of columns

New lazy versions of the existing stack views: LazyVStack, LazyHStack

See more about lists and grids in "Stacks, Grids, and Outlines in SwiftUI"

Toolbars

New common API for constructing toolbars on all platforms:

.toolbar {
    Button(action: recordProgress) {
        Label("Record Progress", systemImage: "book.circle")
    }

    ToolbarItem(placement: .primaryAction) {
        Button(...)
    }
}

You can use a Button to let the system automatically place the toolbar button in an appropriate place, or use ToolbarItem to customize the placement location using the placement parameter

ToolbarItem has various semantic names for placement like principal, confirmationAction, bottomBar etc.

Labels:

Label is a view that combines an icon with a title and shows them appropriately for the context:

Label("Books", systemImage: "book.circle")

// or, the full form:
Label {
    Text("...")
} icon: {
    Image(systemName: "...")
}

Depending on where a Label is used, the system may decide to only show the icon (and use the title for accessibility purposes), only the title, or show them both, arranged properly (also properly aligning multiple label icons in a list, scaling icons depending on dynamic font size, and so on)

Tooltips:

On macOS, you can use the new .help modifier to add help tooltips to toolbar buttons and other controls:

Button(action: recordProgress) {
    Label("Record Progress", systemImage: "book.circle")
}
.help("Record new progress entry")

This is available on all platforms – on other platforms the text is used for accessibility (Voiceover)

Keyboard shortcuts:

New .keyboardShortcut view modifier for adding keyboard shortcuts

This is mainly used in scene commands for Mac menu items / iOS keyboard shortcuts:

.commands {
    CommandMenu("Books") {
        Button("Previous Book", action: selectPrevious)
            .keyboardShortcut("[")
        Button("Next Book", action: selectNext)
            .keyboardShortcut("]")
    }
}

Shortcuts can also be set on other controls within the window content area, e.g. to create default & cancel buttons in Mac alert views:

HStack {
    Button("Cancel", action: dismissSheet)
        .keyboardShortcut(.cancelAction)
    Button("Save", action: saveLink)
        .keyboardShortcut(.defaultAction)
}

Focus

New default focus support on tvOS – the app can control where focus starts on the screen and how focus changes depending on app state

More information in the "Build SwiftUI apps for tvOS" talk

Other new controls

Progress view:

ProgressView("Downloading photo", value: percentComplete)
  • shows a determinate or indeterminate progress indicator
  • can be configured to display with a linear or circular style
  • plain ProgressView() creates a spinner

Gauge:

Gauge(value: acidity, in: 3...10) {
    Label("Soil Acidity", systemImage: "drop.fill")
        .foregroundColor(.green)
} currentValueLabel: {
    Text("\(acidity, specifier: "%.1f")")
} minimumValueLabel: {
    Text("3")
} maximumValueLabel: {
    Text("10")
}

Gauge is a watchOS-specific control, a linear or circular meter which displays the current value of something relative to its possible or expected range (mostly used in complications)

New effects and styling

Matched geometry effect:

Matched geometry is a new animation effect that allows you to fluently transition or morph one view into another view somewhere else in the view hierarchy

To use it, create a "namespace" the two views will share and mark them both with that namespace and a common identifier:

@Namespace private var namespace

var body: some View {
    ...

    LazyVGrid(...) {
        ForEach(unselectedAlbums) { album in
            AlbumCell(album)
        }
        .matchedGeometryEffect(id: album.id, in: namespace)
    }

    ...

    HStack {
        ForEach(selectedAlbums) { album in
            AlbumCell(album)
        }
        .matchedGeometryEffect(id: album.id, in: namespace)
    }
}

Container relative shapes:

This is a new "smart" shape type that takes the shape of the nearest containing shape:

.clipShape(ContainerRelativeShape())

This applies rounded corners that match the ones in the parent view, adjusted by the padding between the views so that the inner corners "fit into" the outer ones

Text scaling improvements:

Custom fonts (.font(.custom(...))) now automatically adapt their sizes with Dynamic Type

Images can be embedded inside text and are scaled together with it:

Text("\(Image(systemName: "music.mic")) \(album.artist)")

The new @ScaledMetric property wrapper can be used to scale any metric of your view with Dynamic Type:

@ScaledMetric private var padding: CGFloat = 10.0

...
Text(...).padding(padding)

This will scale the padding depending on the system font size so that it looks appropriate at all text sizes

See more about text scaling in "The Details of UI Typography"

Accent colors:

Support for accent colors on macOS

App accent color can now be set in the asset catalog in Xcode 12

.listItemTint() – customizes the tint color for list items – for the whole list, a section, or a specific item

On watchOS this replaces .listRowPlatterColor

You can now override the standard tint color for some controls like buttons and toggles using customized styles:

.toggleStyle(SwitchToggleStyle(tint: .accentColor))

System integration

Link element:

Link(destination: appleURL) {
    Label("SwiftUI tutorials", systemImage: "swift")
}

This creates a link label which opens the given URL in the default browser when tapped/clicked (or opens the relevant app in case of universal links)

openURL function:

There is a new openURL function stored inside the Environment that can be used to programmatically open URLs in the browser (like UIApplication.shared.open):

@Environment(\.openURL) private var openURL

var body: some View {
    Button("Open page", action: { openURL(homepageURL) })
}

Drag & Drop:

Drag & drop APIs are now based on the new UniformTypeIdentifiers framework, which provides content type identifiers

UTType objects let you access information about a content type like human readable description or whether it conforms to general types like "image"

Views from specialized frameworks:

A SwiftUI button that launches the "Sign In With Apple" flow:

import AuthenticationServices
import SwiftUI

SignInWithAppleButton(
    .signUp,
    onRequest: handleRequest,
    onCompletion: handleCompletion
)
.signInWithAppleButtonStyle(.black)

func onRequest(request: ASAuthorizationAppleIDRequest) { … }

func onCompletion(result: Result<ASAuthorization, Error>) { … }

Various new SwiftUI views for: AVKit, MapKit, SceneKit, SpriteKit, QuickLook, HomeKit, ClockKit, StoreKit, WatchKit