MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 19

App ClipsAppKitCloudKitExtensionsFoundationiCloudLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 12WWDC 14WWDC 15WWDC 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 frameworks

AppKit is no longer the only framework to build Mac apps with – you also have an option now to use UIKit via Catalyst and the new SwiftUI framework

  • although you will probably still need to use at least a little bit of AppKit in both cases

NSColor

New system colors: NSColor.systemTeal and systemIndigo (dynamic colors, specific color values depend on light/dark appearance)

NSColor uses tagged pointers

→ this means that color data is stored in the pointer itself and allocating NSColor objects becomes very cheap

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

New NSColor initializer that lets you return different colors based on appearance:

let color = NSColor(name: "userWidgetColor") { appearance in
  switch appearance.bestMatch(from: [.aqua, .darkAqua]) {
  case .darkAqua:
    return darkUserWidgetColor
  case .aqua, .default:
    return lightUserWidgetColor
  }
}

Screens

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

Extended dynamic range:

Computer screens are very bright these days and most of the time aren't used at full brightness

This means that at lower brightness levels we can use the monitor's capability to show brighter pixels than what 100% white means at that brightness level, and provide an extended dynamic range, i.e. color values of more than 1.0

This feature has been available in macOS since 10.11:

  • CAMetalLayer.wantsExtendedDynamicRangeContent – enables dynamic range in this layer
  • NSScreen.maximumExtendedDynamicRangeColorComponentValue – tells you the maximum color component value (e.g. 1.3), if some content on the screen is using EDR

New APIs in 10.15:

  • NSScreen.maximumPotentialExtendedDynamicRangeColorComponentValue – tells you the maximum component value even if it’s not rendering in EDR mode at the moment, so that you can make some decisions in advance before you enable it
  • NSScreen.maximumReferenceExtendedDynamicRangeColorComponentValue – maximum usable value on reference screens like the new Pro Display XDR

Finding the current screen in Metal:

Previously, in order to find the MTLDevice for the current screen in Metal you had to do something like this:

let preferredDevice = CGDirectDisplayCopyCurrentMetalDevice(
    self.window?.screen?.deviceDescription["NSScreenNumber"]
)

In 10.15, there's a new property CAMetalLayer.preferredDevice that returns the MTLDevice for the current screen

Also MTKView.preferredDevice

Text & Fonts

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

Enable this if it's more important for rich text to be readable regardless of appearance than to show the document in an original unchanged form

Almost all of the NSText* related classes support secure coding now

Spell checking:

NSSpellChecker – old API for managing spell checking (since OS X 10.0!)

NSTextCheckingController – new API built as a successor of NSSpellChecker

  • used in UIKit, WebKit and AppKit
  • you can add support for it to any view by implementing the NSTextCheckingClient protocol
  • also does grammar checks, data detection, autocorrection

Font descriptors:

NSFontDescriptor.withDesign(.monospaced / .rounded / .serif)

Lets you choose a different variant of the same font, if available

Scaling fonts between macOS & iOS:

If your app opens rich text documents created on another platform, you will notice that fonts at the same point size will look different, because Mac and iOS devices use very different screen densities

There are now new NSAttributedString APIs that let you scale fonts in documents coming from the other platform to a more appropriate size:

  • NSAttributedString.DocumentAttributeKey.textScaling and sourceTextScaling
  • NSAttributedString.DocumentReadingOptionKey.targetTextScaling and sourceTextScaling

Text hyphenation:

Previously, you could configure hyphenation for each paragraph using NSParagraphStyle

Now, this can be enabled for the whole container using NSLayoutManager.usesDefaultHyphenation

Toolbars

Easier way to make standard toolbar buttons: NSToolbarItem.isBordered

Just create an NSToolbarItem with an image and set isBordered = true

Needs to be done in code though, no setting on the storyboard yet

Previously, you had to manually create an NSButton, configure it appropriately and add it as a custom view to the toolbar item

Using NSToolbarItem directly also allows you to make use of its built-in functionality like automatic enabling/disabling

NSToolbarItem.title: allows you to make toolbar items that are buttons with a text label instead of an icon (this is different than the item label, which appears below the button)

NSToolbarItemGroup:

  • new convenience initializers that create a group with given item labels/icons in one line
  • can display items as a segmented control which collapses into a popup or pulldown menu when there isn't enough space (use the new initializers for this)

