MacKuba

Kuba Suder's blog on Mac & iOS development

Foundation

App ClipsAppKitExtensionsFoundationLocationMacMapsPhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 19WWDC 20

View as index

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

Combine in Practice

Categories: Foundation, WWDC 19 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, WWDC 19 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, WWDC 19 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.