WWDC20 - Technical Notes

WWDC20 - Technical Notes

As passionate Apple developer, I have started watching the various WWDC20 videos.
Quite immediately, I had the need to takes notes on technical aspects or API. So I've thought that it would be better to write them here.

Keynote

  • No technical notes here

Platforms State of the Union

Apple Silicon Macs and Big Sur

  • 17:10 - Universal apps contain code compiled for both Apple Silicon and Intel; they are merged together in a single executable; the OS chooses the right one. In this way it's possible to distribute a single executable and it will run on every Mac, independently from its processor architecture.
  • 18:02 - For most apps, all is required is to open the project in Xcode 12 and recompile it. During development, Xcode builds only for the current processor architecture, to save time. To create an universal version, you need to switch and compile for "Any Mac (Apple Silicon, Intel)".
  • 24:50 - Rosetta is completely integrated in MacOS Big Sur, so if you have an Intel app that hasn't been recompiled for Apple Silicon, it will run automatically - you can test the emulation in Xcode, targeting My Mac (Rosetta).
  • 32:05 - Apple Silicon Macs will be able to run the majority of iPhone and iPad apps without recompiling. It uses the same Catalyst infrastructure to port the iOS AppKit to the Mac (but unmodified apps won't be able to get the advantages the Catalyst offers).
  • 32:45 - By default, eligible iOS apps will apps will appear in the Mac AppStore (developers can opt-out).
  • 39:15 - It's possible to conform to the UNNotificationContentExtension protocol to show custom content when a notification is received.
  • 40:00 - Windows will automatically adapt the new structure (toolbar, with separate left bar) automatically for Catalyst apps, otherwise you will need to use NSWindow.StyleMask.fullSizeContentView and NSSplitViewItem.Behavior.sidebar.
  • 40:30 - Style changes in the toolbar: you can designate a toolbar item to be navigational with NSToolbarItem.isNavigational (AppKit) and ToolbarItemPlacement.navigation (SwiftUI).
  • 40:45 - You can make the toolbar large with NSControlSize.large (AppKit) and ControlSize.large (SwfitUI).
  • 40:50 - You can make the search field in the toolbar to expand automatically embedding it in a NSSearchToolbarItem.
  • 41:00 - There is a new toolbar structure with 3 panes (like mail), with the new NSTrackingSeparatorToolbarItem (AppKit), primarySidebarTrackingSeparator / supplementarySidebarTrackingSeparator (Catalyst) and .toolbar (SwiftUI).
  • 41:40 - SF Symbols can be configure to perfectly match the size and weight of text with NSImage.SymbolConfiguration.
  • 42:05 - It's possible to define an app's accent color in the Info.Plist with NSAccentColorName and it's also possible to define accent color for individual sidebar items with outlineView(_:tintColorForItem:) (AppKit) and listItemTint(_:) (SwiftUI).

iPadOS

iOS

  • 1:01:30 - Create a widget in SwiftUI with a struct conforming to the Widget protocol, that returns a WidgetConfiguration to the system. The WidgetConfiguration receives a TimelineProvider, that has a snapshot() function, used when the system wants to display a single entry, and timeline(), used when the system wants to display multiple entries, based on the time of the day.

SwiftUI

  • 1:22:00 - New LazyVStack and LazyVGrid controls allows to transform a normal VStack and Grid for big collections, reducing memory usage.

Introduction to SwiftUI

  • 0:50 - Xcode 12 has a project template for the creation of multi-platform applications.
  • 6:35 - Make each element of a List data source to be Identifiable, to let the list know when new items are coming and going.
  • 7:00 - In previews you can use your own test data.
  • 9:00 - NavigationView enables navigation between different parts of your application. It's also possible to provide a NavigationTitle.
  • 9:35 - Wrap each list cell in a NavigationLink; this receives the destination view to push.
  • 12:20 - To have a list footer showing the number of items in the list, do this:
    List {
       ForEach(sandwiches) { sandwich in
          SandwichCell(sandwich: sandwich)
       }
       
       HStack {
          Spacer()
          Text("\(sandwiches.count) sandwiches")
             .foregroundColor(.secondary)
          Spacer()
       }
    }
    
  • 13:40 - For the detail view, during the preview it's again possible to use the test data.
  • 16:00 - It's also possible to put the preview detail in a NavigationView, to be able to see its navigation title.
  • 19:50 - @State is an attribute for variables that contain local state of the view (it's a source of truth).
  • 20:15 - Use onTapGesture to execute code when the user taps on a view.
  • 23:00 - Data flow primitives:
    Source of Truth Derived Value
    Read-only Constant Property
    Read-write @State, ObservableObject @Binding
  • 31:45 - By default SwiftUI respects the safe area, but you can override it with .edgesIgnoringSafeArea(.bottom).
  • 32:00 - It's possible to build an animation with withAnimation wrapping a method, for example:
    .onTapGesture {
       withAnimation {
          zoomed.toggle()
       }
    }
    
  • 33:10 - Display a label with an associate image with:
    Label("Spicy", systemImage: "flame.fill")
    
  • 34:05 - It's possible to specify a minLength for a spacer, so that the spacer can be collapsed when maximizing another element.
  • 36:20 - It's possible now to show some items only in determinate conditions with if, for example:
    if sandwich.isSpicy {
       HStack {
          ...
       }
    }
    
  • 36:40 - The previews can show multiple versions of your view, clicking on the "+" button in the previews toolbar. This creates a Group view containing the different views that we want to show in each preview.
  • 37:40 - It's possible to customize the animation behavior specifying a different transition, for example:
    .transition(.move(edge: .bottom))
    
  • 40:30 - When displaying the app on an iPad, to have a placeholder saying to select an item from the list, add below that another view:
    NavigationView {
       List {
          ...
       }
       
       Text("Select a sandwich")
          .font(.largeTitle)
    }
    
    In this case, the first view is shown on the left, and the second view becomes the placeholder on the right (on an iPhone the placeholder is automatically removed, because not needed).
  • 43:00 - If you want to have an object that tells SwiftUI when its value changes, make it conformant to ObservableObject, and mark the properties that I want to track with @Published; then in our view use @StateObject to make it source of truth for our mutable object.
  • 43:50 - If the model is shared, it's also possible to move it into the App model (again, marked with @StateObject), and the pass it as parameter to the views. When receiving it as parameter, mark it in the view with @ObservedObject.
  • 46:00 - In our List, we can add onMove() and onDelete() modifiers to execute actions when the list items are moved and deleted.
  • 46:50 - In iOS, to go to edit mode, we add a toolbar modifier to the List, and inside add an EditButton(), but make it visible only in iOS:
    List {
       ...
    }
    .navigationTitle("Sandwiches")
    .toolbar {
       #if os(iOS)
       EditButton()
       #endif
    }
    

48:00 - Button to add a new sandwich:

.toolbar {
   #if os(iOS)
   EditButton()
   #endif
   Button("Add", action: makeSandwich)
}

49:45 - Add a new preview, then use the Inspect button to customize it, for example changing the default font size (sizeCategory), or setting the color scheme to dark, or testing the app in another layoutDirection (right to left) and another locale.
53:20 - It's possible to send the views to the physical iPhone, without the need to recompile.

What's new in SwiftUI

Apps and widgets

  • 1:15 - For the first time, it's possible to build apps totally in SwiftUI. The App model follows very closely what already learned for views. The body of an app returns a Scene.
  • 3:15 - WindowGroup is a scene that provides multi-platform functionalities out of the box (for example, multi-window on iPad and MacOS).
  • 4:50 - The Settings scene (available on MacOS) allows to add a Preferences window to your app.
    @main
    struct BookClubApp: App {
       @SceneBuilder var body: some Scene {
          WindowGroup {
             ...
          }
       
       #if os(macOS)
          Settings {
             BookClubSettingsView()
          }
       #endif
       }
    }
    
  • 5:10 - The DocumentGroup scene allows multiple documents and automatically manages opening, closing and saving documents:
    @main
    struct ShapeEditApp: App {
       var body: some Scene {
          DocumentGroup(newDocument: SketchDocument()) { file in
             DocumentView(file.$document)
          }
       }
    }
    
    On iOS and iPadOS, the document group will present a document interface; on Mac it will open a different window for each document, and automatically add commands to the main menu for common document actions.
  • 5:40 - It's possible to specify additional commands:
    @main
    struct ShapeEditApp: App {
       var body: some Scene {
          DocumentGroup(newDocument: SketchDocument()) { file in
             ...
          }
          .commands {
             CommandMenu("Shape") {
                Button("Add Shape...", action: { ... })
                   .keyboardShortcut("N")
                Button("Add Text", action: { ... })
                   .keyboardShortcut("T")
             }
          }
       }
    }
    
  • 6:45 - New multi-project experience in Xcode with the new Multiplatform tab.
  • 7:15 - New Launch Screen key in info.plist.
  • 7:50 - Widgets: they are built exclusively with SwiftUI, using custom structs conforming to the new Widget protocol:
    @main
    struct RecommendedAlbum: Widget {
       var body: some WidgetConfiguration {
          StaticConfiguration(
             kind: "RecommendedAlbum",
             provider: Provider(),
             placeholder: PlaceholderView()
          ) { entry in
             AlbumWidgetView(album: entry.album)
          }
          .configurationDisplayName("Recommended Album")
          .description("Your recommendation for the day.")
       }
    }
    
  • 8:35 - Complications for Apple Watch:
    struct CoffeeHistoryChart: View {
       var body: some View {
          VStack {
             ComplicationHistoryLabel {
                Text("Weekly Coffee")
                   .complicationForeground()
          }
          HistoryChart()
       }
       .complicationChartFront()
    }
    

Lists and collections

  • 9:30 - Outlines: they are lists with nested elements. Note the children parameter passed to the List initializer:
    struct ContentView: View {
       var graphics: [Graphic]
       
       var body: some View {
          List(graphics, children: \.children) { graphic in
             GraphicRow(graphic)
          }
          .listStyle(SidebarListStyle())
       }
    }
    
    struct Graphic: Indetifiable {
       var id: String
       var name: String
       var icon: Image
       var children: [Graphic]?
    }
    
  • 10:10 - Lazy loading grid layouts, can be composed with ScrollViews:
    var items: [Item]
    
    ScrollView {
       LazyVGrid(columns: [GridItem(.adaptive(minimum: 176))]) {
          ForEach(items) { item in
             ItemView(item: item)
          }
       }
    }
    
    Grids can have adaptive columns (like the example above), so in landscape there will be more columns.
    Otherwise it's possible to have a fixed number of columns:
    var items: [Item]
    
    ScrollView {
       LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) {
          ForEach(items) { item in
             ItemView(item: item)
          }
       }
    }
    
  • 10:45: There are lazy loading versions of the existing VStack and HStack:
    var rows: [ImageRow]
    
    ScrollView {
       LazyVStack(spacing: 2) {
          ForEach(rows) { row in
             switch row.content {
                case .singleImage(let image): SingleImageLayout(image: image)
                case .imageGroup(let images): ImageGroupLayout(images: images)
                case .imageRow(let images): ImageRowLayout(images: images)
             }
          }
       }
    }
    
    Note above the use of the new builder support for switch statements, allowing to easily alternate between different image layouts in the vertical stack.

Toolbars and controls

  • 12:25 - New toolbar modifier in SwiftUI:
    BookList()
       .toolbar {
          Button(action: recordProgress) {
             Label("Record Progress", systemImage: "book.circle")
          }
       }
    }
    
  • 12:40 - In the example above the button is automatically placed, but it's possible to specify its location with ToolbarItem:
    BookList()
       .toolbar {
          ToolbarItem(placement: .primaryAction) {
             Button(action: recordProgress) {
                Label("Record Progress", systemImage: "book.circle")
             }
          }
       }
    }
    
  • 12:50 - For example, cancellation and confirmation are semantic placements:
    CurrentProgressSheet()
       .toolbar {
          ToolbarItem(placement: .cancellationAction) {
             Button("Cancel", action: dismissSheet)
          }
          ToolbarItem(placement: .confirmationAction) {
             Button("Save", action: saveProgress)
          }
       }
    }
    
  • 13:00 - Principal placement is another example of semantic placement:
    ToolbarItem(placement: .principal) {
       Picker("View", selection: $viewMode) {
          Text("Details").tag(ViewMode.details)
          Text("Notes").tag(ViewMode.notes)
       }
       .pickerStyle(SegmentedPickerStyle())
    }
    
  • 13:20 - Placement in bottom toolbar:
    BookDetail() {
       .toolbar {
          ToolbarItem {
             Button(action: recordProgress) {
                Label("Progress", systemImage: "book.circle")
             }
          }
          ToolbarItem(placement: .bottomBar) {
             Button(action: shareBook) {
                Label("Share", systemImage: "square.and.arrow.up")
             }
          }
       }
    }
    
  • 13:40 - The new Label view is a combination of title and icon
    Label("Progress", systemImage: "book.circle")
    
    This is a convenient form of:
    Label {
       Text("Progress")
    } icon: {
       Image(systemName: "book.circle")
    }
    
    Note that the font and icon of a Label view both adapt automatically on the font size.
  • 15:30 - The new help modifier allows to add a description to what effect a control has:
    Button(action: recordProgress) {
       Label("Progress", systemImage: "book.circle")
    }
    .help("Record new progress entry")
    
  • 16:15 - Keyboard shortcut modifier:
    .commands {
       CommandGroup(before: .sidebar) {
          Button("Previous book", action: selectPrevious)
             .keyboardShortcut("[")
          Button("Next book", action: selectNext)
             .keyboardShortcut("]")
       }
    }
    
  • 16:35 - Keyboard shortcut can be also used for cancel and default actions:
    HStack {
       Button("Cancel", action: dismissSheet).keyboardShortcut(.cancelAction)
       Button("Save", action: saveLink).keyboardShortcut(.defaultAction)
    }
    
  • 17:15 - ProgressView is a new control:
    ProgressView("Downloading Photo", value: percentComplete)
    
    It's also available with a circular style:
    ProgressView("Downloading Photo", value: percentComplete)
       .progressViewStyle(CircularProgressViewStyle())
    
    Or spinning style:
    ProgressView()
    
  • 17:35 - Gauge is used to represent a value compared to a relative capacity:
    Gauge(value: acidity, in: 3...10) {
       Label("Soil Acidity", systemImage: "drop.fill")
          .foregroundColor(.green)
    }
    
  • 17:55 - It's possible to display the current level as a number too:
    Gauge(value: acidity, in: 3...10) {
       Label("Soil Acidity", systemImage: "drop.fill")
          .foregroundColor(.green)
    } currentValueLabel: {
       Text("\(acidity), specifier: "%1.f")")
    }
    
  • 18:05 - Gauges can also have minimum and maximum value labels:
    Gauge(value: acidity, in: 3...10) {
       Label("Soil Acidity", systemImage: "drop.fill")
          .foregroundColor(.green)
    } currentValueLabel: {
       Text("\(acidity), specifier: "%1.f")")
    } minimumValueLabel: {
       Text("3")
    } maximumValueLabel: {
       Text("10")
    }
    

