MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 19

App ClipsAppKitExtensionsFoundationLocationMacMapsPhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 19WWDC 20

View as index

Building Custom Views with SwiftUI

Categories: SwiftUI, WWDC 19 0 comments Watch the video

Layout for a simple view with just text:

  • text sizes to fit the contents
  • the view's content view contains everything inside, i.e. the text, and also has the size needed to fit all the subviews with their preferred size
  • the root view of the screen takes the whole frame of the screen and centers the small content view inside itself

By default the root view does not fill the safe area at the top – to make it really fill the whole screen, use .edgesIgnoringSafeArea(.all)

The layout process in general goes like this:

  1. 1. The parent proposes a size to the child (the maximum area that it can offer)
  2. 2. The child chooses its own preferred size
  3. 3. Parent places the child inside its coordinate space, by default in the middle

This means that subviews have their own sizing behaviors, the child decides what size it needs for itself

If a view uses a modifer like .aspectRatio(1) or .frame(width: 50, height 10), the parent has to obey this

Coordinates are always rounded to the nearest pixel, so there are crisp lines and no antialiasing

.background() inserts a view wrapping directly the view it’s called on, and it always has the same bounds as that view – so it can be useful for debugging to see the actual sizes of each view

A background view is “layout neutral”, so it doesn’t affect the layout at all

.padding() adds padding around the view – if not specified, SwiftUI chooses the default amount of padding appropriate for the given element, platform and environment; it offers its subview slightly smaller area than it was offered, inset by the specified padding

A Color view fills whatever space it’s given

Images:

Images are by default fixed size (equal to the image dimensions), unless you mark them as resizable – so just applying .frame(…) to an image won’t change its size, it just wraps it in a larger frame (empty on the sides)

A .frame() is *not* a constraint like in AutoLayout – it’s just a wrapping view that proposes a specified size to its child – which the child can take into account or not, depending on the type of view

“There is no such thing as an incorrect layout… unless you don’t like the result you’re getting” :D

SwiftUI automatically applies spacing between elements depending on their kind

You can override the spacings, but the defaults should usually be the right values

When you view the layout in a right-to-left language, SwiftUI automatically switches all subviews in the correct direction

Stacks:

A stack takes the space it was given from the parent, deducts the spacing and divides it equally into children, then proposes that space to children starting with the least flexible ones (e.g. a fixed size image)

If they don’t take all available space, they’re aligned inside the stack according to specified alignment, and then the stack sets its size to enclose all the children

To define which children in a stack take more available space if there isn’t enough, use .layoutPriority(x) to specify their priority (default is 0)

Vertical alignment:

Don’t align labels in an HStack to .bottom – align them to the baseline instead (.lastTextBaseline)

If there are images too, by default the image’s baseline is the bottom edge, but you can override it this way:

.alignmentGuide(.lastTextBaseline) { d in d[.bottom] * 0.927 }

Aligning views in separate branches of the view tree:

To align views that are in separate stacks to each other, you need to specify a custom named alignment guide:

extension VerticalAlignment {
  private enum MidStarAndTitle: AlignmentID {
    static func defaultValue(in d: ViewDimensions) -> Length {
      return d[.bottom]  // not important
    }
  }

  static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
}

Now, any view with an added modifier .alignmentGuide(.midStarAndTitle) { … } will be aligned to the same line

In the block, specify how to calculate the position of the guide in the view:

.alignmentGuide(.midStarAndTitle) { d in d[.bottom] / 2 }

Drawing graphics:

Graphics in SwiftUI is done with special kinds of views representing shapes, but they’re views just like the buttons and labels, so everything about buttons and labels applies to drawing, and all the effects done for drawing can also be applied to controls

Circle(), Capsule(), Ellipse()
Circle().fill(Color.red)
Capsule().stroke(red, lineWidth: 20)
Ellipse().strokeBorder(red, style: …)
Gradient(colors: [.red, .yellow, …])
AngularGradient(gradient: spectrum, center: .center, angle: .degrees(-90))

