MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 20

App ClipsAppKitExtensionsFoundationLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 14WWDC 16WWDC 18WWDC 19WWDC 20

View as index

Explore logging in Swift

Categories: Foundation, Logging 0 comments Watch the video

Xcode 12 introduces new APIs for the unified logging system:

import os

let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards")
logger.log("Started a task")

It's now much easier to pass arguments to logs using new Swift interpolation than the old os_log API with C-style formatters:

logger.log("Started a task \(taskId)")

Like in os_log, it does not generate the resulting formatted string at the moment of logging, but stores an optimized representation and defers formatting to a later moment

You can pass any values like numeric types, ObjC types that implement description and any type conforming to CustomStringConvertible

Non-numeric data is private by default:

logger.log("Paid with bank account \(accountNr)")
// -> Paid with bank account <private>

To actually display the values, use the privacy: parameter:

logger.log("Ordered smoothie \(smoothieName, privacy: .public)")

You can also log private data in an obfuscated form, but hashed in such a way that each value gets its unique hash which you can track through a chain of logs:

logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")
// -> Paid with bank account <mask.hash:'CSvWylJ63...'>

Collecting recent logs from a connected device to a file:

log collect --device --start "2020-06-22 9:41" --output fruta.logarchive

Log levels:

  • debug – useful only during debugging
  • info – helpful but not essential for troubleshooting
  • notice (default) – essential for troubleshooting
  • error – expected errors
  • fault – unexpected errors, assumptions that weren’t true, potential bugs

The Logger has a separate method for each log level:

logger.debug(…)
logger.error(…)
// etc.

Take into account that not all logs are persisted after the app finishes execution, some can only be live streamed

debug  ⭢  not persisted by default

info  ⭢  saved to memory, only persisted if collected using log collect before they disappear

notice  ⭢  persisted up to a storage limit, older messages are purged after some time

error, fault  ⭢  persisted for a longer time than notice messages (typically a few days)

The level also affects logging performance: debug logs are the fastest, error and fault are the slowest

It’s safe to call even some slower code within log messages that will not be logged – the code isn’t run unless the log is enabled:

logger.debug("\(slowFunction(data))")

Logger provides a number of built-in formatters that let you customize how an interpolated value is displayed:

logger.log("\(cardId, align: .left(columns: 5))")
logger.log("\(seconds, format: .fixed(precision: 2))")
logger.log("\(data, format: .hex, align: .right(columns: 4))")

+ others like decimal, exponential, octal

Existing os_log(…) function also accepts the new interpolating parameters (when running on latest OS versions)

What's new in SwiftUI

Categories: SwiftUI 0 comments Watch the video

App & scene definition:

The whole app can now be built with SwiftUI, including what was previously in app delegate:

@main
struct HelloWorld: App {
    var body: some Scene {
        WindowGroup {
            ...
        }
    }
}

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

Settings – creates Mac preferences windows:

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

For document-based apps:

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

Defining menu commands:

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

New multiplatform Xcode project template that creates one SwiftUI app for all platforms

New “Launch Screen” Info.plist key

Lets you define launch screen elements like background, navigation bar, tab bar

Replacement for the launch storyboard for SwiftUI apps that don't otherwise have storyboards

New API for widgets:

@main
struct RecommendedAlbum: Widget {
    var body: some WidgetConfiguration {
        ...
    }
}

Complications for Apple Watch can now also be built using SwiftUI

Outlines (hierarchical lists):

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

Grids:

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

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

Also horizontally loading grids

Lazy versions of the existing stack views: LazyVStack, LazyHStack

Toolbars:

.toolbar {
    Button(action: { ... }) { Label("Record", systemImage: ...)
    // placed automatically in an appropriate place

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

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

Labels:

Label(“Books”, systemImage: “book.circle”)
Label { Text(“…”) } icon: { Image(systemName: “…”) }
  • combines an icon with a title and shows them appropriately for the context

.help(“Does something”) – tooltips on macOS, voiceover comment on iOS

.keyboardShortcut(“X”)

.keyboardShortcut(.cancelAction / .defaultAction)

New support for focus on tvOS

ProgressView – spinner or progress bar

Gauge – on watchOS, the circular meter control from complications

.matchedGeometryEffect(id: albumId, in: namespace) – animates a view from one view hierarchy into another

.clipShape(ContainerRelativeShape()) – applies rounded corners matching the parent view

Text – custom fonts are now adapted to Dynamic Type

Images can be embedded inside text and are scaled with it

@ScaledMetric var padding – a value defined by you that is scales with Dynamic Type

App accent color can now be set in the asset catalog

Support for accent color on the Mac

.listItemTint() customizes the color for a section or item

Tint parameter in control styles: .toggleStyle(SwitchToggleStyle(tint: .accentColor))

Link(destination: url) { Label(…) } – a link label that opens a URL in Safari

@Environment(\.openURL) var openURL – provides a openURL(url) function

UniformTypeIdentifiers framework provides content type identifiers for drag & drop

SignInWithAppleButton()

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

Stacks, Grids, and Outlines in SwiftUI

Categories: SwiftUI 0 comments Watch the video

Stacks

New lazy stack views: LazyVStack and LazyHStack

They work just like the existing horizontal and vertical stacks, but they render their content incrementally as it becomes visible

Useful for very long lists built using VStack/HStack that had performance issues previously

There’s no point using lazy stacks for small local stacks that just lay out single views that are all visible on the screen together

If you’re unsure, use standard HStack/VStack by default, and only use lazy stacks for long content that’s scrolling and becomes a performance bottleneck

Grids

New grid views: LazyHGrid and LazyVGrid

They lay out subviews in a grid like collection view

LazyVGrid(columns: columns, spacing: 0) { … }

Grids require a definition of columns:

Constant number of columns:

var columns = [
  GridItem(spacing: 0),
  GridItem(spacing: 0),
  GridItem(spacing: 0)
]

Adaptive columns, number depends on the window size:

var columns = [
  GridItem(.adaptive(minimum: 300), spacing: 0)
]

Lists and outlines

Lists are more than just stacks of content – they support selection and scrolling etc.

Lists don’t need a Lazy variant, because list contents are always loaded lazily

Lists can now show hierarchical trees of items (outlines)

To make an outline list, provide a “children” keypath:

List(graphics, children: \.children) { graphic in
    GraphicRow(graphic)
}

To make collapsible list sections, use an OutlineGroup:

ForEach(canvases) { canvas in
    Section(header: Text(canvas.name)) {
        OutlineGroup(items, children: \.children) { graphic in
            GraphicRow(graphic)
        }
    }
}

You can also implement other views with parts that the user can collapse and expand like in an outline using DisclosureGroup:

DisclosureGroup(isExpanded: $expanded) {
  Contents()
} label: {
  Label(“Name”)
}

or:

DisclosureGroup(“Label”) { contents }

Build document-based apps in SwiftUI

Categories: SwiftUI 0 comments Watch the video

For document based apps, use the DocumentGroup scene as the main scene of the app:

DocumentGroup(newDocument: MyDocument()) { file in
  ContentView(document: file.$document)
}

The content view receives a binding to the document contents, so when it changes the contents, the system knows it was modified

Document based apps have a “Document Types” section in the Info.plist, where you declare Uniform Type Identifiers of document types associated with your app

Imported types  ⭢  documents from other apps or sources that your app is able to open

Exported types  ⭢  document types owned by your app

TextEditor() – built in text view type

The type that implements the document model conforms to FileDocument (for value types) and declares its own UTType instances that represent imported and exported file types:

import UniformTypeIdentifiers

extension UTType {
    static var exampleText: UTType {
        UTType(importedAs: "com.example.plain-text")
    }

    static let shapeEditDocument =
        UTType(exportedAs: "com.example.ShapeEdit.shapes")
}

Imported type needs to be a computed property, because the value returned from the constructor may change between calls while the app is running, depending on system configuration. Exported type can just be assigned once and stored.

The document type provides a list of types (own and generic) that it can accept:

static var readableContentTypes: [UTType] { [.exampleText] }

It also has methods for reading and writing its document to/from a file, which you need to implement:

init(fileWrapper: FileWrapper, contentType: UTType) throws { ... }

func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { ... }

In those methods, you can assume that the content type is one of those you declared as accepted by your app.

What's new in Swift

Categories: Swift 0 comments Watch the video

Improvements to binary size, especially in SwiftUI apps

Smaller memory usage overhead from things like internal caches of the Swift runtime (use iOS 14 deployment target for best effect)

Apple can now use Swift in very low level frameworks, where previously only C was available

Much better compiler diagnostics (warnings and errors)

New diagnostics subsystem that helps identifying code issues much more precisely

More actionable errors with guidance how to fix them

Improved code completion

Code completion better understands features such as ternary operator or keypaths

Drastically improved performance, up to 15x faster

Improved code indentation in things like: chained calls, call arguments, multi-line control flow statements

Better error messages for runtime failures like arithmetic overflow

Debugging improvements: improved module importing which should lead to less issues while evaluating variables in lldb

Cross-platform support:

Improved support for various distributions of Linux

Coming soon: initial support for Windows

Swift AWS Lambda runtime

New language features:

Multiple trailing closures

(API design tip: design names for methods with closure arguments so that the call site still makes sense if a trailing closure w/o an argument label is used)

Keypaths can be used as functions, so you can pass them to APIs that expect a function, like xx.map(\.field) (this is from Xcode 11.4)

@main – marks the entry point to the program

As a library author, you can declare a static function main() in a class or type

Users can then use an instance of that type with the tag @main, and compiler implicitly creates a main.swift file that calls that function

class Foo {
  public static func main() {
    …
  }
}
import FooKit

@main
class Hello: Foo {
  …
}

Replaces the old @UIApplicationDelegate which could only be used in the specific context of UIApplicationDelegate

Increased availability of implicit self in closures:

  • you can skip self. if you capture [self] in the closure arguments
  • you can skip self. if self is a struct or enum (like in SwiftUI)

Multi-clause catch statements in do-catch

Automatic comparable conformance for (some) enum types

Enums can fulfill protocol requirements requiring static properties (plain case) and static functions (case with arguments)

View builders improvements:

  • support for more kinds of statements, e.g. switch and if-let
  • adding a view builder annotation to body declaration if it has more than one children is not necessary anymore

Float16 – a float type that uses 2 bytes (half of standard float)

Apple Archive – new archive file format, .aar extension

Fast, uses multithreading

Includes a command-line tool and Finder integration

Swift API – AppleArchive framework

Swift System – Swift interfaces to system calls and other low level APIs

OSLog:

Improved performance

Support for string interpolations and formatting options:

logger.log("\(offerID, align: .left(columns: 10), privacy: .public)")
logger.log("\(seconds, format: .fixed(precision: 2)) seconds")

Packages available on GitHub:

Swift Numerics

Swift Argument Parser

Swift Standard Library Preview

iPad and iPhone apps on Apple Silicon Macs

Categories: Mac, UIKit 0 comments Watch the video

iPhone and iPad apps can now run unmodified on macOS on ARM, the same exact binary as on iOS

Uses Catalyst APIs, so apps get a lot of behavior for free

Making a separate Catalyst build manually is still recommended for optimal result (and supports Intel Macs)

To run on the Mac, an iOS app must not:

  • depend on missing symbols, frameworks or other functionality (Xcode should help you at least in some cases)
  • depend on hardware which Macs don’t have

Compatible apps are automatically made available for Macs unless you opt out

Possible compatibility issues:

Hardware:

  • Macs use mouse & keyboard and not touch screens, so if an app uses touch in non-standard ways or relies on multi-touch, this might not work
  • apps that depend on sensors like accelerometer, gyroscope, compass, depth camera, precise GPS will not work
  • iOS apps expect one front and one back camera, Macs may have one, none or multiple (use AVCaptureDeviceDiscoverySession)

UI:

  • alerts and panels like open/save will look differently than on iOS, so don't assume their dimensions and position
  • if an iPad app supports multitasking, it will be fully resizable to any dimensions, including some layouts and sizes you might not expect
  • iPhone-only apps and iPad apps that don’t support multitasking will run in a fixed size window matching iOS device size
  • window resizing is live on macOS – make sure it’s optimized

System software:

  • on the Mac, user can move the app bundle to different locations, application container is also in a different location
  • APIs returning device type or model will include Mac model types
  • screen size will not be one of the standard iOS device screen sizes that you may expect

You can debug, profile and test iOS on an ARM Mac as you would expect

Distribution for Mac devices works the same as for iOS devices

Apps can reuse existing subscriptions and in-app purchases

App thinning automatically selects most appropriate resources for the given Mac

→ when making a thinned build, you can select the option to optimize for any Mac

TestFlight is (still) not available on the Mac

Design with iOS pickers, menus and actions

Categories: UIKit 0 comments Watch the video

iOS now includes new lightweight menus in a lot of places, same as the context menus shown below previews

They replace a lot of previous use of action sheets

Disadvantages of action sheets:

  • dimming the background of the whole window is a too strong effect in most cases
  • wastes space because the rows take full screen width even though labels are usually short
  • they always appear at the bottom, so you often need to move your finger across whole screen if the element that launched the menu is at the top

The new menus appear next to the element they launch from, take less space and require less finger movement, the transition is shorter and lighter

Actions in the new menus can also be used for selection and navigation

Each action has a title and an optional image (SF symbol or custom)

Menu can have its own title at the top

It can also include separators between items

A menu can be presented from any button, in a toolbar or the main view

Menu is dismissed when the user taps somewhere outside, so an explicit “cancel” action isn’t needed

Example use cases for menus:

Disambiguation:

  • “add” button shows a choice of what kind of item you want to add
  • “insert photo” button shows a choice of where you want to load the photo from
  • “done” button asks how you want to save the finished video

Navigation:

  • “back” button shows a history when you press and hold it

Selection:

  • “sort” button lets you choose the sorting field

Secondary actions:

  • “…” button displays a list of less important actions grouped in a menu – it lets you clear up the rest of the UI which would otherwise be cluttered with buttons and switches
  • however, do not hide primary actions inside a “more” menu

Destructive actions (delete a list) should be marked as such (with red color) and should show a confirmation in a different place than the first button

→ display an action sheet at the bottom of the screen: Are you sure? Delete List / Cancel

Do this also for destructive actions that aren’t launched from a menu (e.g. when clicking a “Cancel” button while composing a message) – don’t use menus for this since it would make it too easy to confirm by accident

Build with iOS pickers, menus and actions

Categories: UIKit 0 comments Watch the video

Control design updates:

UISlider and UIProgressView: slightly updated design to make it more similar to the Mac

→ design is again slightly more Mac-like when running in a Catalyst environment – some UI customizations in these classes are ignored there

UIActivityIndicatorView: updated design, larger and with fewer lines

UIPickerView: updated design

→ it’s strongly recommended to use menus instead in Catalyst apps

UIPageControl

  • allows an unlimited number of pages
  • you can use custom images for all page dots (preferredIndicatorImage) or for a specific page (setIndicatorImage(…, forPage: i))
  • backgroundStyle = .prominent

Color picker

UIColorPickerViewController – new color picker dialog like on macOS

  • presents as a sheet or popover
  • has a selectedColor property that you can preset and read from
  • allows picking a color using a few different methods
  • colors can be favorited and reused across apps
  • includes an eyedropper that lets you pick a color from the screen below, or another app on iPad
  • on macOS it appears as the standard Mac color picker panel
var colorPicker = UIColorPickerViewController()
colorPicker.supportsAlpha = true
colorPicker.selectedColor = UIColor.blue
self.present(colorPicker, animated: true, completion: nil)

func colorPickerViewControllerDidSelectColor(
  _ viewController: UIColorPickerViewController
) { … }

func colorPickerViewControllerDidFinish(
  _ viewController: UIColorPickerViewController
) { … }

Date picker

UIDatePicker – new compact style, shows two fields that open a popup when tapped

Inline style – shows picker as part of the main view instead of a popup

datePicker.preferredDatePickerStyle = .automatic / .compact / .inline / .wheels

Menus

button.menu, barButtonItem.menu – shows a context menu when that button is tapped

For UIButton: set button.showsMenuAsPrimaryAction = true

For UIBarButtonItem: set menu but don’t provide a primaryAction

Back button now shows a “history” menu when you press and hold it

Looks at: backBarButtonItem.title, backButtonTitle, title

Implemented in UIControl

UIControl.showsMenuAsPrimaryAction

UIControl.contextMenuInteraction, isContextMenuInteractionEnabled

Register for UIControl.Event.menuActionTriggered to observe when menu is opened

UIDeferredMenuElement – asynchronously provides menu items

Shows a standard loading UI (“Loading” + spinner) until menu items are ready

Once loaded, menu items are cached

UIContextMenuInteraction:

  • updateVisibleMenu(_ block: (UIMenu) -> UIMenu) – replaces currently displayed menu
  • menuAppearance  ⭢  .rich = under a preview, .compact = just the menu

Actions

UIAction is used in more places across UIKit

All controls can be constructed using a UIAction now

UIBarButtonItem:

  • new initializers taking primaryAction and/or menu
  • .fixedSpace(width: x) and .flexibleSpace() spacers

UIButton: init(type:primaryAction:), defaults to using action’s title and image on the button

UISegmentedControl: init(frame: actions:) – creates segments from a list of actions, action is automatically called when that segment is selected