New effects and styling

  • 19:00 - Here we have a grid of unselected albums and a row of selected albums:
    var albumGrid: some View {
       LazyVGrid(...) {
          ForEach(unselectedAlbums) { album in
             Button(action: { select(album) }) {
                AlbumCell(album)
             }
          }
       }
    }
    
    var selectedAlbums: some View {
       HStack {
          ForEach(selectedAlbums) { album in
             AlbumCell(album)
          }
       }
    }
    
    When selecting an album, I would like that the album gradually flows from the grid to the bottom row; it's very easy using the matchedGeometryEffect:
    @namespace private var namespace
    
    var albumGrid: some View {
       LazyVGrid(...) {
          ForEach(unselectedAlbums) { album in
             Button(action: { select(album) }) {
                AlbumCell(album)
             }
             .matchedGeometryEffect(id: album.id, in: namespace)
          }
       }
    }
    
    var selectedAlbums: some View {
       HStack {
          ForEach(selectedAlbums) { album in
             AlbumCell(album)
          }
          .matchedGeometryEffect(id: album.id, in: namespace)
       }
    }
    
  • 19:55 - ContainerRelativeShape is a new shape type that will take the shape of the container shape:
    struct AlbumWidgetView: View {
       var album: Album
       
       var body: some View {
          album.image
             .clipShape(ContainerRelativeShape())
             .padding()
       }
    }
    
    We can change the padding to minimize it to 1:
    struct AlbumWidgetView: View {
       var album: Album
       
       var body: some View {
          album.image
             .clipShape(ContainerRelativeShape())
             .padding(1)
       }
    }
    
  • 20:40 - Custom fonts will automatically scale with dynamic type changes, thanks to the ScaledMetric:
    @ScaledMetric private var padding: *CGFloat = 10.0
    
    VStack {
       Text(album.name)
          .font(.custom("AvenirNext-Regular", size: 30))
       Text("\(Image(systemImage: "music.mic")) \(album.artist)")
          .font(.custom("AvenirNext-Regular", size: 17))
    }
    .padding(padding)
    .background(...)
    
  • 21:50 - Possibility to customize the accent color for the whole app, but also possibility to customize it for each control of our app:
    List {
       Section {
          Label("Menu", systemImage: "list.bullet")
          Label("Favorites", systemImage: "heart")
             .listitemTint(.red)
          Label("Rewards", systemImage: "seal")
             .listitemTint(.purple)
       }
       Section(header: Text("Recipes")) { ... }
          .listitemTint(.monochrome)
    }
    
  • 22:50 - Switch tinting, for example to let it take the accent color instead of the default green:
    Toggle(isOn: $order.notifyWhenReady) {
       Text("Send notification when ready")
    }
    .toggleStyle(SwitchToggleStyle(tint: accentColor))
    