Views like Color or Gradient act as independent views themselves, or can be used as a fill for shapes:

Circle().fill(angularGradient)
Circle().strokeBorder(gradient, lineWidth: 50)

Custom shapes:

To define a custom shape, build a struct conforming to the Shape protocol that defines a path:

struct WedgeShape: Shape {
  var wedge: Ring.Wedge

  func path(in rect: CGRect) -> Path {
    var p = Path()
    p.addArc(…)
    p.addLine(…)
    p.closeSubpath()
    return p
  }
}

Custom transition:

struct ScaleAndFade: ViewModifier {
  var isActive: Bool

  func body(content: Content) -> some view {
    return content
      .scaleEffect(isActive ? 0.1 : 1)
      .opacity(isActive ? 0 : 1)
  }
}

let scaleAndFade = AnyTransition.modifier(
  active: ScaleAndFade(isActive: true),
  identity: ScaleAndFade(isActive: false)
)

.transition(scaleAndFade)

When drawing with a lot of elements, mark all shapes inside a container as a “drawing group” (.drawingGroup()) to give a hint to the rendering engine to flatten them all into one native view or layer and render the contents using Metal

What’s New in AppKit for macOS

Categories: AppKit, Mac, WWDC 19 0 comments Watch the video

New system colors: NSColor.systemTeal and systemIndigo

NSColor uses tagged pointers

NSColorSampler – a magnifier tool for picking a color from somewhere on the screen

Recording the screen will now ask the user for permission

NSColor(name: “…”) { appearance … }

NSScreen.localizedName now returns e.g. “Thunderbolt Display”

(10.11)

CAMetal.wantsExtendedDynamicRangeContent = enables dynamic range in this layer

NSScreen.maximumExtendedDynamicRangeColorComponentValue = tells you the maximum white value (e.g. 1.3)

NSScreen.maximumPotentialExtendedDynamicRangeColorComponentValue – tells you this even when it’s not on

NSScreen.maximumReferenceExtendedDynamicRangeColorComponentValue

CAMetalLayer.preferredDevice, MTKView.preferredDevice

NSTextView.usesAdaptiveColorMappingForDarkAppearance – automatically updates colors for light/dark appearance

NSTextCheckingController + NSTextCheckingClient – for spell checking, data detection, autocorrection

NSFontDescriptorSystemDesign

NSAttributedString text scaling macOS  ⭤  iOS

NSLayoutManager.usesDefaultHyphenation

NSToolbarItem.isBordered, title

NSToolbarItemGroup: segmented controls and pulldown/popup menus, collapsed representation

NSMenuToolbarItem

NSTouchBar.isAutomaticCustomizeTouchBarMenuItemEnabled

NSStepperTouchBarItem

NSSliderTouchBarItem.minimumSliderWidth, maximumSliderWidth

NSSwitch – a new NSControl like UISwitch on iOS (avoid using for small things and in large numbers, just one for some general mode switch, like Time Machine on/off)

NSCollectionView – compositional layout, diffable data source

using custom VC initializers for injecting dependencies:

@IBSegueAction func showFoo(_ coder: NSCoder) -> NSViewController { }

NSView.isHorizontalContentSizeConstraintActive, isVertical

Open and save panels are now always out-of-process, even for non-sandboxed apps

NSWorkspace: asynchronous methods for opening URLs and applications

NSWorkspace.OpenConfiguration

Using iPad with Sidecar as a tablet: tablet events come as mouse events with NSEvent.SubType.tabletPoint and pressure

NSEventType .changeMode  ⭢  on double-tap on the pencil

NSDirectionalRectEdge

NSDirectionalEdgeInsets

NSRectAlignment

NSRelativeDateFormatter

NSListFormatter – formats list of things, adding commas properly

Non-UI file provider action extension

