MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 20

App essentials in SwiftUI

Categories: SwiftUI 0 comments Watch the video

You can now build the entire app using just SwiftUI

New APIs for defining apps and their scenes and views

Defining apps & scenes

An app consists of one or more "scenes"

→ see "Introducing Multiple Windows on iPad" from 2019 for an introduction to the UIKit scenes API

A scene roughly maps to a window:

  • on iOS, watchOS and tvOS the app can present one scene at a time
  • on iPadOS you can see multiple scenes from the same or different apps side by side
  • on macOS, each scene is displayed in a different window or inside tabs within a window

Each scene contains a view hierarchy and/or a hierarchy of child scenes

An app declaration looks similar to a view declaration:

  • it's also a lightweight struct
  • it implements the App protocol similar to View
  • the struct can include properties defining data dependencies, marked with wrappers like @State
  • the protocol requires a single body property which returns some Scene
  • inside the body definition, a DSL similar to the one for views is used to define the scene hierarchy:
@main
struct BookClubApp: App {
  @StateObject private var store = ReadingListStore()

  var body: some Scene {
    WindowGroup {
      ReadingListView(store: store)
    }
  }
}

The @main attribute is a new feature of Swift 5.3 – it allows a type to serve as the program entry point

This replaces the @NSApplicationMain/@UIApplicationMain or main.swift file

The code above is a complete declaration of the app structure

Even though it's just a few lines of code, this automatically provides quite a lot of functionality for free

Window Group

The most common type of scene is a WindowGroup, which defines the primary interface (main view) of your app

A WindowGroup displays its view in the expected way or each platform:

  • on iPhone, tvOS and watchOS it's shown as a single full-screen window
  • on the iPad it supports the new multi-window scene system
  • on the Mac it creates multiple Mac windows

The lifecycle of scenes depends on the platform

On iPadOS, the app switcher shows the app name and an optional scene title above each scene/window – this can be the document title, title of the open web page etc.

In latest SwiftUI, you can set that scene title using the new .navigationTitle("Name") view modifier, which replaces .navigationBarTitle (it sets both the title seen in the navigation bar and the scene name in the app switcher)

On the Mac, WindowGroup automatically provides a "File > New Window" command that opens a new window and a Window menu that lists all windows

The scene title is used as the window title in the title bar and the Window menu

Scene windows can also me merged into a single window with tabs by choosing Window > Merge All Windows

Scene storage

@SceneStorage is a new property wrapper that helps you manage persistence and restoration of view state in scenes

Provide a unique key under which the given value should be saved, and it will be saved and restored at appropriate times automatically

struct ReadingListViewer: View {
  @SceneStorage("selectedItem") var selectedItem: String?

  var body: some View {
    NavigationView {
      ReadingList(selectedItem: $selectedItem)
    }
  }
}

Document Group

Another basic kind of scene in the app definition is DocumentGroup, which defines documents in a document-based app:

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

Preferences windows

The last scene type, Settings, is Mac-only and defines standard Mac Preferences windows:

var body: some Scene {
  WindowGroup {
    ...
  }

  Settings {
    BooksSettingsView()
  }
}

The Preferences window is automatically linked to from the main app menu with the standard Cmd+, keyboard shortcut

Menu commands

The new .commands view modifier allows you to define menus and menu items with shortcuts for the macOS app menu:

WindowGroup {
  ...
}
.commands {
  BookCommands()
}
struct BookCommands: Commands {
  @FocusedBinding(\.selectedBook) private var selectedBook: Book?

  var body: some Commands {
    CommandMenu("Book") {
      Section {
        Button("Update Progress...", action: updateProgress)
          .keyboardShortcut("u")
        Button("Mark Completed", action: markCompleted)
          .keyboardShortcut("C")
      }
      .disabled(selectedBook == nil)
    }
  }

  private func updateProgress() { selectedBook?.recordNewProgress() }
  private func markCompleted() { selectedBook?.markCompleted() }
}

The commands are also used on the iPad and are displayed in the keyboard shortcuts help popup

The @FocusedBinding property wrapper used in this commands block allows you to selectively enable/disable some groups of commands based on your focus, similar to how the responder chain is used in AppKit or UIKit

See more about commands in the reference documentation