System integration

  • 23:20 - LinkView is a new view to display links in the app. They can be web pages or also universal links that open in other apps:
    Link(destination: appleURL) {
       Label("SwiftUI Tutorials", systemImage: "swift")
    }
    Link(destination: wwdcAnnouncementURL) {
       Label("WWDC 2020 Announcements", systemImge: "chevron.left.slash.chevron.right")
    }
    
    Links can also
  • 23:50 - Links also work within a widget, where they can link back to content in the main app.
  • 24:00 - If you want to open a link programmatically, there is the Environment.openUrl action:
    struct ContentView: View {
       @Environment(\.openUrl) private var openURL
       
       var body: some View {
          CustomContent()
             .onReceive(customPublisher) { output in
                if let url = output.requestedURL {
                   openURL(url)
                }
             }
       }
    }
    
  • 24:25 - SwiftUI gains support for drag & drop to/from other apps:
    AlbumCell(album)
       .onDrag {
          album.itemProvider
       }
    
    AlbumViewer()
       .onDrop(of [.album]) {
          // add album from incoming item provider
       }
    
  • 24:35 - New Uniform Type Identifiers framework for strong typing of items that are being dragged:
    import UniformTypeIdentifiers
    
    extension UTType {
       static int myFileFormat = UTType(exportedAs: "com.example.myfileformat")
    }
    
    Then it's possible to inspect and validate data that we have received:
    import UniformTypeIdentifiers
    
    let resoruceValues = try fileURL.resourceValues(forKeys: [.contentTypeKey])
    if let type = resoruceValues.contentType {
       let description = type.localizedDescription
       
       if type.conforms(to: .myFileFormat) {
          // The file is out app's format
       } else if type.conforms(to: .image) {
          // The file is an image
       }
    }
    
  • 25:20 - Sign in with Apple:
    import AuthenticationServices
    import SwiftUI
    
    SignInWithAppleButton(
       .signUp,
       onRequest: handleRequest
       onCompletion: handleCompletion
    )
    .signInWithAppleButtonStyle(.black)
    
    func onRequest(request: ASAuthorizationAppleIDRequest) {
    
    }
    
    func OnCompletion(result: Result<ASAuthorization>, Error>) {
    
    }
    
  • 25:50 - New frameworks with SwiftUI views and modifiers:
    • AuthenticationServices
    • AVKit
    • MapKit
    • SceneKit
    • SpriteKit
    • QuickLook
    • HomeKit
    • ClockKit
    • StoreKit
    • WatchKit