Replacing kernel extensions with Network Extensions, DriverKit, Endpoint Security

Advances in Foundation

Categories: Foundation, WWDC 19 0 comments Watch the video

Ordered collection diffing:

let diff = bird.difference(from: bear)
bear.applying(diff)

Data’s storage is now always a contiguous area in memory

ContiguousBytes: withUnsafeBytes(_ body: (UnsafeRawBufferPointer) -> R)

DataProtocol, MutableDataProtocol

Compression:

let compressed = try data.compressed(using: CompressionAlgorithm.lzfse / .lz4 / .lzma / .zlib)

UnitDuration: + milliseconds, microseconds, nanoseconds, picoseconds

UnitFrequency: + framesPerSecond

UnitInformationStorage (bits, bytes, kilo, etc.)

RelativeDateTimeFormatter: for formatting dates as e.g. "2 weeks ago"

ListFormatter: for formatting lists of things in a language-specific way

NSOperationQueue: addBarrierBlock { save() }

queue.progress.totalUnitCount – set this and then show 2/10 etc.

scanner.scanUpToCharacters(from: …) returns a String directly instead of through a reference

Support for USB drives and network volumes

Use FileManager.SearchPathDirectory.itemReplacementDirectory when writing files to USB / SMB

Do filesystem access on a background thread, because it might be loading from the network!

Test filesystem capabilities with URLResourceKey

Be prepared to handle errors

Advances in App Background Execution

Categories: Foundation, WWDC 19 0 comments Watch the video

VoIP calls: when your app gets a pushRegistry(_: didReceiveIncomingPushWith: for: completion:) callback, it now has to start a call (reportNewIncomingCall) or the app will be killed

Background/silent pushes (content-available):

  • you must set apns-priority = 5
  • you should set apns-push-type = background

BackgroundTasks – a new background mode and framework for deferrable work

  • it lets you schedule some work to do in the background later at a more appropriate time, e.g. when the device is charging
  • several minutes of runtime
  • meant for e.g. maintenance tasks, ML training
  • you can request to turn off the CPU monitor that normally kills your app if you use too much CPU

New API for background refresh

  • works as before – 30 seconds of runtime, called throughout the day so that you keep your app up to date
  • the exact times and frequency are based on user’s usage patterns
  • old API (setMinimumBackgroundFetchInterval, performFetchWithCompletionHandler) is deprecated

Tasks/refresh are scheduled using requests sent to a BGTaskScheduler:

* BGAppRefreshTaskRequest – for app refresh (“a short task typically used to refresh content”)

* BGProcessingTaskRequest – for maintenance/learning tasks (“a time-consuming processing task”, “can take minutes to complete”)

Tasks can be scheduled from an extension, but are always run in the main app

Multiple tasks can be given to the app in one batch

Each type of task has a unique ID (preferably reverse-dns style), and an Info.plist key needs to list all task IDs

Scheduling tasks requires two steps:

1) Registering a task:

BGTaskScheduler.shared.register(forTaskWithIdentifier: “…”, using: queue/nil) { task in … }

In the task block, you can assign task.expirationHandler to be notified if the task needs to be killed before it finishes work

Call task.setTaskCompleted(success) when finished (or when expiration handler fires)

This needs to be done before the app finishes launching.

2) Actually scheduling a registered task:

let request = BGAppRefreshTaskRequest(identifier: “…”)
BGTaskScheduler.shared.submit(request)

request.earliestBeginDate – only start at or after this moment (don’t set more than a week into the future)

For processing tasks:

request.requiresNetworkConnectivity = true – default is false

request.requiresExternalPower = true – disables CPU monitor

You also need to schedule the next run of a task again once you’re in the task execution block, to keep the “refresh loop” going

If you call submit() during app initialization, run it on a background queue

Forcing a task to be scheduled from the debugger:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@“…”]
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@“…”]

Make sure that all files you need in a task are accessible when the device is locked

Tasks will not be run until the first device unlock after the reboot

