MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 19

App ClipsAppKitCloudKitExtensionsFoundationiCloudLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 12WWDC 14WWDC 15WWDC 16WWDC 18WWDC 19WWDC 20WWDC 21

View as index

Advances in Collection View Layout

Categories: UIKit 0 comments Watch the video

Previously:

iOS ships by default with the UICollectionView flow layout, which is great in simple cases

However, doing custom layouts is very complex, requires boilerplate code, thinking about performance etc.

New in iOS 13 and macOS 10.15: Compositional Layout

Compositional layout is composable, flexible and fast

It works by taking small layout groups (line-based, i.e. using flow layout) and composing them together into bigger pieces

Available on iOS, tvOS and macOS (same API)

UI/NSCollectionViewCompositionalLayout  ⭢  NSCollectionLayoutSection  ⭢  NSCollectionLayoutGroup  ⭢  NSCollectionLayoutItem

NSCollectionLayoutSize – defines size (width + height) for an item or group:

.fractionalWidth(0.5) – half of the width of the container

.fractionalHeight(0.5) – half of the height of the container

.absolute(200)

.estimated(200) – tells iOS how much you think it’s going to be, but the final value is calculated from AutoLayout

Both width and height can be proportional to either container’s width or height

NSCollectionLayoutItem – specification of a single cell type, with a definition of size, insets etc.

NSCollectionLayoutGroup – basic unit of layout, usually some row or column of items

  • can be horizontal, vertical or custom
  • has its defined size and contains a number of items
  • custom group includes a NSCollectionLayoutGroupCustomItemProvider
  • can contain other nested groups as items

NSCollectionLayoutSection – section of the collection view, like before

  • includes a single group

UICollectionViewCompositionalLayout – sets up the layout for the UICollectionView, manages whole view

  • includes a section or a section provider (closure)
  • section provider takes section index and a “layout environment” object and returns a specification for that section

NSCollectionLayoutSupplementaryItem – generic supplementary item like a badge on the item

NSCollectionLayoutBoundarySupplementaryItem – an item that can stick to the edge when scrolling (header/footer)

  • can be added to a section or the whole layout

NSCollectionLayoutDecorationItem – an item that is displayed in the background of a section

NSCollectionLayoutAnchor – specifies how a supplementary item is anchored to its containing item/group

  • includes edges (top, leading etc.) and offset from the edges (fractional)

Supplementary and decoration item types have to be registered using: layout.register(Klass.self, for…Kind: “background”)

Horizontal scrolling like in the App Store app sections:

section.orthogonalScrollingBehavior = …

  • continuous – scrolls to any position
  • continuousGroupLeadingBoundary – stops between groups
  • paging – stops at multiplies of the width of the container
  • groupPaging – shows each item of the main group in whole
  • groupPagingCentered – as above, but centers the group

Combine in Practice

Categories: Foundation 0 comments Watch the video

try*** variants (tryMap etc.) – accepts a throwing closure, captures thrown errors and converts them to stream failures

.decode(User.self, JSONDecoder()) -> <User, Error>

.assertNoFailure() -> <T, Never> – asserts if it receives a failure

Just(value) – just publishes this single value

.catch { otherPublisher } – if it receives an error, cancels the connection and subscribes to the fallback publisher instead

.flatMap { otherPublisher } – maps all values & errors to this publisher, returns whatever it outputs

.publisher(for: \.name) – like map { $0.name } ?…

.receive(on: RunLoop.main)

Subscribers:

.sink { x in … }

Subject: both subscriber and publisher, broadcasts to multiple subscribers, you can send() manually

PassthroughSubject: stores no value, so you’ll only see values some time after you subscribe

CurrentValue: stores last value, allows new subscribers to catch up

@Published var password: String  ⭢  property wrapper that adds a publisher

Future { promise in … promise(.success(data)) }  ⭢  publisher that does something asynchronously and returns a result once

Introducing Combine

Categories: Foundation 0 comments Watch the video

Combine doesn’t intend to replace notifications, KVO, target-action, delegates, etc.

Instead, it’s meant to be a common interface between them

Publisher: defines how values and errors are produced. (not necessarily the thing that actually produces them)

They’re simple descriptions, value types (structs)

Can be subscribed to by subscribers