App essentials in SwiftUI

  • 0:40 - This year SwiftUI is extended to add Scenes and Apps, allowing to build a full app exclusively in SwiftUI.
  • 1:20 - A scene is a distinct region of the screen. A window is the most common case. iPadOS and MacOS can show multiple scenes on the screen at the same time (on MacOS the scenes can be multiple windows, or can be grouped in multiple tabs in a parent scene).
  • 3:00 - The collection of scenes makes an app.
  • 4:00 - A basic app in SwiftUI:
    @main
    struct BookClubApp: App {
       @StateObject private var store = ReadingListStore()
       
       var body: some Scene {
          WindowGroup {
             ReadingListViewer(store: store)
          }
       }
    }
    
    struct ReadingListViewer: View {
       @ObservedObject var store: ReadingListStore
       
       var body: some View {
          NavigationView {
             ...
          }
       }
    }
    
  • 5:40 - StateObject is used to identify that I own an object, while ObservedObject is an object that I observe, but I am not the owner.
  • 7:15 - WindowGroup automatically offer the possibility to open the app multiple times on iPadOS. The app can provider a shared model to be used by multiple scenes, but then the state in these scenes can be independent.
  • 8:30 - Also in app switcher, for each window there is the name of the app and also the name of the window. This is done via a new view modifier, navigationTitle, that affects the owning scene.
  • 12:10 - SceneStorage is a new property wrapper can be used to persist the view state.
  • 13:00 - DocumentGroup is another scene type:
    @main
    struct ShapeEditApp: App {
       var body: some Scene {
          DocumentGroup(newDocument: SketchDocument()) { file in
             DocumentView(file.$document)
          }
       }
    }
    
  • 13:35 - There is the possibility to add a Preferences window:
    @main
    struct BookClubApp: App {
       @StateObject private var store = ReadlingListStore()
       
       @SceneBuilder var body: some Scene {
          WindowGroup {
             ReadingListViewer(store: store)
          }
       
       #if os(macOS)
          Settings {
             BookClubSettingsView()
          }
       #endif
       }
    }
    
  • 14:00 - New commands modifier:
    @main
    struct BookClubApp: App {
       @StateObject private var store = ReadlingListStore()
       
       @SceneBuilder var body: some Scene {
          WindowGroup {
             ReadingListViewer(store: store)
          }
          .commands {
             BookComamnds()
          }
       
       #if os(macOS)
          Settings {
             BookClubSettingsView()
          }
       #endif
       }
    }
    
    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() }
    }
    

Build document-based apps in SwiftUI

  • 0:20 - Document-based apps open, edit and save documents from the finder in MacOS and the Files app in iPadOS and iOS. This is different from importing the document inside the app. In fact, changes are saved back and are available to other apps.
  • 2:05 - Adding support for document is done by using another scene type, called DocumentGroup:
    @main
    struct TextEdit: App {
       var body: some Scene {
          DocumentGroup(newDocument: TextDocument()) { file in
             TextEditor(text: file.$document.text)
          }
       }
    }
    
  • 2:40 - It's possible to have multiple scene groups in a single application, for example having multiple DocumentGroup for different document types, or a DocumentGroup and a WindowGroup.
  • 5:40 - In the info.plist, in the Document Types section, you can specify the type of document (identifier) your application will handle. As we save the data as binary, it will conform to public.data, public.content. Provide also an extension (without dot).
  • 7:40 - ShapeEditDocument is a value type that represents the document on disk. readableContentTypes is an array of UTType and specifies what documents our application will be able to open.
  • 8:20 - In the UTType extension, replace the existing declaration with shapeEditDocument using the same string used in the info.plist (as exportedAs).
  • 9:15 - Code the init method, for example using JSONDecoder - so in this case our type must conform to Codable. In the same way, we rewrite write using a JSONEncoder.