Implementing Dark Mode on iOS

Categories: UIKit, WWDC 19 1 comment Watch the video

Traditionally iOS apps had hardcoded all colors

Now, since you need to use different variants depending on the appearance, it’s better to use semantic, dynamic colors

iOS does the work for you – chooses the right color and updates it automatically when appearance changes

Semantic system colors:

label – default label text color (black/white)

systemBackground – base background color, pure white / pure black

secondarySystemBackground, tertiarySystemBackground – slightly darker/lighter shades of gray to let you visualize the view hierarchy of your app

systemGroupedBackground – for table background

Base colors adapting to appearance: systemBlue, systemRed, …

Dynamic colors defined in asset catalogs with 2 versions

All UIColors contain the defined variants inside and update to the right one automatically

Images can also have a dark appearance variant in the asset catalog

Debug dark mode using the “Environment overrides” runtime panel in Xcode status bar

Materials with blurred translucent background & vibrancy

Use by creating a UIVisualEffectView with UIBlurEffect(style: .systemMaterial)

Material types: thick, regular, thin, ultrathin

Vibrancy types: primary, secondary, tertiary, quarternary (for text and fills)

To use, create another UIVisualEffectView with a UIVibrancyEffect inside the blur effect view’s contentView

Hierarchy: UIVisualEffectView (+UIBlurEffect)  ⭢  contentView  ⭢  UIVisualEffectView (+UIVibrancyEffect)  ⭢  contentView  ⭢  labels etc.

Your views can be in one of two “layers” of UI: the base level and the elevated level. Elevated level is used e.g. in modals presented as sheets.

In dark mode, backgrounds in the elevated level are slighly darker (primary background is not pure black)

UITraitCollection has a userInterfaceStyle (light/dark) and userInterfaceLevel (base/elevated)

To get the actual rendered color in a given context: dynamicColor.resolvedColor(with: traitCollection)

Dynamic colors in code:

UIColor { traitCollection in … return .black/.white }

For custom drawing, access the UITraitCollection.current property which UIKit sets for you

When you want to set e.g. a layer’s color to a cgColor, you can:

  • resolve it from a UIColor using resolvedColor()
  • tell traitCollection to update .current by calling: traitCollection.performAsCurrent { … }
  • update .current yourself

If you need to manually update the color when appearance changes, use traitCollectionDidChange() and do:

if traitCollection.hasDifferentColorAppearance(comparedTo: previous) { … }

UIImage does not store all variants inside, so ideally you should use UIImageView which manages the variants for you

If you need to resolve an image manually, do: image.imageAsset.image(with: traitCollection)

Trait collections

New behavior for trait collections when initializing views: iOS predicts the target trait collection for when the view is added to the hierarchy, and only calls didChange() when it actually changes

Debugging trait collection changes: add a launch argument -UITraitCollectionChangeLoggingEnabled YES

The best places to use traits are: viewWill/DidLayoutSubviews() and UIView.layoutSubviews(), traits are guaranteed to be resolved at those points

Forcing a specific appearance:

  • UIViewController.overrideUserInterfaceStyle (preferred)
  • UIView.overrideUserInterfaceStyle
  • UIPresentationController.overrideTraitCollection and UIViewController.setOverrideTraitCollection(_: forChild:) – only set the modified properties
  • for the whole app: UIUserInterfaceStyle key in Info.plist = Light/Dark

Status bar styles:

  • .darkContent – always dark (like .default before)
  • .default = automatic based on the userInferfaceStyle of the VC

UIActivityIndicator:

  • .medium and .large styles instead of .gray, .white and .whiteLarge
  • updates automatically for the appearance
  • you can use color property to set a custom color

NSAttributedText: you need to manually set .foregroundColor = .label, otherwise you get black

Modern Swift API Design

Categories: Swift, WWDC 19 0 comments Watch the video

Your most important goal as an API designer: Clarity at the point of use

