MacKuba

Kuba Suder's blog on Mac & iOS development

Notes

App ClipsAppKitExtensionsFoundationLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 14WWDC 16WWDC 18WWDC 19WWDC 20

What's new in Swift

Categories: Swift, WWDC 20 0 comments Watch the video

Improvements to binary size, especially in SwiftUI apps

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

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

Much better compiler diagnostics (warnings and errors)

New diagnostics subsystem that helps identifying code issues much more precisely

More actionable errors with guidance how to fix them

Improved code completion

Code completion better understands features such as ternary operator or keypaths

Drastically improved performance, up to 15x faster

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

Better error messages for runtime failures like arithmetic overflow

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

Cross-platform support:

Improved support for various distributions of Linux

Coming soon: initial support for Windows

Swift AWS Lambda runtime

New language features:

Multiple trailing closures

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

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

@main – marks the entry point to the program

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

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

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

@main
class Hello: Foo {
  …
}

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

Increased availability of implicit self in closures:

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

Multi-clause catch statements in do-catch

Automatic comparable conformance for (some) enum types

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

View builders improvements:

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

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

Apple Archive – new archive file format, .aar extension

Fast, uses multithreading

Includes a command-line tool and Finder integration

Swift API – AppleArchive framework

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

OSLog:

Improved performance

Support for string interpolations and formatting options:

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

Packages available on GitHub:

Swift Numerics

Swift Argument Parser

Swift Standard Library Preview

iPad and iPhone apps on Apple Silicon Macs

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

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

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

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

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

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

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

Possible compatibility issues:

Hardware:

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

UI:

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

System software:

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

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

Distribution for Mac devices works the same as for iOS devices

Apps can reuse existing subscriptions and in-app purchases

App thinning automatically selects most appropriate resources for the given Mac

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

TestFlight is (still) not available on the Mac

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

Design with iOS pickers, menus and actions

Categories: UIKit, WWDC 20 0 comments Watch the video

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

They replace a lot of previous use of action sheets

Disadvantages of action sheets:

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

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

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

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

Menu can have its own title at the top

It can also include separators between items

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

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

Example use cases for menus:

Disambiguation:

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

Navigation:

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

Selection:

  • “sort” button lets you choose the sorting field

Secondary actions:

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

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

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

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

Build with iOS pickers, menus and actions

Categories: UIKit, WWDC 20 0 comments Watch the video

Control design updates:

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

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

UIActivityIndicatorView: updated design, larger and with fewer lines

UIPickerView: updated design

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

UIPageControl

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

Color picker

UIColorPickerViewController – new color picker dialog like on macOS

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

func colorPickerViewControllerDidSelectColor(
  _ viewController: UIColorPickerViewController
) { … }

func colorPickerViewControllerDidFinish(
  _ viewController: UIColorPickerViewController
) { … }

Date picker

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

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

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

Menus

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

For UIButton: set button.showsMenuAsPrimaryAction = true

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

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

Looks at: backBarButtonItem.title, backButtonTitle, title

Implemented in UIControl

UIControl.showsMenuAsPrimaryAction

UIControl.contextMenuInteraction, isContextMenuInteractionEnabled

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

UIDeferredMenuElement – asynchronously provides menu items

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

Once loaded, menu items are cached

UIContextMenuInteraction:

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

Actions

UIAction is used in more places across UIKit

All controls can be constructed using a UIAction now

UIBarButtonItem:

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

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

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

Build trust through better privacy

Categories: Privacy, WWDC 20 0 comments Watch the video

Main rules of privacy at Apple:

1. On-device processing - process data locally without sending it to the cloud

Once the data is sent off device, the user loses control over who can access it and what they can do with it

It also requires additional work to secure the user data in transit and when stored in the cloud

For ML, Apple is adding more and more ways to train models on device so that you don’t need to send user data somewhere in order to update the models

Private federated learning – sending only anonymized updates to the model to the server

Used previously for: Quick Type, Hey Siri, now also photo sharing, dictation, HomeKit object detection

2. Data minimization - build user’s trust by requesting and using only the data that you actually need

New APIs introduced this year to let you access less data to achieve what you need:

For contacts, instead of requesting access to the full contacts database, you can set an appropriate textContentType on a text field (.emailAddress, .telephoneNumber) and then rely on Quick Type autocompleting contact info when the user is typing