Stacks, Grids, and Outlines in SwiftUI

  • 2:10 - Here is the data model used for the following samples:
    struct Sandwich: Indetifiable {
       var id = UUID()
       var name: String
       var rating: Int
       var heroImage: Image { ... }
    }
    
    struct HeroView: View {
       var sandwich: Sandwich
       
       var body: some View {
          sandwich.heroImage
             .resizable()
             .aspectRatio(contentMode: .fit)
             .overlay(BannerView(sandwich: sandwich))
       }
    }
    
  • 2:30 - Here is the BannerView used above:
    struct BannerView: View {
       var sandwich: Sandwich
       
       var body: some View {
          VStack(alignment: .leading, spacing: 10) {
             Spacer()
             TitleView(title: sandwich.name)
             RatingView(rating: sandwich.rating)
          }
          .padding(...)
          .background(...)
       }
    }
    
  • 2:35 - The star rating is just an horizontal stack of images:
    struct RatingView: View {
       var rating: Int
       
       var body: some View {
          HStack {
             ForEach(0..<5) { startIndex in
                StartImage(isFilled: rating > starIndex)
             }
             Spacer()
          }
       }
    }
    
  • 2:40 - Initial implementation with a gallery of stacked views. Note the ForEach and the ScrollView (stacks don't automatically scroll by their own).
    let sandwiches: [Sandwich] = ...
    
    ScrollView {
       VStack(spacing: 0) {
          ForEach(sandwiches) { sandwich in
             HeroView(sandwich: sandwich)
          }
       }
    }
    
  • 3:00 - This approach has one disadvantage: with many photos, we need to wait that the control load many photos, so it will become even more slow. We need a lazy stack that builds incrementally, as the user scrolls down and it becomes visible.
    All we need to do is to replace the VStack with the LazyVStack (there is also a LazyHStack):
    let sandwiches: [Sandwich] = ...
    
    ScrollView {
       LazyVStack(spacing: 0) {
          ForEach(sandwiches) { sandwich in
             HeroView(sandwich: sandwich)
          }
       }
    }
    
  • 5:00 - As a rule, if unsure about what type of stack to use, use HStack and VStack. Use the lazy versions only after you find bottlenecks with profiling and instruments.
  • 5:15 - When moving to an iPad, I don't want simply a huge list of images, but rather I would prefer multiple columns. We have two new types: LazyVGrid and LazyHGrid:
    let sandwiches: [Sandwich] = ...
    
    var columns = [
       GridItem(spacing: 0),
       GridItem(spacing: 0),
       GridItem(spacing: 0)
    }
    
    ScrollView {
       LazyVGrid(columns: columns, spacing: 0) {
          ForEach(sandwiches) { sandwich in
             HeroView(sandwich: sandwich)
          }
       }
    }
    
  • 7:10 - We can also have a variable number of columns, based on the available space. These are great for landspace mode, or MacOS, where windows can be resized:
    let sandwiches: [Sandwich] = ...
    
    var columns = [
       GridItem(.adaptive(minimum: 300), spacing: 0)
    }
    
    ScrollView {
       LazyVGrid(columns: columns, spacing: 0) {
          ForEach(sandwiches) { sandwich in
             HeroView(sandwich: sandwich)
          }
       }
    }
    
  • 8:00 - Lists are always lazy loaded:
    struct graphics: [Graphic]
    
    var body: some View {
       List(
          graphics
       ) { graphic in
          GraphicRow(graphic)
       }
       .listStyle(SidebarListStyle())
    }
    
  • 8:45 - Outlines are nested lists, and we simply need to tell SwiftUI what are the children of a given element, passing the children key path:
    struct graphics: [Graphic]
    
    var body: some View {
       List(
          graphics,
          children: \.children
       ) { graphic in
          GraphicRow(graphic)
       }
       .listStyle(SidebarListStyle())
    }
    
  • 9:50 - More complex outlines can have custom outlines. OutlineGroup is like a List, but instead iterating over a list, it traverses tree structures:
    List {
       ForEach(canvases) { canvas in
          Section(header: Text(canvas.name)) {
             OutlineGroup(canvas.graphic, children: \.children) { graphic in
                GraphicRow(graphic)
             }
          }
       }
    }
    
  • 13:10 - DisclosureGroups are used for progressive display of information. They provide a disclosure indicator, a label and a content. When they tap the disclosure indicator, the content is revealed (and if tapped again, the content is hidden):
    @State private var areFillControlsShowing = true
    
    Form {
       DisclosureGroup(isExpanded: $areFillControlsShowing) {
          Toggle("Fill shape?", isOn: isFilled)
          ColorRow("Fill color", color: fillColor)
       } label: {
          Label("Fill", systemImage: "rectangle.3.offgrid.fill")
       }
    }
    
    Note the label closure that allows to use any view as label, in this case a label with an icon.
  • 16:50 - An OutlineGroup is expanded by SwiftUI into a ForEach (on the same initial collection), whose body is a DisclosureGroup. The label is generated by the single element on the original collection, and the content is another OutlineGroup, pointing to the children of the single element.
    This continues until we don't find an element without children. But as this calculation is done only when opening a group, the amount of required computation is reduced to the minimum.

Data Essentials in SwiftUI

  • 1:20 - When starting a new app, you must ask yourself these three questions about the data in a view:
    1. What data does this view need to do its job?
    2. How will the view manipulate that data? <- if it doesn't change, use let properties
    3. Where will the data come from? <- this is the source of truth
  • 4:00 - When I have multiple variables holding the state of a view, it's better to encapsulate them in a struct. Any change to to any property will invoke a change to the full struct:
    struct EditorConfig {
       var isEditorPresent = false
       var note = ""
       var progress: Double = 0
       mutating func present(initialProgress: Double) {
          progress = initialProgress
          note = ""
          isEditorPresent = true
       }
    }
    
    struct BookView: View {
       @State private var editorConfig = EditorConfig()
       
       func presentEditor() { editorConfig.present(...) }
       
       var body: some View {
          ...
          Button(action: presentEditor) { ... }
          ...
       }
    }
    
  • 5:15 - State is the modified for local source of truth. Note that SwiftUI doesn't destroy this variable when the view is dismissed, but it keeps it for the next time.
  • 6:00 - When modifying data, we declare it with var. But it is a value type, any changes of the data in the subview would create a new copy of the data. We also can't use @State, otherwise we would create a new source of truth. The solution is to use @Binding, to share a read/write reference between different views (note the use of the dollar sign when passing the reference to the subview):
    struct BookView: View {
       @State private var editorConfig = EditorConfig()
       
       var body: some View {
          ...
          ProgressEditor(editorConfig: $editorConfig)
          ...
       }
    }
    
    struct ProgressEditor: View {
       @Binding var editorConfig: EditorConfig
    }
    
  • 9:00 - Here are the things to use so far:
    • Properties for data that isn't mutated by the view;
    • @State for transient data owned by the view;
    • @Binding for mutating data owned by another view.
  • 9.55 - Typically in a SwiftUI app you manage your model out of your UI, handling persisting and syncing it. In this case you should use @ObservableObject, that can be applied only to objects (so reference types).
    ObservableObject is a new source of truth.
  • 12:05 - You can have a single ObservableObject that models all of your data, or you can have multiple ObservableObjects.
  • 13:15 - In our example, we have a class conforming to ObservableObject:
    class CurrentlyReading: ObservableObject {
       let book: Book
       @Published var progress: ReadingProgress
    }
    
    struct ReadingProgress {
       struct Entry: Identifiable {
          let id: UUID
          let progress: Double
          let time: Date
          let note: String?
       }
       
       var entries: [Entry]
    }
    
  • 13:15 - @Published automatically works with ObservableObject, and publishes every time the value changes. This is so very easy, to keep your data in sync with your view.
  • 14:55 - How to create an ObservableObject dependency:
    • @ObservedObject: if tracks the ObservableObject as a dependency and doesn't own the instance; derive a binding using the dollar prefix;
    • @StateObject: here SwiftUI owns the ObservableObject, so creation and destruction is tied to the view's life cycle; the object is instantiated just before the body;
    • @EnvironmentObject is used when you want to pass your object to subviews and the intermediate views don't need the data. You use the view modifier in the parent object and the property wrapper in the children views.
  • 25:00 - How to avoid slow updates:
    • Make view initialization cheap;
    • Make body a pure function, free of side effects;
    • Avoid assumptions.
  • 26:35 - Example of a bug that impacts performances:
    struct ReadingListViewer: View {
       var body: some View {
          NavigationView {
             ReadingList()
             Placeholder()
          }
       }
    }
    
    struct ReadingList: View {
       @ObservedObject var store = ReadingListStore()
       
       var body: some View {
          ...
       }
    }
    
    In this case, we create a store at each instantiation of the view, so this affect performances and is also wrong: it causes a data loss because the object is recreated every time.
    How to fix it? in the past, you had to create the model outside and pass it, but now you can use @StateObject:
    struct ReadingList: View {
      @StateObject var store = ReadingListStore()
      
      var body: some View {
         ...
      }
    }
    
  • 27:50 - Event sources: note that these closures are executed in the main thread, so eventually move the code to a background queue:
    • onReceive
    • onChange
    • onOpenURL
    • onContinueUserActivity
  • 29:10 - Who owns the data?
    • Lift data to a common ancestor;
    • Leverage @StateObject;
    • Consider placing global data in App (app-wide source of truth).
  • 31:40 - Source of truth lifetime:
    • Process Lifetime:
      • State
      • StateObject
      • Constant
    • Extended Lifetime (these are lightweight storage used in conjunction with your model):
      • @SceneStorage: this is used in scenes and views and is a scene-wide source of truth;
      • @AppStorage: this can be used everywhere and is an app-wide source of truth;
    • Custom Lifetime:
      • ObservableObject.
  • 32:50 - Example of SceneStorage:
    struct ReadingListViewer: View {
       @SceneStorage("selection") var selection: String?
       
       var body: some View {
          NavigationView {
             ReadingList(selection: $selection)
             BookDetailPlaceholder()
          }
       }
    }
    
  • 33:40 - Example of AppStorage, for settings:
    struct BookClubSettings: View {
       @AppStorage("updateArtwork") private var updateArtwork = true
       @AppStorage("syncProgress") private var syncProgress = true
       
       var body: some View {
          Form {
             Toggle(isOn: $updateArtwork) {
                // ...
             }
             
             Toggle(isOn: $syncProgress) {
                // ...
             }
          }
       }
    }
    

Visually edit SwiftUI views

  • No technical notes here

Structure your app for SwiftUI previews

  • 1:45 - Previewing multiple files at once:
    struct LayoutSelectorView_Previews: PreviewProvider {
       static var previews: some View {
          Group {
             LayoutSelectorView(
                selectedLayout: .constant(.twoByTwo)
             )
             .previewLayout(.sizeThatFits)
             .padding(50)
             
             LayoutSelectorView(
                selectedLayout: .constant(.twoByTwo)
             )
             .previewLayout(.sizeThatFits)
             .padding(50)
             .background(
                Image("SampleBackground")
                   .resizable()
                   .aspectRatio(contentMode: .fill)
          }
       }
    }
    
  • 2:30 - It's possible to pin a preview pressing a button in the bottom bar of the previewer (first button on the left), so even when switching the active view, the previous preview will remain pinned and so visible.
  • 3:00 - To duplicate a preview, press the "+" button on the toolbar of the single preview. For example then it's possible to open the inspector (top right button) and change the scheme to Dark.
  • 3:25 - To modify a color for white/dark backgrounds, the recommended way is to use an asset in the asset catalog. Then it's possible to invoke it with the "+" button, colors tab, and then choose the available color name. In the asset catalog, it's still possible to keep the pinned previews, and modify the color for dark mode.
  • 6:10 - Previews should handle simple data; if the app requires heavy initialization data, that should be handled separately.
  • 7:20 - It's possible to attach the debugger to a preview, pressing the debug button on the toolbar of the preview. So with the debugger I could see that the app is doing a lot of CPU work and using a lot of network.
    So in this case we add the @StateObject property, so it will be initialized only at the first run (and on changes on the data model).
  • 9:50 - In case you need assets only for debugging, you can leverage Xcode development assets (in the project properties, General tab, Development Assets). The app comes preconfigured with a development asset path, but it's possible to add your own.
  • 11:00 - The development content can also contain Swift files, that are added only in Debug builds.
  • 14:10 - Structuring views for the preview requires to think about the minimum amount of data that you want to transfer from the model to the view. In particular you don't want to be bound to the database.
  • 15:25 - Sometimes you pass a too simple data type, for example to adapt the displayed strings to the locale used in the app. For example, instead of passing a string, we can pass a PersonNameComponents, that we can format in this way:
    let name: PersonNameComponents
    
    let formatter = PersonNameComponentsFormatter()
    formatter.style = .long
    
    let nameLabel = Text("\(name, formatter: formatter)")
    
  • 17:00 - When the view can change a value, pass a mutable data type, via a @Binding. Then in the previews you can create the binding simply with .constant.
  • 18:20 - If I want to the the changing of a binding, I create an intermediate view that store and passes the value of the binding:
    struct Inspector: View {
       @Binding var effects: SlotEffects
       
       var body: some View { ... }
    }
    
    struct Inspector_Previews: PreviewProvider {
       struct EffectsContainer: View {
          @State var effects: SlotEffects
          
          var body: some View {
             Inspector(effects: $effects)
          }
       }
       
       static var previews: some View {
          Group {
             Inspector(effects: .constant(SlotEffects())
             EffectsContainer(effects: SlotEffects())
          }
          .previewLayout(.sizeThatFits)
          .padding(20)
       }
    }
    
  • 19:10 - Passing a closure that is called when a button is pressed:
    struct Inspector: View {
       @Binding var effects: SlotEffects
       var replacePhotoHandler: () -> ()
       
       var body: some View {
          VStack {
             GroupBox {
                Button(action: replacePhotoHandler) {
                   Text("Replace Photo")
                }
                .frame(maxWidth: .infinity)
             }
             
             ...
          }
       }
    }
    
    struct Inspector_Previews: PreviewProvider {
       struct EffectsContainer: View {
          @State var effects: SlotEffects
          
          var body: some View {
             Inspector(effects: $effects) {
                effects.saturation -= 0.1
             }
          }
       }
       
       static var previews: some View {
          Group {
             Inspector(effects: .constant(SlotEffects()) { }
             EffectsContainer(effects: SlotEffects())
          }
          .previewLayout(.sizeThatFits)
          .padding(20)
       }
    }
    
  • 20:05 - It's also possile to run previews on a physical phone: from the preview, click on the device button, and then your phone. As you apply changes, they are seamless applied to the device. There is a new icon, "Xcode Previews", that runs the last preview even when the phone is disconnected.
  • 21:15 - In other cases, passing the required data can still be difficult, even with bindings. So we can create a protocol, to represent the minimum amount of data that we want to present:
    protocol CollageProtocol: ObservableObject, Identifiable {
       associatedtype SlotType: SlotProtocol
       
       var name: String { get set }
       var layout: CollageLayout { get set }
       var slots: [SlotAddress: SlotType] { get set }
    }
    
    protocol SlotProtocol: ObjservableObject {
       var imagePublisher: AnyPublisher<Image, ImageLoadError> { get }
       
       var effects: SlotEffect { get set }
    }
    
    So with this protocol we can now create a design-time version of our model, very easy to create:
    struct CollageEditor<
       CollageType: CollageProtocol,
       ImagePickerType: View
    >: View { ... }
    
    struct CollageEditor_Preview: PreviewProvider {
       static var previews: some View {
          CollageEditor(
             collage: DesignTimeCollage(
                name: "My Collage",
                layout: .twoBytwo,
                slots: [SlotAddress(major: 0, minor: 0):
                   DesignTimeImageSlot(
                      imagePublisher: Image("Barcelona_01").constantPublisher,
                      effects: SlotEffects()
                   )
                ),
             makePhotoPicker: DesignTimePhotoPicker.makePicker
          ).padding()
       }
    }
    
  • 27:50 - When you need to pass Environment variables, you need to provide a value for each preview:
    struct CloudSyncStatusView: View {
       @EnvironmentObject
       var status: CloudSyncStatus
       
       var body: some View { ... }
    }
    
    extension CloudSyncStatus.Status { ... }
    
    struct CloudSyncStatusView_Previews: PreviewProvider {
       static var previews: some View {
          Group {
             CloudSyncStatusView()
                .environmentObject(CloudSyncStatus())
             CloudSyncStatusView()
                .environmentObject(CloudSyncStatus(
                   currentStatus: .offline,
                   lastSync: Date() - 10000.0))
          }.previewLayout(.sizeThatFits)
       }
    }
    

Add custom views and modifiers to the Xcode Library

  • 0:45 - Benefits of the Xcode library:
    • discoverability;
    • learning;
    • visual editing.
  • 1:45 - The most logical place to indicate how to export to the Xcode library is next to the code itself. Your view must conform to the LibraryContentProvider protocol:
    public protocol LibraryContentProvider {
       @LibraryContentBuilder
       var views: [LibraryItem] { get }
       
       @LibraryContentBuilder
       func modifiers(base: ModifierBase) -> [LibraryItem]
    }
    
  • 2:25 - Here is a minimal sample of LibraryItem:
    LibraryItem {
       SmoothieRowView(smoothie: .lemonberry),
       visible: true,
       title: "Smoothie Row View",
       category: .control
    }
    
  • 3:30 - Then we add to our code an implementation of LibraryContentProvider:
    struct LibraryContent: LibraryContentProvider {
       @LibraryContentBuilder
       var views: [LibraryItem] {
          LibraryItem(
             SmoothieRowView(smoothie: .lemonberry)
          )
       }
    }
    
  • 4:25 - Now it will be possible to search for "Smoothie Row View" in the Xcode library.
  • 6:30 - It's possible to specify a category:
    struct LibraryContent: LibraryContentProvider {
       @LibraryContentBuilder
       var views: [LibraryItem] {
          LibraryItem(
             SmoothieRowView(smoothie: .lemonberry),
             category: .control
          )
       }
    }
    
  • 7:15 - It's perfectly possible to create different library items, for the same view, but in different configurations:
    struct LibraryContent: LibraryContentProvider {
       @LibraryContentBuilder
       var views: [LibraryItem] {
          LibraryItem(
             SmoothieRowView(smoothie: .lemonberry),
             category: .control
          )
          
          LibraryItem(
             SmoothieRowView(smoothie: .lemonberry, showNearbyPopularity: true),
             category: .control,
             title: "Smoothie Row View With Popularity"
          )
       }
    }
    
  • 9:00 - Sample modifier for images:
    extension Image {
       func resizedToFill(width: CGFloat, height: CGFloat) -> some View {
          self.
             .resizable()
             .aspectRatio(contentMode: .fill)
             .frame(width: width, height: height)
       }
    }
    
  • 9:20 - To add this modifier to the Xcode library, I add it:
    struct LibraryContent: LibraryContentProvider {
       @LibraryContentBuilder
       var views: [LibraryItem] {
          LibraryItem(
             SmoothieRowView(smoothie: .lemonberry),
             category: .control
          )
          
          LibraryItem(
             SmoothieRowView(smoothie: .lemonberry, showNearbyPopularity: true),
             category: .control,
             title: "Smoothie Row View With Popularity"
          )
       }
       
       @LibraryContentBuilder
       func modifiers(base: Image) -> [LibraryItem] {
          LibraryItem(
             base.resizedToFill(width: 100.0, height: 100.0)
          )
       }
    }
    
  • 10:50 - In all this demo, we never did compile our code. Even if the project is not in a runnable state, it can still contribute to the Xcode library. Also the compiler will strip it in a distribution build. This also works very well with Swift Packages.

Create Swift Playgrounds content for iPad and Mac

  • 0:45 - There are differences between iPad and Mac: for example code completion.
  • 1:50 - How content can be customized for iPad and Mac:
    • supportedDevices (values: "iPad" and "Mac"), to be added to Manifest.plist and feed.json;
    • requiredCapabilities (values from UIRequiredDeviceCapabilities, that includes things like arkit, microphone and wifi), to be added to Manifest.plist and feed.json.
  • 3:25 - To differentiate in code between iPad and Mac running platform-specific code:
    #if targetEnvironment(macCatalyst)
    // Call this code on Mac.
    showTurtleInAR()
    #else
    // Call this code in iPad.
    showTurtleOnScreen()
    #endif
    
  • 3:45 - How content can conform to device settings: system colors, accent colors, dark mode (for example, use system background instead of white). It's also possible to add additional resources in the Asset Catalog.
  • 4:20 - Use safe are layout guides with safeAreaLayoutGuide instead of liveViewSafeAreaGuide.

Build a SwiftUI view in Swift Playgrounds

  • 1:45 - To be able to bring your playground to Xcode, create directly it choosing "View More", scroll all right, and choose "Xcode Playground". Then it's better to rename it to something more meaningful.
  • 2:30 - Code needed to start displaying the first view:
    import SwiftUI
    import PlaygroundSupport
    
    struct ProgressView: View {
       var body: some View {
          Text("Hello, world!")
       }
    }
    
    PlaygroundPage.current.setLive(ProgressView())
    
  • 3:55 - Creating the circle:
    import SwiftUI
    import PlaygroundSupport
    
    struct ProgressView: View {
       var body: some View {
          Circle()
             .stroke(lineWidth: 40)
             .foregroundColor(.blue)
       }
    }
    
    PlaygroundPage.current.setLive(ProgressView()
       .padding(150))
    
  • 5:30 - Adding the text:
    import SwiftUI
    import PlaygroundSupport
    
    struct ProgressView: View {
       var body: some View {
          ZStack {
             Circle()
                .stroke(lineWidth: 40)
                .foregroundColor(.blue)
             Text("25%")
          }
       }
    }
    
    PlaygroundPage.current.setLive(ProgressView()
       .padding(150))
    
  • 7:15 - It's possible to add colors with a color picker.
  • 8:15 - It's possible to add a new file tapping on the New File icon in the top left corner, and giving a name for the new file. When moving to a new file, remember to import SwiftUI, and also make the struct public (body and init must be declared public as well).
  • 10:05 - How to add a new view acting as a preview:
    import SwiftUI
    import PlaygroundSupport
    
    struct Preview: View {
       var body: some View {
          VStack(spacing: 30) {
             ProgressView()
             ProgressView()
                .environment(\.colorScheme, .dark)
          }
          .padding(100)
          .background(Color(UIColor.secondarySystemBackground))
       }
    }
    
    PlaygroundPage.current.setLive(Preview())
    
  • 12:15 - It's possible now to create a new @State variable called progress and pass it to the two views. Then create a new method increment that increases the progress by 25%; also wrap it in withAnimation. Finally add a button that invokes this method once pressed.

What's New in Swift

  • 1:45 - Code size has decreased from 2.3 times (Swift 4) to below 1.5 (Swift 5.3) times compared to Objective-C. Some more code size is inevitable, because Swift has more safety features.
    With SwiftUI, using Xcode 12 instead of Xcode 11 brings a 43% code size reduction.
  • 3:40 - Dirty memory is the memory that is written during program execution (compared to clean memory, that is the static code that is run, and so can be purged, because it is easily reloadable).
    Swift is already more compact than Objective-C, because of use of more value types. The overhead is less than one third of the previous case, if you target iOS 14.
  • 7:00 - Swift is now lower in system stack than Foundation.
  • 7:15 - Diagnostics: errors and warning have vastly improved in this release, with errors pointing to real code issues, and providing real help to fix the issues.
  • 8:45 - Code Completion for Swift has dramatically improved, with better type-checking inference, and also with dynamic aspects of the language, for example with key paths. Also it is 15 times faster, compared to Xcode 11.5.
  • 10:05 - Code Indentation has also improved in many situations.
  • 11:00 - Debugging shows better runtime error messages, and is more robust.
  • 12:40 - Swift officially supports Apple platforms, plus Ubuntu, CentOS and Amazon Linux 2. Support for Windows is coming soon. It also supports Amazon AWS Lambda, thanks for the open-source runtime.
  • 14:20 - The Swift language is open-source, and each improvement is related to a feature improvement.
  • 15:00 - SE-0279: Multiple trailing closures syntax.
    It allows now to specify more than one closures after a method. It allows to convert from:
    UIView.animate(withDuration: 0.3) {
       self.view.alpha = 0
    }
    
    UIView.animate(withDuration: 0.3, animations: {
       self.view.alpha = 0
    }) { _ in
       self.view.removeFromSuperview()
    }
    

to:

UIView.animate(withDuration: 0.3) {
   self.view.alpha = 0
} completion: { _ in
   self.view.removeFromSuperview()
}
  • 16:50 - This comes useful in SwiftUI, for example:
    Gauge(value: acidity, in: 3...10) {
       Label("Soil Acidity", systemImage: "drop.fill")
          .foregroundColor(.green)
    } currentValueLable: {
       Text("\(acidity, specifier: "%.1f")")
    } minimumValueLabel: {
       Text("3")
    } maximumValueLable: {
       Text("10")
    }
    
    Note: it's important to clarify the meaning of the first closure, because its label will be dropped from the call expression.
  • SE-0249: KeyPath expressions as functions.
    You can pass a KeyPath argument to any matching function:
    extension Collection {
       func chunked<Key>(by keyForValue: @escaping (Element) -> Key) -> Chunked<Self, Key>
    }
    
  • 20:10 - SE-0281 @main.
    It was already possible to use the UIApplicationMain attribute to tell Swift to generate an implicit Main function.
    Now it's enough to declare a @main attribute to tag that type and tell the compiler to generate an implicitly main function.
  • 21:45 - SE-0269: Increased availability of implicit self on closures.
    It was already possible to skip self in escaping closures. Still you needed to explicitly tell that you want to capture self in your declaration.
    Now it's finally to skip completely the self declaration.
  • 23:00 - SE-0276: Multi-pattern catch clauses.
    Catch clauses now have the full power of switch statements.
  • 23:30 - Enum Enhancements - Since Swift 4, the compiler has been able to synthesize Equatable and Hashable conformance to enums. Now in Swift 5.3 the compiler is able to synthesize also Comparable conformance to enum types.
  • 24:40 - Embedded DSL Enhancements: before it was possible to use if, now it's possible to use if let and switch.
  • 25:15 - In SwiftUI now it's possible to remove @SceneBuilder, because it's detected from the Protocol requirements.
  • 26:00 - Float16 uses only two bytes in memory. It has limited small precision and range.
  • 27:05 - Apple Archive is a modular archive format providing fast, multithreaded compression. It comes with command-line tool and Finder integration:
    import AppleArchive
    
    try ArchiveByteStream.withFileStream {
       path: "/tmp/VacationPhotos.aar",
       mode: .writeOnly,
       options: [.create, .truncate],
       permissions: [.ownerReadWrite, .groupRead, .otherRead]
    ) { file in
       try ArchiveByteStream.withCompressionStream(using: .lzfse, writingTo: file) { compressor in
          try ArchiveStream.withEncodeStream(writingTo: compressor) { encoder in
             try encoder.writeDirectoryContents(archiveFrom: source, keySet: fieldKeySet)
          }
       }
    }
    
  • 28:10 - OSLog: high-performance, privacy-sensitive logging.
  • 29:00 - Packages for numeric computing, argument parser
  • 30:45 - Swift Standard Library Preview offers early access to new Swift feature library.

Explore numerical computing in Swift

  • 0:20 - The Swift Numerics Package is an open source library available in GitHub, with the purpose of providing building blocks for generic numerical computing in Swift. So, instead of coding something specifically for Double, for example, now it can be generic.
  • 2:25 - Importing numerics, now you have access to the Real protocol and so can write generic floating-point code:
    import numerics
    
    func logit<NumberType: Real>(_ p: NumberType) -> NumberType {
       .log(p) - .log1p(-p)
    }
    
    Note that in front of the log() and log1p() functions there is one dot, to use their generic version.
  • 3:20 - This is possible thanks to the Real protocol, that is part of the numerics package and provides generic access to all standard floating-point capabilities.
  • 4:25 - Let's review the protocols already in the Swift standard library:
  • 5:25 - The numerics package builds on these concepts and adds:
    • AlgebraicField, based on SignedNumeric, to add / and the reciprocal;
    • ElementaryFunctions are a large collection of common mathematical functions (like cos, sin, tan, exp, log, pow, root, expMinusOne, log);
    • RealFunctions extend this with even further, with less used functions (atan2, cos, sin, tan, erf, hypot, gamma, logGamma);
  • 6:10 - The Real protocol combines FloatingPoint, AlgebraicField and RealFunctions.
  • 6:50 - Generics constrained to Real support all standard floating-point types, reducing code duplications and simplifying maintenance.
  • 7:05 - The Complex type is a fully featured implementation:
    import numerics
    
    let z = Complex(1.0, 2.0) // z = 1 + 2i
    
    This works because both 1.0 and 2.0 are Double, otherwise it would have been possible to write:
    import numerics
    
    let z = Complex<Double>(1.0, 2.0) // z = 1 + 2i
    
  • 7:40 - So it clear how the Complex type is also an example of usage of the Real protocol.
  • 9:00 - The structure in memory of the Complex type is of two values, and it can be exchanged with C and C++ Complex types. For example:
    import numerics
    import Accelerate
    
    let z = (0..<100).map {
       Complex(length: 1.0, phase: Double.random(in: -.pi ... .pi))
    }
    
    let norm = cblas_dznrm2(z.count, &z, 1)
    
  • 9:55 - Treatment of infinity values can be different from C and C++. This is for performance reasons, that make multiplications and divisions much faster.
  • 11:30 - Float16 is already available on ARM and there are discussion for support on Intel.
  • 12:30 - Float16 is very fast and interoperable with __fp16 type, but it has low precision and small range.
  • 13:30 - Float16 is already supported on Apple GPUs, on Apple CPUs with A11 Bionic, and simulated (using Float) on older hardware.

Explore logging in Swift

  • 2:25 - New logging API in Xcode 12 to record events as they happen; the logs are archived on the device for later retrieval; the new APIs have low performance overhead.
  • 2:45 - How to add logging to an app:
    import os
    
    let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards")
    
    func beginTask(url: URL, handler: (Data) -> Void) {
       launchTask(with: url) {
          handler($0)
       }
       logger.log("Started a task \(taskID)")
    }
    
  • 4:00 - The compilers handles log message differently, to optimize performances. For example, it doesn't convert to strings, because it is slow. There are built-in data types natively supported for logging:
    • native types, like Int and Double;
    • Objective-C objects with -description;
    • any type conforming to CustomStringConvertible.
  • 4:45 - Non-numeric values are redacted by default, i.e. they don't show any personal information. Data can be made visible in the following way:
    logger.log ("Paid with bank account \(accountNumber)")  // <private>
    
    logger.log("Ordered smoothie \(smoothieName, privacy: .public)")
    
  • 5:20 - When you app logs some messages, they are saved on the device in a compressed form. You can use the log collect command on your Mac to retrieve those logs:
    • connect your device to your Mac;
    • run log collect --device --start '2020-06-22 9:41:00' --output fruta.logarchive;
    • double click on the generated file;
    • filter by subsystem, in the case the bundle identifier of my app;
    • filter additionally by the task identifier.
  • 8:45 - It is also possible to stream logs while the app is still running, if you device is connected to your Mac. You can see them in the Console app, or from Xcode when launched with Product | Run.
  • 9:15 - There are 5 log levels:
    • Debug: useful only during debugging;
    • Info: helpful but not essential for troubleshooting;
    • Notice (default): essential for troubleshooting;
    • Error: error seen during execution;
    • Fault: bug in program.
      The Logger type has method to log with each log level.
  • 10:30 - Message persistence is also important: only persisted messages can be retrieved after execution (the others can only be streamed while the app is running). Persistence is also determined by the log levels:
    • Debug: not persisted;
    • Info: persisted only during log collect;
    • Notice, Error, Fault: persisted up to a storage limit.
  • 11.30 - Log levels also affect performance: the levels that are less important are faster. The Fault level is the slowest and the Debug level is the fastest.
    Also the compiler optimizes away debug messages except while streaming:
    logger.debug("\(slowFunction(data))")
    
    It is safe to call slow calls at Debug level.
  • 12:15 - Format data to improve readability, at no runtime cost:
    statisticsLogger.log("\(taskID) \(giftCardID, align: .left(columns: GiftCard.maxIDLength)) \(serverID) \(seconds, format(precision: 2))")
    
  • 14:50 - It's possible to format using optional parameters:
    logger.log("\(data, format: .hex, align: .right(columns: width))")
    
  • 15:30 - Messages are logged even when your app is deployed, and logs can be seen with physical access to the device. So, personal information must not be marked .public.
    It's possible to redact data with equality-preserving hash:
    logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")
    
    This doesn't reveal the data, but still it allows to know when two values are the same (useful for example when filtering logs).
  • 16:25 - The logger APIs are available starting with iOS 14; previous releases can use os_log(), that accepts format strings.