MacKuba

Kuba Suder's blog on Mac & iOS development

Notes

App ClipsAppKitExtensionsFoundationLocationMacMapsPhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 19WWDC 20

Streamline your app clip

Categories: App Clips, UIKit, WWDC 20 0 comments Watch the video

(Watch the “Explore app clips” and “Configure and link your app clips” videos before.)

App clips are designed for speed of interaction – interaction needs to be focused on a specific goal and essential tasks

Leave any advanced or complex features for the main app

App clip should be usable immediately when launched

  • include any assets that will be needed immediately in the bundle
  • don’t add a splash screen
  • don’t ask people to log in or sign up before they can perform the task they want
  • ask for permission to access data only if it’s required and only at the moment when you need it

The main app should present the same experience in the same way

The app clip should have the same name and icon as the app

Organizing the project:

Keep in mind that the smaller the app clip bundle is, the faster your users can launch it, so only include what you really need in the bundle

Put shared assets in a separate shared asset catalog

Use Sign in with Apple, ASWebAuthenticationSession or password autofill for logging in

Consider giving an option to upgrade the account to Sign in with Apple if the user logs in using a password

App clips are ephemeral – users should be able to try them out easily while trusting that their privacy is being protected

Some user data is not accessible

You can ask for permission to camera, microphone and Bluetooth

Location access

Don’t ask for location access to determine which physical store the user is in – encode the store ID in the URL

To make sure they’re in the right store, you can use a new API to confirm the location that you already assume you know:

  • add NSAppClipRequestLocationConfirmation to NSAppClip dictionary in the Info.plist
  • get a payload object from userActivity.appClipActivationPayload
  • run payload.confirmAcquired(in: region) { … } (you can set a radius of up to 500m)

Notifications

Ephemeral notifications: you can ask for permission to send the user notifications up to 8 hours after the app clip is accessed

This does not show a regular popup asking for permission (user can refuse access in the initial app clip sheet)

Add NSAppClipRequestEphemeralUserNotification to the dictionary

When ephemeral access is granted, authorizationStatus in UNUserNotificationCenter will be set to .ephemeral

Migrating to the app

After the app clip is activated, a banner is shown at the top of the screen for a moment that leads you to the app’s page in the App Store

You can also use a new SKOverlay from StoreKit to show a popup recommending the app to the user

.appStoreOverlay() in SwiftUI

Do this after they finish the task

Passing data from the app clip to the app: use an app group container

You can also pass Sign in with Apple authorization:

  • get the user ID authorization.credential.user from the Sign in with Apple authorization success handler
  • save it as a file inside the group container (containerURL(forSecurityApplicationGroupIdentifier: “…”))
  • on the app side, read the file and pass it to ASAuthorizationAppleIDProvider: getCredentialState(forUserId: user)

Configure and link your app clips

Categories: App Clips, UIKit, WWDC 20 0 comments Watch the video

(Watch the “Explore app clips” video before.)

App Clips provide an entry point for your users with minimal friction

Touching a physical NFC tag  ⭢  NFC tag encodes a URL  ⭢  your app’s clip configured for that URL shows up as a popup  ⭢  when user presses Open, it’s open in full screen

App clips can also be accessed from: QR codes, Maps, Safari (Smart App Banners), Messages, Siri suggestions

Tools will be available later this year that can create special circular app clip QR codes

Steps to configure:

Configure your web server

Set up the apple-app-site-association file (in .well-known folder)

Add an appclips key:

“appclips”: {
    “apps”: [“ABDE12345.com.example.app”]
}

Configure the app clip for handling links

  • add an associated domains entitlement to the app clip target
  • add a domain prefixed with appclips:, e.g. appclips:example.com
  • handle opening the app with NSUserActivity containing a given URL
  • in SwiftUI: .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { … }
  • in UIKit: scene(_: continue userActivity:)

Once the user has the full app installed, opening the same URL opens the full app instead of the clip – so the app needs to be able to handle the same URLs and show the same content

When testing in Xcode, you can set up a scheme that sets the environment variable _XCAppClipURL to the given URL

Configure app clip experiences on App Store Connect

Prepare metadata for the app clip:

  • Title up to 18 chars
  • Subtitle up to 43 chars
  • Header image: 3000×2000, aspect ratio 3:2, jpeg or png with no transparency

Set up on App Store Connect in a new section that appears once you upload a build with an app clip

If you want the clip to be accessible from more than Safari or Messages (e.g. QR codes), you need to set up “Advanced App Clip Experiences”

One app can set up multiple advanced app clip experiences matching different URLs

URL is matched based on the most specific prefix, so you can have one general catch-all URL and other more specific ones

App Clips can also be accessed by TestFlight testers – configure in “App Clip Invocations” sections in TestFlight on App Store Connect

Configure the Smart App Banner

Standard app banner:

<meta name='apple-itunes-app' content='app-id=12345678'>

For an app clip, add app-clip-bundle-id inside:

<meta name='apple-itunes-app' content='app-clip-bundle-id=com.example.fruta.clip, app-id=12345678'>

Explore app clips

