MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 19

App ClipsAppKitExtensionsFoundationLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 14WWDC 16WWDC 18WWDC 19WWDC 20

View as index

SwiftUI on watchOS

Categories: SwiftUI, WatchKit 0 comments Watch the video

SwiftUI offers apps some capabilities that were not possible before, e.g. swipe to delete and reordering in lists, or an easy way to do custom graphics and animations

Fully integrated with WatchKit, both ways

SwiftUI allows you to use the same code on all platforms

However, Apple Watch is a very special platform; a watchOS app should not be just a tiny version of the iOS app, it should be specifically designed for the Watch by only picking the right elements of the experience

Building an Apple Watch app is building the whole experience, not just the main app, but also complications, notifications, Siri interface – depending on the app

Use subclasses of WKHostingController to embed SwiftUI views in an interface controller that WatchKit can use

.font(.system(.headline, design: .rounded)) – uses a rounded version of San Francisco

.listRowPlatterColor(topic.color) – sets the color of the list cell background

.listStyle(.carousel) – a list design which centers the currently focused item on the screen

.onMove { } – enables drag to reorder

.onDelete { } – enables swipe to delete

For notification UI, inherit from WKUserNotificationHostingController

When a notification is received (didReceive(_:)), the view body is automatically invalidated and reloaded

Using digital crown:

Fluent scrolling between the beginning and the end:

.digitalCrownRotation($binding, from:, through:)

→ lets you create some kind of custom scrollable container

Discrete values:

.digitalCrownRotation($binding, from:, through:, by:)

→ for building interfaces where e.g. some value moves up or down by 1 when scrolling

Going around in a circle:

.digitalCrownRotation($binding, from:, through:, by:, sensitivity:, isContinuous: true)

→ sensitivity says how fast it rotates

.focusable(true) – lets the user switch focus between elements; digital crown events go to the focused item

Building Custom Views with SwiftUI

Categories: SwiftUI 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 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 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 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 0 comments 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 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 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