Has two associated types: produced value and error type (use Never if it never produces an error)

Subscriber: it’s what receives the values from the publisher

Receives a value + completion if the publisher is finite

Subscribers mutate some state when they receive a value, so they’re reference types (classes)

Subscriber has an input type and error type (they must match publisher’s output and error types)

Flow of subscription:

  1. 1. Subscriber calls subscribe() on Publisher
  2. 2. Publisher calls receive(subscription:)
  3. 3. Subscriber calls request(_: Demand) to indicate how many items they can receive
  4. 4. Publisher sends those items via receive(_: Input)
  5. 5. Publisher sends receive(completion:) when finished

NotificationCenter.Publisher(center: .default, name: .notificationName, object: obj)

Subscribers.Assign(object: obj, keyPath: \.address)

Operators:

They are both publishers and subscribers

They subscribe to a publisher (upstream), transform the received values and pass them on to subscribers (downstream)

Declarative, value types (structs)

p = Publishers.Map(upstream: somePublisher) { return … }
p.subscribe(subscriber)

Operators can just be called on any publisher:

publisher.map { … }

Names of operators are generally taken from Swift standard library methods when possible (compactMap, filter, prefix…)

And publishers can be created from various classes like this:

NotificationCenter.default.publisher(for: .notificationName, object: obj).map { … }

Some subscribers can be created this way too:

publisher.assign(to: \.path, on: obj)

This returns a “Cancellable”, which unsubscribes everything when released

Zip3(a, b, c).map { a, b, c in … }

→ waits for a new element from each stream and combines them into a tuple

CombineLatest3(a, b, c).map { a, b, c in … }

→ caches last value and sends an updated tuple whenever any of the values changes

It’s ok to adopt Combine incrementally, you don’t have to go all in

Good places to use:

  • receiving NotificationCenter notifications with filter()
  • combining the result of several network operations with zip()
  • decoding JSON from a URL operation with decode()

Advances in Networking, Part 1

Categories: Foundation 0 comments Watch the video

Low Data Mode

User preference to minimize data usage

Can be set in the cellular menu and for each WiFi network separately

What it changes:

  • disables background app refresh
  • defers all discretionary background URL sessions
  • tells apps that they should use less data (it’s up to them to take this setting into account)

If you can save data without affecting user experience, then always do this regardless of this setting

Low data mode signals to your app that you should make some tradeoffs if you can save even more data while providing slightly worse experience

What you can do to save data:

  • reduce image quality
  • do less prefetching
  • synchronize less often
  • mark background tasks as discretionary, run URL tasks in the background
  • disable auto-play

But: do not block any user-initiated work, don’t ask for confirmation

URLSessionConfiguration.allowsConstrainedNetworkAccess

URLSessionRequest.allowsConstrainedNetworkAccess

  • if set to false, this tells the system that these requests should not go through if low data mode is on
  • in this case you’re going to get error.networkUnavailableReason == .constrained, and then you should try some other strategy (e.g. fetch a smaller resource)

Network framework: NWParameters.prohibitConstrainedPaths, NWPath.isConstraint (handle updates to the path later)

“Expensive” networks (allowsExpensiveNetworkAccess): no user-visible setting, should (usually) be set to true for any cellular network and WiFi when connected to a mobile hotspot

Use this property instead of checking explicitly if the connection is cellular, it will be more future-proof

Combine & Networking

DataTaskPublisher:

session.dataTaskPublisher(for: request)

Web sockets

let task = URLSession.shared.webSocketTask(with: URL(…))

task.send(.string(“Hello”))
task.receive { … }

In Network framework: both client and server support

Use NWProtocolWebSocket.Options() in parameters to NWConnection, create a listener using NWListener

Mobility improvements

iOS 7  ⭢  Siri using multipath TCP (requires server-side support, so can’t be enabled globally)

iOS 9  ⭢  WiFi assist – falling back to cellular if connection can’t be established on WiFi (but once it’s connected, it stays there)

iOS 11  ⭢  Multipath API available in the SDK

“We believe users should never have to turn off WiFi when they are walking out of their home”

WiFi signal is not consistently spread in all directions – it depends on what objects are behind you and the access point, and sometimes going a meter away can make a big difference