3. Security protections of the data

This year Apple platforms are adding support for:

  • DNS over TLS
  • DNS over HTTPS

Server name (SNI) encryption in TLS handshake  ⭢  in progress

4. Transparency and control

Currently, privacy policy is required for all apps

Starting in fall 2020, when you submit an app to the App Store you will need to fill a form stating:

  • what kind of data you collect
  • how it is used
  • if it’s linked to the user
  • if it’s used to track users

This will be displayed on the app’s App Store page on all platforms

Remember, SDKs are a part of your app too 😅

"In the meantime, reach out to your SDKs' developers to make sure you understand how they may collect and use data"

On the web – ITP protects Safari users from trackers tracking their data, and now it will be able to present a “Privacy Report” showing what trackers specifically it has caught on each page

Clipboard usage:

iOS 14 will now make it clear when an app copies information put into the clipboard by another app (“Notes pasted from Messages”)

Camera and microphone:

An indicator will now be displayed in the status bar if an app is using either of these

In the control center, you will be able to see which app is using or has recently used camera or microphone

If you turn on camera recording immediately on launch, users may be surprised if they see the indicator immediately before they access any camera feature

Make sure that users understand why you are accessing camera, microphone or pasteboard

And again, remember that SDKs are a part of your app too and they may unexpectedly access these resources

Local network:

Apps can currently freely access a lot of information about the local network

This information can be used to build a profile about the user, learn their location or locate other people nearby

In iOS 14, when an app wants to access the local network, it will trigger a prompt to let the user grant or refuse access

Declare the Bonjour services you connect to in Info.plist

Provide a usage string and make sure that the user understands why they are seeing the popup

MAC addresses:

They are used to let devices identify other devices on the local network

However, they can be used to track devices and people

iOS 8  ⭢  MAC address randomization

  • randomizes address when scanning for WiFi networks
  • however, one fixed address is still used when you’re connected to a network

iOS 14: Private Wi-Fi Address

  • different random addresses are now used when connecting to different networks
  • address is changed within one network every 24 hours, or when you leave and re-join the network
  • setting can be changed in the WiFi settings

Network Interaction framework

  • gets distance and direction information to a nearby peer
  • uses the new U1 chip
  • e.g. for local multi-player games
  • app gets access to this information for the duration of a session, while the app is in the foreground
  • include a usage prompt

App Clips

  • app clips are cleaned up when not used for a period of time
  • new location confirmation API

Safari Extensions:

  • you can grant access for an extension to a specific website or only for one day

Tracking prevention for apps:

App Store policy will now require apps to ask for user’s permission to track their data across apps and websites owned by other companies

To share user data with other companies, your app must display a special prompt and only track the data if the user agrees

This includes tracking for:

  • targeted advertising
  • advertising measurement
  • sharing with data brokers

You’re tracking data across other companies if you share info about the user that can be identified by some kind of user ID, device ID, fingerprint, profile or IDFA

Asking for this permission is required to access the IDFA (IDFA returns 0000… if you don’t have permission)

Tracking permission is *not* required if:

  • linking is done only on user’s device and not sent anywhere
  • sharing with a data broker is done only for the purposes of fraud prevention or security (and only for your purposes, not the data broker’s)

Use the new AppTrackingTransparency framework to show the prompt

Requires NSUserTrackingUsageDescription usage key

User can choose to not be asked by any app – there’s a switch in the Settings named “Allow Apps to Request to Track” (replacement for existing “Limit Ad Tracking”)

Remember that users can change the setting at any moment:

  • call the AppTrackingTransparency every time your app is relaunched
  • don’t store or cache the IDFA
  • think about what changes you should make to stop tracking the user if they switch if off later

Advertising attribution

Tracking often only happens because someone is trying to answer an advertising-related question, like which advertising campaign is the most effective

SKAdNetwork – a new privacy-friendly system that lets you measure conversion without tracking specific users

Uses aggegation and on-device intelligence

Doesn’t require tracking permission

Lets the ad network learn which campaigns in which apps lead to downloads, but not who exactly is downloading each app

Meet the new Photos picker

Categories: Photos, UIKit, WWDC 20 0 comments Watch the video

PHPicker: new system-provided picker screen that gives you access to photos and videos from the user’s photo library

New design and new easy to use API