Make it obvious when reading the API what it’s doing and how to use it correctly

Apple will not use prefixes in new Swift-only frameworks

Be careful – imported names are brought into a single namespace, so a very general name will require your users to manually disambiguate names in case of conflicts

Structs vs. classes:

  • both have their roles
  • prefer structs by default over classes, unless you have a good reason
  • use classes if you need the reference semantics, reference counting and deinitialization
  • use classes if the value is held centrally and shared
  • use classes if there is a separate notion of identity and equality

RealityKit: entities are references, handles into shared objects that live inside the rendering engine, so classes make sense

Objects used to configure entities – locations, materials etc. – are mostly structs

Modernizing Your UI for iOS 13

Categories: UIKit, WWDC 19 0 comments Watch the video

Launch images are deprecated – starting from April 2020 App Store will require apps to have a launch storyboard

When a new device with new screen size is launched, apps built on iOS 13 and above that aren’t rebuilt on latest SDK will always be displayed in native resolution, not in some kind of compatibility mode like before. This means your app needs to support being rendered in unexpected resolutions – for iPad apps this includes split screen modes.

Customizing navigation title appearance (large titles):

UINavigationBarAppearance()

.configureWithOpaqueBackground()

.titleTextAttributes

.largeTitleTextAttributes

navigationBar.standardAppearance – settings to use as default

navigationBar.compactAppearance – on smaller iPhones in landscape

navigationBar.scrollEdgeAppearance – when scrolled to the top (default is now to have transparent background below the large title)

navigationBar.buttonAppearance – appearance for normal navigation bar buttons

navigationBar.doneButtonAppearance – appearance for “done” (bold) navigation bar buttons

You can customize e.g. title color per screen by overriding the appearance in VC’s navigationItem.***Appearance.

Same for:

  • UIToolbar  ⭢  UIToolbarAppearance
  • UITabBar  ⭢  UITabBarAppearance

Additional options for tab bars:

tabBar.stackedLayoutAppearance – default

tabBar.inlineLayoutAppearance – on iPads

tabBar.compactLayoutAppearance – on smaller phones

Sheets - new default presentation style for modals

UIModalPresentationStyle .pageSheet and .formSheet no longer render as full screen in compact width + regular height layout (i.e. iphone portrait)

On iPads page sheets also have an updated design, the default width follows the “readable width” and depends on font size

If a page sheet opens another page sheet on ipad, they form a stack

Default style is now “automatic” and shows a page sheet for custom VCs, or chooses display mode depending on configuration for system modals

Set presentation style explicitly to .fullScreen to opt out

Popovers also display as new-style sheets in compact width

The "view will/did appear/disappear" callbacks are not called on the presenting (parent) view if the modal appears as a sheet, since it doesn’t completely disappear

To prevent a sheet from being pulled down:

  • set isModalInPresentation = true
  • delegate method presentationControllerDidAttemptToDismiss() is called if the user tries to pull the sheet down
  • you can also implement presentationControllerShouldDismiss() and make the decision there
  • presentationControllerWillDismiss() and presentationControllerDidDismiss() are called before/after dismiss animation

Share extension views are also presented as sheets by default

Search

UISearchController:

  • automaticallyShowsCancelButton – toggling cancel button
  • automaticallyShowsScopeBar – toggling the segmented control below
  • searchBar.textField – for customizing the search text field
  • showsSearchResultsController = true – show results immediately (to e.g. show suggestions or favorites)

UISearchTextField – a UITextField that supports tokens

let token = UISearchToken(icon: icon, text: “…”)
field.replaceTextualPortion(of: range, with: token, at: field.tokens.count)

field.textualRange  ⭢  range of just the text after all tokens

Gestures

Using system text selecting/editing gestures in custom text views:

let interaction = UITextInteraction(for: .editable)
interaction.textInput = textView
textView.addInteraction(interaction)