It’s hard to predict where the signal will be good just based on distance, so it’s a big challenge to decide in a predictable way when to use WiFi and when to fall back to cellular

Improved WiFi assist – cross-layer mobility detection – all components of the system provide the necessary information: WiFi and cellular hardware continuously provide information about the signal quality, Network framework and URLSession provide information about the status of requests

→ leads to improved flow recovery

Even when a flow has been established on WiFi and has started exchanging data, if later on the signal is reduced, iOS can decide to move the next request to cellular instead. So the result should be much rarer cases of applications getting stuck trying to load data over a bad WiFi

Do not try to predict which network the request is going to go through by doing a preflight using SCNetworkReachability, because now this can change ay any moment

Apple is now using multipath TCP in: Maps, Apple Music streaming

If you want to use it yourself, see multipathServiceType in URLSessionConfiguration. Useful in particular for services that users are likely to often be using while they’re leaving their home.

Advances in macOS Security

Categories: Mac 0 comments Watch the video

Defense in depth: there isn’t any single layer that can always perfectly protect you, so there are multiple layers of security, so if any single layer fails that doesn’t defeat the whole security of the system

Layers can delay the advance of the attacker, reduce the attack surface, create “choke points” that are easier to defend

Gatekeeper: designed to protect users from running malicious software, while allowing them to use the software they choose

What does Gatekeeper check:

  • does the app contain any known malicious content?
  • has the software been tampered with since it was signed?
  • does it meet the security policy configured on the computer?
  • first launch prompt  ⭢  does the user actually want to run this?

On Mojave, Gatekeeper runs the check on the 1st launch of quarantined software launched via Launch Services

Quarantine – a technology on macOS for marking files that arrived from some external source (website, airdrop, iMessage, email)

  • includes metadata about where the file came from
  • opt-in – the app has to opt-in to this, so e.g. when apps download their own updates they are usually not quarantined, except for sandboxed apps

Launch Services – a framework for finding and launching apps on macOS, used when launching apps from Finder, NSWorkspace, document handlers etc.

What does not use Launch Services: NSTask, NSBundle/dlopen, exec/posix_spawn

In macOS Catalina:

  • all new software must be notarized to pass Gatekeeper
  • all software is checked when first launched, even when launching through those non-LaunchServices methods
  • all software (even not quarantined) is checked for malicious content on every launch

"You can always choose to run any software on your system" – there will always be a way to run a specific piece of software that you want to run

“We want to make macOS just as secure as iOS, while still maintaining the flexibility that you’ve come to expect from your Mac”

Platform security is increasingly reliant on validity of code signatures; that means if code has no signature, it’s impossible to detect tampering

In a future version of macOS, unsigned code will not load by default, so:

  • sign and notarize all software
  • don’t modify signed applications and bundles
  • handle failures when loading libraries

Privacy changes:

Requires user confirmation for:

  • screen recording
  • keyboard input monitoring

Requires confirmation for access to:

  • Desktop, Documents, Downloads
  • iCloud Drive and third-party cloud storage
  • Removable and network volumes

But:

  • *not* required for creating new files, only for reading existing files
  • tries to understand intent, e.g. doesn’t ask if user double-clicked a file in Finder, or drag&dropped it, or used an open/save panel
  • declare handled CFBundleDocumentTypes with NSIsRelatedItemType to e.g. automatically have access to a subtitles file when opening a movie file

Purpose strings are accepted, but not required (NSDesktopFolderUsageDescription etc.)

Open and save panels always run out of process

Be careful with:

panel(_:userEnteredFilename:confirmed:)

panel(_:validate:)

panel(_:didChangeToDirectoryURL:)

Checking for readability without triggering a consent dialog: isReadableFile, isWritableFile, access()

Apps and other binaries that have previously been denied access to some kind of directory now appear automatically in the "Security & Privacy" access list, unchecked

Full disk access now required for access to Trash (except files that your app has moved there)

What's New in Safari Extensions

Categories: Extensions, Safari 0 comments Watch the video

Safari app extension can now be notified when something is blocked by the content blocker

  • SFSafariAssociatedContentBlockers list in Info.plist
  • contentBlocker(withIdentifier: blockedResourcesWithURLs: on page:)
  • notifications are batched, and you will only be notified about URLs listed in the access section in the plist