It’s recommended that you use this picker instead of building your own custom photo selection UI

New version includes:

  • an integrated search
  • easy multi-select
  • zoom gesture

PHPicker is private by default:

  • the picker screen runs out of process and talks to the app via XPC
  • your app has no direct access to the photos library
  • it doesn’t need to get photo library permission (don’t ask for it unless you *really* need it)
  • you only get selected photos and videos in response

Elements of the API:

PHPickerConfiguration – lets you specify limits and filters:

  • selectionLimit – number of items that can be selected (1 by default, 0 = unlimited)
  • filter – e.g. .images or .any(of: [.videos, .livePhotos])

PHPickerViewController – the main VC handling the picker

  • the picker doesn’t dismiss itself automatically, call picker.dismiss(animated:) when you get the response

PHPickerViewControllerDelegate – delegate for the picker

  • picker(_: didFinishPicking results:)

PHPickerResult – an array of these objects is passed to the app in response

  • get itemProvider from the result
  • check itemProvider.canLoadObject(ofClass: UIImage.self)
  • get the image via itemProvider.loadObject(ofClass: UIImage.self) { … }

You can normally extract picked photos from PHPickerResult item providers without touching the PHPhotoLibrary at all, but if you do need to access the photo library anyway, then pass it to PHPickerConfiguration.init and get assetIdentifier references from the picker results

If you use PHPicker with photo library access and you only got limited access to a subset of photos, then:

  • PHPicker will still let the user choose photos from their whole library
  • but the selection you have direct access to will not be extended by what they choose in the picker

The photo library APIs from UIImagePickerController are deprecated

Handle the Limited Photos Library in your app

Categories: Photos, UIKit, WWDC 20 0 comments Watch the video

Current full photo library access: your app has full access to all contents of user’s photo library, can read and write to the photo database

Limited mode: you only have access to “user’s limited library selection” – a kind of filter for the photo library

You can only fetch assets and resources related to what the user has chosen to give you access to

When the user updates the selection, your app is notified so you can update your UI

This affects all apps using photo library, even apps that are already live

When the user is asked for access to photos, they can choose: Allow access to all / Don’t allow / Select photos

Selected photo set can be changed in the Settings

If an app hasn’t made any changes to support this new mode yet, you will be asked if you want to change selection every time the app accesses the photo library after a restart

Users usually have thousands or tens of thousands of photos in their library, possibly from many years, but they almost never want an app to have access to all of them

Other ways to access the library

How can you use the photo library without requesting access to it at all?

If your app only needs to let you choose a photo for an avatar, or to post to a chat  ⭢  use system photo picker (PHPickerViewController)

  • replacement for UIImagePickerController
  • improved with search and multi-select
  • doesn’t require user to grant photo library access

If you only need to save photos and images to the library, you can ask for add-only access

Full access makes sense for apps like photo browsing and editing apps, camera or backup apps

New APIs

When asking for and checking authorization, you need to specify PHAccessLevel: .readWrite / .addOnly

PHPhotoLibrary.authorizationStatus(for: accessLevel)

If the user gave you limited access (only to a subset of photos), then authorizationStatus == .limited

When requesting access, you can just send a request to the photo library when the user performs an action that requires it, and the popup will appear automatically

If you want to request access explicitly without making a request, do:

PHPhotoLibrary.requestAuthorization(for: accessLevel) { … }

Older authorizationStatus() and requestAuthorization(_ handler:) APIs will be deprecated and will just return .authorized

Other APIs should work the same regardless of access, they will just return fewer items if access is limited

A few exceptions:

  • assets created by your app are automatically accessible to your app
  • you can’t create or fetch user’s albums in limited mode
  • no assets to cloud shared assets or albums

If you want to let the user update the selection:

  • call PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController)
  • monitor changes through PHPhotoLibraryChangeObserver
  • you may want to add some new button in the UI that lets the user access this screen

To prevent the selection alert from appearing after a relaunch, add the key PHPhotoLibraryPreventAutomaticLimitedAccessAlert to Info.plist

Steps to take when updating:

  1. 1. Reconsider if your app needs photo library access at all  ⭢  consider using system picker instead, or asking for write-only access
  2. 2. Adopt the new authorization APIs
  3. 3. Add a button which lets the user access the library selection UI if it makes sense
  4. 4. Add the Info.plist key to prevent the alert