Categories: App Clips, UIKit, WWDC 20 0 comments Watch the video

App clip is a part of an app – you need to have an app to add an app clip

App Clip Experience – a specific URL configured to display an app clip popup sheet on iOS 14 when opened in some app

App clip itself  ⭢  an on-demand binary

App clip experience is a new entry point to your app

Configured in a similar way as Universal Links, + additional setup in App Store Connect

App clip appears whenever a user accesses that URL: through a QR code or NFC tag, links in Safari and Messages, places in Maps

Also through Siri suggestions

Coming later this year: special Apple-designed circular QR codes for app clips

App clip

Created as another target in your app project in Xcode

Uploaded to App Store Connect together with the whole app

Once the app containing an app clip is released, the app clip will be downloaded separately when its URL is opened by the user

→ if the full app is already installed, it will be launched instead, so it must be able to do everything that the clip does

App clips are designed to be downloaded quickly in the background, so they should be as small as possible

App clip bundle must be less than 10 MB after thinning

Include only what is needed immediately, and download other assets and data from the internet when needed

Include only a minimal part of the app UI in the app clip, only what will be required in this specific context where the app clip will be used

App clips should generally not include top level parts of your navigation flow, like tab bars; also things like profile screens

They should deep-link to the specific screen in the app that they are concerned with

Use separate URLs for any distinct experiences, each app clip experience should focus on one thing

E.g. if you have multiple physical stores, have separate URLs for each store so that you can skip a “Choose store” screen

An app clip receives an NSUserActivity with the given URL when launched

App clips can use any frameworks from the iOS SDK (including Apple Pay and notifications), and use UIKit or SwiftUI app lifecycle

Access to some sensitive user data is limited (e.g. no access to HealthKit)

New location confirmation API – lets you confirm if the user really is where you think they are

You can migrate data from the app clip to the full app using a group container

→ group container is deleted when the data is migrated

Autorizations for camera, microphone and Bluetooth access are automatically migrated to the app when installed

App clip and its storage are deleted from user’s device after a period of inactivity (~ several days)

App clips also aren’t included in backups

Any app clip data should be treated more like a cache

If a specific app clip is used frequently, iOS will keep it on device for a longer period after last use

App clip can’t register as document or URL scheme opener, it can only be opened using the URL configured in the app clip experience

They also can’t include their own extensions, like content blockers

SKOverlay or .appStoreOverlay in SwiftUI – a standard UI for inviting your users to the full app

Use ASAutthorizationController if you need a way to log the user in to an account

What's new in watchOS design

Categories: WWDC 20, WatchKit 0 comments Watch the video

Watch apps should be:

  • lightweight – limit the amount of functionality that you make available in the Watch app
  • actionable
  • discoverable – make important actions easy to find

All actions that were previously hidden inside Force Touch menus have now been moved into the main views of their apps (e.g. Activity settings, Weather modes, Mail’s new message)

Where to put such secondary actions now?

Sorting (example from Activity): a button at the top / first table cell that opens a modal with a sorting mode selection

View switcher (example from Stocks): a button at the top that opens a modal which lets you choose if you want to see points/percentages/market cap

→ in SwiftUI – Picker()

Swipe actions (World Clock): a delete button when you swipe a row, an add button at the bottom

→ SwiftUI: .onDelete()

Actions in a full screen view like Maps or Camera: a “more” button in the corner that opens a modal with a list of actions or options

→ SF symbol “ellipsis”

If there’s only one action, just make a button with an icon of that action (Photos app)

→ white circle at 85% opacity, 1pt black outer glow at 50% opacity

“More” button inside table cells – Workouts app

→ don’t put primary actions inside “More” menus!

Action buttons at the bottom of a scroll view – Calendar app:

  • on an invite page, buttons to accept/decline an event or to email the sender
  • on your own event, a button at the bottom to delete the event
  • for destructive actions, make the label text red and add a confirmation dialog

New “toolbar” button (Messages) – a button that appears at the top only when you scroll up, but is initially hidden

→ SwiftUI: .toolbar()

Switching between folders or groups (Mail, Home) – hierarchical navigation where you start inside a default folder, but can move up to root level

Adopt the new look of macOS

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

macOS has an all new design

A new toolbar with inline title, big bold controls and integration with the window’s split view

Full height sidebars with colorful icons and updated symbol iconography

New appearance for lists/tables using a new inset selection style

Most of these changes happen automatically to your app (if you’re using classes like NSToolbar and NSSplitViewController)

Sidebars

Sidebar: use NSSplitViewController, items configured using NSSplitViewItem.Behavior.sidebar

Use full size content window mask – NSWindow.StyleMask.fullSizeContentView, so that content is laid out in the space normally taken by the title bar

NSView properties to find the safe areas of the view: safeAreaInsets, safeAreaLayoutGuide, safeAreaRect

Also available on the storyboard (“Safe Area Layout Guide”)

New view item in the library: Window Controller with Sidebar (NSWindowController + NSSplitController)

Opting out of the full height sidebar: NSSplitViewItem.allowsFullHeightLayout

  • when sidebar is typically collapsed, or when you need more space for the toolbar