You can also get notified when the user navigates to a different page:

page(_: willNavigateTo url:)

Capturing a screenshot of the visible contents of a page:

page.getScreenshotOfVisibleArea { image in … }

Resources stored in the app extension bundle can be accessed by the browser and JS:

SFSafariExtension.getBaseURI { baseURI in
    tab.navigate(to: baseURI.appendingPathComponent(“index.html”))
}

Listing all browser windows and tabs:

SFSafariApplication.getAllWindows { window in
    window.getAllTabs { … }
}

page.getContainingTab()

tab.getContainingWindow()

Programatically showing popovers:

window.getToolbarItem { item in item?.showPopover() }

Dismissing popover: SFSafariExtensionViewController.dismissPopover

What's New in Core Location

Categories: 0 comments Watch the video

Instead of giving the user the option of Always / When in Use / Don’t Allow, current options are:

  • Allow While in Use
  • *Allow Once* – a temporary authorization
  • Don’t Allow

In order to get Always access:

  • you call requestAlwaysAuthorization() as before
  • if the user grants access “While in Use”, your app gets “provisional always authorization
  • your delegate gets back a response that "Always" access was granted
  • then your app tries to actually use location in the background
  • some location event is generated, and only then iOS asks the user (at an appropriate moment) if they want to give the app "Always" access location in the background

You can only try this once, and if the user says they’d prefer to stay at the “While in Use” access level, they will not be asked again

You can also ask for “While in Use” first and then upgrade to “Always” later after the user uses the app for some time

Instead of diving APIs into those requiring “Always” and those that don’t, now the rule is: if your app accesses location while the app is in the foreground (or has started to do so in the foreground + shows a blue bar after going to the background), then it does not require “Always” access, regardless of which API it uses

On Apple Watch: being in an active complication counts as being in use

Temporary authorization (“Allow Once”): your app gets “While in Use” access, but it reverts back to .notDetermined when the app stops being used

Updates to Beacon Ranging API:

CLBeaconIdentityConstraint

What’s New in MapKit and MapKit JS

Categories: Maps, UIKit 0 comments Watch the video

Maps Web Snapshots:

Snapshot service for generating static images of a map fragment (similar API has already existed in native MapKit since iOS 7 – MKMapSnapshotter)

URL like: http://snapshot.apple-mapkit.com/api/v1/snapshot?center=50.0,20.0&size=800x600 (requires API key)

20K free snapshots per day on the free plan

Maps automatically support dark mode on iOS and the web

For map snapshots, set traitCollection in options manually to get a dark snapshot and update the snapshot if needed when appearance changes

Showing/hiding specific categories of places on the map:

MKPointOfInterestFilter, MKPointOfInterestCategory

MKPointOfInterestFilter(excluding:) or MKPointOfInterestFilter(including:), or .excludingAll

Filtering search:

MKLocalSearchCompleter.pointOfInterestFilter, MKLocalSearch.Request.pointOfInterestFilter

Removing addresses from search results:

MKLocalSearchCompleter.filterType  ⭢  .resultTypes (.address / .pointOfInterest / .query)

MKLocalSearch.Request.resultTypes (.address / .pointOfInterest)

Results: MKMapItem.pointOfInterestCategory

(You might get results with a different pointOfInterestCategory than the filter you’ve set, because some places may have a primary and secondary category)

MKMultiPolygon, MKMultiLine – for creating groups of map overlays with the same settings (colors etc.)

MKMultiPolygonRenderer, MKMultiLineRenderer (whole group uses one renderer)

Overlays are now rendered as vectors, not bitmaps

Opt out with: MKOverlayPathRenderer.shouldRasterize = true

Parsing GeoJSON: MKGeoJSONFeature, MKGeoJSONDecoder

Indoor Mapping Data Format – built on top of GeoJSON

Camera boundary: puts a constraint on how far the user can move/zoom out, by specifying a region in which the map center must be contained

mapView.cameraBoundary / MKMapView.CameraBoundary

Specify using MKMapRect or MKCoordinateRegion

This is also enforced when moving the map programatically

centerCoordinateDistance

mapView.cameraZoomRange = MKMapView.CameraZoomRange(…)