NSMenuToolbarItem – a button toolbar item that shows a pulldown menu, which can be any NSMenu (with separators, nested menus etc.)

Touch Bar

NSTouchBar.isAutomaticCustomizeTouchBarMenuItemEnabled – lets you enable/disable the "Customize Touch Bar…" entry in the Edit menu

NSApplication already has such property, but this is useful if you want to avoid referencing NSApplication in the Touch Bar related code, or if you don't have access to it (e.g. Catalyst apps don't)

NSStepperTouchBarItem – a new item type for selecing an item from a list, with left/right arrows

NSSliderTouchBarItem: minimumSliderWidth, maximumSliderWidth properties for defining min/max width (previously you could achieve this using AutoLayout constraints, but it was less convenient)

Other controls

NSSwitch – a new NSControl that looks like UISwitch on iOS

Not a replacement for checkbox

Avoid using it for small things and in large numbers, just one for some general mode switch, a "master toggle", like the Time Machine on/off switch

NSCollectionView – added compositional layout, diffable data source (same as on iOS)

Storyboards

Storyboards now let you use custom VC initializers for injecting dependencies:

@IBSegueAction func showFoo(_ coder: NSCoder) -> NSViewController {
    return MyViewController(params...)
}

This lets you use initializers in view controller classes that require passing any data the view controller needs, while still using storyboard segues to connect view controllers on the storyboard

AutoLayout

Controls like text labels, buttons with titles etc. calculate their intrinsic content size that lets the AutoLayout system scale the whole window layout depending on the text contents of those controls

However, in some scenarios (e.g. inside an NSGridView) you want all controls to have their size set externally, and the control's intrinsic size is ignored

The calculations to determine the intrinsic size are still performed though in such case, and now you can manually disable them (as an optimization) by setting these to false if you know the result will not be used in the layout calculations:

  • NSView.isHorizontalContentSizeConstraintActive
  • NSView.isVerticalContentSizeConstraintActive

NSResponder

There was a possible source of bugs before if you captured an instance of a UI-related class in a block in such a way that it could be later deallocated on a background thread:

let label = NSTextField(labelWithString: "...")

dispatch_async(dispatch_get_global_queue(0, 0)) {
    // ...

    dispatch_async(dispatch_get_main_queue()) {
        label.value = "done"
    }
}

If the outer block is released after the inner block (which you have no control over), then at that point the label object is deallocated on the background thread, and this could cause various hard to debug issues. This is now solved in 10.15 – the SDK guarantees that UI objects will be deallocated on the main thread in such scenario.

Privacy & security

Recording the screen will now require asking the user for permission

This doesn't apply to the new NSColorSampler control mentioned earlier, which runs out of process

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

You may run into problems if you were somehow subclassing or customizing them in a non-standard way

NSWorkspace

New asynchronous versions of existing methods for opening one or more URLs or applications

You can configure some aspects of opening using NSWorkspace.OpenConfiguration, e.g.:

  • if the user can control the process of opening a URL/app
  • if the opened app or document is added to the "Recents" menu
  • if the launched app is hidden on launch

iPad & Sidecar

With the Sidecar feature, an iPad can function as an additional screen for the Mac, and also as a drawing tablet

Tablet drawing events are sent as normal mouse events, with .tabletPoint subtype

Tablet events also provide pressure data (although unlike on iOS, you can't register to get retroactive updates for previous pressure)

Switching mode on the Apple Pencil by double-tapping can be handled by another event with event type .changeMode

Also an NSResponder in the responder chain can handle that event through a new changeMode(withEvent:) method

You can also listen to that event outside of the responder chain by using the "local event monitor" API:

let monitorId = NSEvent.addLocalMonitorForEvents(matching: .changeMode) { (event) -> NSEvent? in
    switchTool()
    return event
}

Foundation additions

View geometry:

New data types for specifying view geometry:

NSDirectionalRectEdge

NSDirectionalEdgeInsets

NSRectAlignment

These use the terms "leading" & "trailing" on the horizontal axis instead of e.g. "minX" or "maxX", so they work better with right-to-left languages

Formatters:

NSRelativeDateFormatter – allows you to format dates as e.g. "1 month ago" or "last week"

NSListFormatter – formats lists of things, adding commas properly (uses nested formatters from .itemFormatter to format specific items)

Changes in extensions

Non-UI file provider action extension

Network Extensions, DriverKit and Endpoint Security replace old kernel extensions

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