By default all sidebar icons are colored with the accent color

Use NSOutlineViewDelegate method outlineView(_: tintConfigurationForItem:) to customize

Return an instance of NSTintConfiguration:

  • .default – always uses (system) accent color
  • .monochrome – gray monochrome
  • init(preferredColor:) – use this color when default (“rainbow”) accent color is used, but follow the accent color if it’s customized
  • init(fixedColor:) – a fixed color that is always used (e.g. the yellow star in Mail’s VIP folder)

Use sidebar colors to distinguish different sections of the sidebar, or highlight a specific item like the VIP star

Or use monochrome to de-emphasize groups

Toolbars

There’s no longer any special material behind the toolbar items, it’s a uniform part of the content of the window

Works automatically

New toolbar styles, controlled through NSWindow.toolbarStyle

Types of NSWindowToolbarStyle:

.unified

  • the new standard – like in the new versions of system apps
  • larger controls, bold icons
  • inline title located at the leading edge of the title bar next to the sidebar
  • good choice for most windows

.unifiedCompact

  • more compressed style
  • regular sized controls, smaller toolbar height
  • this is what was previously used if the window was configured to hide the title bar
  • now supports an optional inline title
  • use when user’s focus should be on the content of the window and there aren’t many elements in the toolbar

.preference

  • specifically designed for preference windows
  • automatic when you’re using NSTabViewController with the .toolbar tab style

.expanded

  • what used to be the standard layout of the toolbar
  • title is centered on top of the toolbar and can expand across the window
  • large button icons with labels below
  • use when the window title is long, or the toolbar is heavily populated with items, or when you want to keep existing toolbar layout

.automatic

  • default value
  • determines the toolbar style based on your window structure
  • existing apps linked on older SDKs keep their old layout

Toolbar buttons no longer have a border, a shape only appears when hovering

Controls with text fields show a slight border so that you know where you can click to focus

NSToolbarItem minSize & maxSize properties are deprecated – macOS can automatically give your controls a proper size

You can still use constraints if necessary

New NSWindow.subtitle property – shows a smaller subtitle under the window title, e.g. the message count in Mail

In .expanded style it appears next to the title

Controls like back/forward buttons should be put on the leading edge of the title bar, before the title

Set NSToolbarItem.isNavigational to position them there

Users can add and remove them from the toolbar, but can only put them in the leading edge area

NSSearchToolbarItem:

  • new toolbar control for search text fields
  • appears as a text field if there’s enough space, otherwise collapsed into an icon
  • searchField property
  • works on older versions of macOS

NSTrackingSeparatorToolbarItem:

  • a separator that extends the separator line of the window content’s split view upwards through the toolbar
  • when creating, you pass it the split view to track and the index of its divider

To position items in the title bar area of the sidebar, include an item NSToolbarItem.Identifier.sidebarSeparator and add those items *before* the sidebar separator item

The toolbar has no border below, but a slight shadow appears below it to separate it from the content when the content is scrolled

  • this happens if the scroll view fills the frame and you’re using .fullSizeContentView
  • otherwise, there will be a separator regardless of the scrolling position
  • customize toolbar separator in the window using NSWindow.titlebarSeparatorStyle, or in split view per section using NSSplitViewItem.titlebarSeparatorStyle

Controls

New modern design of controls like popup buttons, sliders, segmented controls

New multicolor system accent color – uses each app’s preferred accent color

Define the app’s global accent color in the asset catalog (can be different for light/dark mode) + set name in build options

People can still chose one of the previously available accent colors, and then that color overrides your setting so they can use that color everywhere

It’s preferred to use named colors like controlAccentColor, selectedContentBackgroundColor, keyboardFocusIndicatorColor instead of explicitly using your own color for controls

New .large control style, e.g. when you need one large action button

Works for: a few kinds of buttons, text fields, search fields, segmented controls

Also used in the unified toolbar style, and in system alerts

New inset style for table selection

Adds extra padding, taller default row heights

NSTableView.style:

  • .automatic (default)
  • .fullWidth – edge to edge selection background, like previously
  • .inset
  • .sourceList – new appearance of sidebar source list

Automatic style uses .inset by default (on apps built on the latest SDK), .fullWidth in bordered scroll views, .sourceList in source lists

→ you can check the effectiveStyle property to see what style is actually used

The old SelectionHighlightStyle.sourceList setting is deprecated

Text

System text styles are now available (Large Title, Headline, Body etc.) – but without Dynamic Type, so they have one constant size

NSFont.preferredFont(forTextStyle: options:)

NSFontDescriptor.preferredFontDescriptor(forTextStyle: options:)

Symbol images

SF Symbols is now available on the Mac

They can scale to different font sizes and weights

Toolbar and sidebar items automatically configure symbol images to match the size & style of the container

NSImage.init?(systemSymbolName: accessibilityDescription:)

It’s best to use them inside NSImageView (see symbolFont, symbolScale properties)

To customize symbol configuration, use NSImage.withSymbolConfiguration(…)

Most of existing named system images now return some kind of symbol image from SF Symbols

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