Table/collection view multi-selection:

  • tables & collection views now allow quick multi-selection by dragging two fingers across the list
  • on iPads with external keyboards also works with shift+select
  • tableView(_: shouldBeginMultipleSelectionInteractionAtIndexPath:) – return true to opt in
  • tableView(_: didBeginMultipleSelectionInteractionAtIndexPath:) – update UI when the user turns on edit mode

New 3-finger editing gestures: copy, paste, undo & redo

Your app gets them automatically if it uses UndoManager

Set UIResponder.editingInteractionConfiguration = .none to disable if this conflicts with existing gestures in your app

Context menus (replaces old 3D Touch peek & pop actions):

Lets you present a rich preview of content + menus with complex hierarchies

iOS adapts the exact layout depending on the context (portrait iPhone / landscape iPhone / iPad)

In Catalyst apps it renders as a Mac context menu

Menus are built with UIMenu and UIAction objects

UIMenus are hierarchical, so they can contain other UIMenus as submenus

  • UIAction – a named action with a title and optionally an image, which takes a block to execute when it's selected in the menu
  • UIMenu – groups a list of UIActions and possibly nested UIMenus into a menu
  • there's also UICommand – like UIAction, but it takes a selector instead of a block and passes it through the responder chain

Adding to a view:

let interaction = UIContextMenuInteraction(delegate: self)
image.addInteraction(interaction)

Delegate protocol requires one method:

func contextMenuInteraction(_ interaction: configurationForMenuAtLocation:) -> UIContextMenuConfiguration?

Return nil if context menu should not be displayed

UIContextMenuConfiguration takes 3 parameters:

  • identifer – an id for you to identify this specific context menu
  • previewProvider
  • actionProvider – a closure that returns a UIMenu with a list of actions

→ receives a list of system-provided “suggested actions” as a parameter that you can include

You can customize the context menu’s animation, the look and location of the preview (UITargetedPreview), etc.

In table & collection views, context menus on table cells can be enabled easily using:

tableView(_: contextMenuConfigurationForForAtIndexPath: point:) -> UIContextMenuConfiguration?

Peek & pop (UIViewControllerPreviewing) is deprecated

Advances in UI Data Sources

Categories: UIKit, WWDC 19 0 comments Watch the video

Current approach:

UI***ViewDataSource protocol with numberOfSections, numberOfItems and cellForItemAt methods

This works well for static data, but when you need to update the list later, you need to either reload all data, which results in bad UX, or calculate the changes manually, which is complicated and error-prone

The UI and data source must always agree and there’s no single source of truth

New approach:

Diffable data sources

performBatchUpdates()  ⭢  apply()

Snapshot: saved current state of the UI

Uses identifiers instead of index paths

UICollectionViewDiffableDataSource, UITableViewDiffableDataSource, NSCollectionViewDiffableDataSource

Setting up:

dataSource = UICollectionViewDiffableDataSource<…>(collectionView: collectionView) {
  collectionView, indexPath, mountain -> UICollectionViewCell? in
  …
  // basically the same as cellForItemAt
}

If we use native objects/structs as identifiers, we get them passed here so we don’t need to look them up in an array by index path

Updating data:

Step 1: Create an empty snapshot

let snapshot = NSDiffableDataSourceSnapshot<Section, Mountain>()

Step 2: Fill it with sections and items

snapshot.appendSections([.main])
snapshot.appendItems(mountains)

Section and item identifiers can be your own hashable types (e.g. an enum for sections)

Step 3: Apply the snapshot

dataSource.apply(snapshot, animatingDifferences: true)

→ it’s safe to call apply() from a background thread (but don’t mix main & background threads)

Alternatively, you can use a snapshot of the existing state:

let snapshot = dataSource.snapshot()
snapshot.deleteItems(…)
snapshot.appendItems(…)
dataSource.apply(snapshot)

Getting an identifier from an index path in other delegate methods:

let identifier = dataSource.itemIdentifier(for: indexPath)