MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 19

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

View as index

Creating Independent Watch Apps

Categories: WatchKit 0 comments Watch the video

Watch apps can now be independent – iPhone app is now optional

Up to watchOS 5, the Watch app was embedded in the iOS app and both were downloaded to the iPhone. iPhone then handled the task of installing the Watch app to the Watch.

Now, in iOS 13 and watchOS 6, both apps are installed straight from the App Store directly on the relevant device, each device installs its own app. This applies to all apps in the store today. iOS app no longer has the Watch app bundled inside, it no longer counts towards your iOS app cellular download limit if you don't need it.

This also enables asset/variant thinning for Watch apps – if you have a Series 4 watch, only Series 4 assets are downloaded to the watch (previously the iOS app had to include all variants in the bundled Watch app for any possible watches).

All current apps belong to the category of “dependent apps”, i.e. the Watch app depends on the iOS app. If you install a dependent app on the Watch, the iPhone will automatically install the matching companion iOS app. (Watch app launch is blocked until the iOS app is installed.)

On the other hand, independent apps can live without their iOS counterpart installed. They are backwards-compatible with earlier OSes.

→ to make an app independent, check the checkbox “Supports Running Without iOS App Installation”

Watch-only apps – apps that do not even have an iOS counterpart – are also possible, they require watchOS 6.

Debugging in the simulator is now up to 10x faster, on the device up to 2x faster

Added text field control (WKInterfaceTextField) to let you implement sign in forms

Use WKAlertAction for terms & conditions

Sign In With Apple button (AuthenticationServices, WKInterfaceAuthorizationAppleIDButton)

Continuity keyboard for Watch ⭤ iOS – you can e.g. enter passwords to log in on the watch using your iOS device

For proper handling of logging in, set textContentType and associated domains

Getting health authorization is now also supported on watchOS

Watch is now a standalone push target, you can send user-visible notifications and background notifications straight to the watch

New APNs request header – apns-push-type: set to alert for user-visible notifications, and background for background notifications

→ required for watchOS

Notification service extension support for e.g. decrypting notifications

Complication pushes can also be sent straight to the watch (PushKit)

Networking: use URLSession, CloudKit

  • make sure to use background sessions
  • full CKSubscription / CloudKit notifications support

It’s preferred to use URLSession than WatchConnectivity

  • WC is still available for any iOS-to-watchOS specific communication, but only use it if you really need to
  • check isCompanionAppInstalled

SwiftUI Essentials

Categories: SwiftUI 0 comments Watch the video

Prefer smaller, single-purpose views

Put conditions inside modifiers when possible (flipped ? 180 : 0) – returning separate objects in if/else may create a separate hierarchy where SwiftUI needs to add/remove views when switching, and then by default it uses a fade animation to switch between those

It’s really easy to break your code into smaller pieces and refactor it, so do it often

All controls have adaptive behavior – they describe the role they serve, instead of how they look, and the framework adapts the look to the environment

Same Button() can be a navigation item button, an unstyled iOS button, a macOS push button, or even a context menu item (!)

Modifiers like .disabled() can be applied even to whole screens

SwiftUI On All Devices

Categories: SwiftUI 0 comments Watch the video

(*) marks APIs that were changed since the video was published

SwiftUI is the shortest path to building great apps on every device

SwiftUI is designed to accomodate many UI paradigms – it can be used with a mouse, touch, digital crown, remote, or assistive technologies

It has great support for any specific platform’s conventions out of the box

It was built with the help of experts from all devices, to make sure that on each platform it feels right at home

Learn once and use anywhere

Common elements that render in an appropriate way on every device

Common layout system that works the same way everywhere

More advanced elements like Picker

There’s no such thing as a “one size fits all app” – if you stick to the common denominator across all devices, you’ll be missing out on the things that make each device great, so you have to decide what the right design is for your app on each device

But you can share skills and tools and some code *where it makes sense*

When porting an app to a new platform, take a design-first approach: don’t just look at how to make the code run on the other platform, think about what is the right expression for your app on this device

tvOS:

Biggest, boldest screen

Often used together with friends

Used with Siri Remote, entire interface must be navigable using focus

Streamlined navigation

Rich, immersive experience – media, photos, videos, not performing tasks

Carefully consider which experiences make sense when viewed on a large screen

Here, we want to use: beautiful photos of landmarks, adding favorites, basic tourism information

We don’t want: lengthy historical information, advanced sorting & filtering, anything location-based

.focusable(canBecomeFocused) { isFocused … }  ⭢  set if element should be focusable and react to focus

.onPlayPauseCommand { }

.onExitCommand { }

Don’t use long, vertically scrolled and nested lists of text, use a navigation UI that emphasizes content (pictures)

Take advantage of the big screen

On iOS, when using both a navigation controller and tab controller, the tab controller is the top level and a navigation controller may be used in one or more tabs, so that when you navigate deeper the tab bar is still visible

On tvOS, the tab controller should be added *inside* navigation controller, so that you see tabs at the top on the root level, but when you select some content, the tabs disappear

macOS:

High information density – a large screen with relatively small fonts, so you can show a lot of information

Higher tolerance for large amounts of text

Precision pointing device allows smaller click targets and denser controls (within reason!)

Multiple window support

Keyboard shortcuts

Touch Bar

SwiftUI automatically adjusts padding/spacing to be appropriate for the Mac

Use .controlSize() for more compact controls

Menu item commands: (*)

view.onCommand(Selector(“showExplore:”)) { self.selectedTab = .explore }

Touch Bar:

view.touchBar(TouchBar {
  Button(action: show) { Image(…) }
  Button(action: delete) { Text(…) }
})

To reuse the same container view between platforms but customize e.g. the row type class, you can parametrize the container with the row type and add a rowProvider:

struct SharedLandmarksList<LandmarkRowType: View>: View {
  …
  var rowProvider: (Landmark) -> LandmarkRowType
}

struct MacLandmarksList: View {
  var body: some View {
    SharedLandmarksList() { landmark in
      return MacLandmarkRow(landmark: landmark)
    }
  }
}

double click  ⭢  onTapGesture(count: 2) { … } (*)

watchOS:

A good rule is to aim for important actions to be reachable with 3 taps or less

.digitalCrownRotation modifier

.listStyle(CarouselListStyle()) (*) – a scrolling list that focuses on each cell

Integrating SwiftUI

Categories: SwiftUI 0 comments Watch the video

(*) marks APIs that were changed since the video was published

Embedding SwiftUI views

A SwiftUI view can be put inside a native framework view by wrapping it in a NS/UI/WKHostingController:

UIHostingController(rootView: MyView())

You can also embed a SwiftUI view in a single UIView or NSView using UI/NSHostingView:

NSHostingView(rootView: MyView())

On watchOS, use a subclass of WKHostingController:

class MyHostingController: WKHostingController<MyView> {
    override var body: MyView {
        return MyView()
    }
}

Call setNeedsBodyUpdate() or updateBodyIfNeeded() to trigger a reload when some data changes

You can also use SwiftUI for notification content in a similar way, with a subclass of WKUserNotificationHostingController:

class MyNotificationController: WKUserNotificationHostingController<MyNotificationView> {
    var notification: UNNotification?

    override var body: MyNotificationView {
        return MyNotificationView(notification: notification!)
    }

    override func didReceive(_ notification: UNNotification) {
        self.notification = notification
    }
}

Embedding existing views into SwiftUI

For putting existing views inside a SwiftUI view hierarchy, use Representable protocols: NSViewRepresentable, UIViewRepresentable, WKInterfaceObjectRepresentable

View controller can be wrapped with: NSViewControllerRepresentable, UIViewControllerRepresentable

Lifecycle methods:

  • Make View/Controller (context:) – building the view or controller object
  • Update View/Controller (context:) – called once at first and then whenever view needs to be redrawn
  • Dismantle View/Controller (coordinator:) – optionally called when removing the view

context = NSView[Controller]RepresentableContext / UIView[Controller]RepresentableContext / WKInterfaceObjectRepresentableContext

Context has 3 properties:

  • coordinator – can be used to implement patterns like delegates, data sources, target/action between the two “worlds”
  • environment – the SwiftUI Environment
  • transaction – tells you if there was an animation

To have access to the coordinator, implement optional makeCoordinator method and build a coordinator (which is just any custom object), then in makeView() make the coordinator (pulled out from the context) a delegate of the UIKit view

Integrating with the data model

To integrate SwiftUI with your data model, use @ObservedObject (*) and ObservableObject (*) protocol, which includes an objectWillChange (*) publisher

Your data model + state should be the only source of truth in your app

Example publishers:

NotificationCenter.default.publisher(for: notificationName, object: self)

UserDefaults.standard.publisher(for: \.someSetting)

SomeNSObject.publisher(for: \.myKeyPath)

Publishers.Merge(publisher1, publisher2)

PassthroughSubject<Void, Never>() – just call send() when you need

Publishers need to publish on the main thread – use .receive(on: RunLoop.main)

Integrating with the system

A lot of system APIs are integrated by using view modifiers that make use of NSItemProvider objects:

Drag & Drop:

func onDrag(_ data: @escaping () -> NSItemProvider) -> some View

func onDrop(
    of supportedTypes: [String],
    isTargeted: Binding<Bool>?,
    perform action: @escaping ([NSItemProvider, CGPoint]) -> Bool
) -> some View

func onDrop(of supportedTypes: [String], delegate: DropDelegate) -> some View

Pasteboard:

func onPaste(
    of supportedTypes: [String],
    perform action: @escaping ([NSItemProvider]) -> Void
) -> some View

func onPaste<Payload>(
    of supportedTypes: [String],
    validator: @escaping ([NSItemProvider]) -> Payload?,
    perform action: @escaping ([NSItemProvider]) -> Void
) -> some View

Focus system:

Used on all systems, especially macOS and tvOS, but also on iOS and watchOS

Determines where to send digital crown events, where keyboard shortcuts or pasted content is sent, etc.

When an action like digital crown gesture is performed, SwiftUI checks which view is in focus, and if it has an action bound for this specific event; if not, it walks up the hierarchy to check if its superviews are interested

Custom views aren't focusable by default (unlike text fields etc.) – to make a view focusable, use the modifier:

.focusable(true, onFocusChange: { ... })

Reacting to commands:

There are some standard system commands, e.g.:

.onExitCommand { ... } (*) – menu button on tvOS, Esc on macOS

.onPlayPauseCommand { ... } (*) – play/pause button on tvOS

Generic command: (*)

.onCommand(#selector(Object.mySelector(_:))) { ... }

This can be used to integrate with e.g. AppKit toolbar buttons, menu bar commands

Undo & redo:

Access UndoManager through @Environment(\.undoManager)

Data Flow Through SwiftUI

Categories: SwiftUI 0 comments Watch the video

(*) marks APIs that were changed since the video was published

Every source of truth in the UI (= piece of information on which the UI depends) should only be defined in one place and used from there, not in many places which have to be kept in sync

A @State is a (private) source of truth handled and used by this specific view

→ it’s good to mark it as private

View is a function of state, not a result of a sequence of add/modify/delete events like in UIKit

Traditionally, you would modify views over the life of the app; in SwiftUI, you modify the state and data on which the UI depends

User interacts with UI  ⭢  action is performed  ⭢  action mutates some state  ⭢  system detects that the view needs to be re-rendered and updates it

The framework allows you (and encourages you!) to build small views that represent a single piece of data, that can be composed together

When an external event happens that needs to update the view, the change is funneled through the same chain as user actions

External events use an abstraction from Combine called Publisher

(they need to be received on the main thread!)

Read-only data (plain Swift properties, environment) is preferred when data does not need to be mutated.

Most of the time your data lives outside of the UI, so State normally has limited use.

If you find yourself adding @State, take a step back and think if this data really needs to be owned by this view. Perhaps it should instead be moved to a parent, or maybe it should be represented by an external source added as an ObservableObject (*)

Architecting Your App for Multiple Windows

Categories: UIKit 0 comments Watch the video

App delegate should still handle things like initializing/cleaning up the whole process

It should not however handle things like setting up the UI or updating it when the app goes into foreground/background – because this will now happen separately per session

Additionally, application delegate is now notified when a scene is created or discarded

Lifecycle delegate methods like willEnterForeground, didBecomeActive etc. will not be called at all if scene lifecycle is enabled

Modern lifecycle flow:

Starting the app:

  • AppDelegate: didFinishLaunching
  • AppDelegate: _:configurationForConnecting:options: -> UISceneConfiguration – configuration for the scene
  • SceneDelegate: _:willConnectTo:options: – this is where you set up your new scene

Scene configuration (UISceneConfiguration): an object that describes the delegate class, storyboard etc. It's either built dynamically, or (better) statically described in Info.plist and looked up by name using the constructor:

UISceneConfiguration(name: "Default", sessionRole: session.role)

Going to the background:

  • SceneDelegate: willResignActive
  • SceneDelegate: didEnterBackground
  • SceneDelegate: sceneDidDisconnect – after some time, the system will release your scene (including the delegate object and all views and VCs) to save memory; you should now release any objects loaded to build that piece of UI

When the user force-closes a scene:

  • AppDelegate: _:didDiscardSceneSessions: (may be called on the next launch instead)

“State restoration is no longer a nicety”

Watch out for cases where a view controller needs to be updated if there are multiple scenes presenting the same view, which you might not have taken into account before – it’s better to only update the model and observe changes in the model, instead of updating the view directly when the user adds some new content

  • pro tip: you can pass Swift-only objects like enums in NSNotification if you use them as the sending object instead of in userInfo

Targeting Content with Multiple Windows

Categories: UIKit 0 comments Watch the video

How to determine which scene to show e.g. when a notification is tapped?

The system needs to know which scene can handle what kind of input. This is described by an object called UISceneActivationConditions which includes some number of predicates:

  • canActivateForTargetContentIdentifierPredicate
  • prefersToActivateForTargetContentIdentifierPredicate

You can set this through the activationConditions property on UIScene.

The predicates:

  • should be of the form: self == ‘qwerty’
  • can include some comparisons, but without regexps, and can combine multiple conditions using NSCompoundPredicate
  • use NSPredicate(value: true) to allow any content

The default conditions for a scene are to allow any incoming content (canActivate) but not prefer any content (prefersToActivate), so if there’s any more specific scene handling that kind of content, it will go there instead.

Things that trigger opening your app can have a “target content identifier” assigned (any kind of string) which is then matched against the predicates:

  • push notifications (UNNotificationContent) can have a target-content-id
  • UIApplicationShortcutItem (used when launching an action from the home screen) has one too
  • NSUserActivity also adds a targetContentIdentifier field which can be set anywhere

Introducing Multiple Windows on iPad

Categories: UIKit 0 comments Watch the video

The user should be able to do everything from a single window if they want to – if your app *requires* using multiple windows, then something is wrong

In most apps there will be only one kind of window – you should be able to move from every starting point in each window to every other place in the app

An example of an app that has different kinds of windows is Mail, where you can open a new window for a composed message, but it’s a window dedicated to that new message and you can’t get back to the list inside it

Another example is Messages where you can pull out a conversation to a new window, and it’s a window only for that conversation

In every document-based app (e.g. Pages) the user may have the expectation that they can work with different documents in different windows

In non-document apps like Maps or Calendar you will also often want to be able to do two different things, see two different contexts or scenarios at the same time, and now you can do that by opening two windows

Ways to create a new window:

  • in the app expose, there’s a “+” button in the corner that creates new windows
  • when you have the app open and you drag its icon from the dock to the side, it creates a second window
  • in a lot of places in your app it might make sense to hold & drag an element like a tab to the side and create a window from it, but for that you need to implement drag & drop. Whenever you have a place where the user can drag & drop something and it would make sense to make a window from the dragged thing, it should be possible.
  • a common example is any master-detail view like Mail, where you can drag any list row (email) out to a new window
  • explicit action like an “Open in New Window” action in a context menu

UIWindowScene:

  • contains the user interface (UIWindow) of a single “window”
  • it’s created on demand for you and destroyed when unused

UISceneSession:

  • represents the state in which the UI in a given window is or was last time when it was persisted

Difference between scene and session: scenes are released to save memory, and when the user sees 4 windows of the app in the switcher, it’s possible that only one of them is currently active in memory – the others are stored as persisted sessions, and when selected in the switcher, they will be “unfreezed” and assigned a scene again

UIApplicationDelegate gets split up:

  • UIApplicationDelegate still processes lifecycle events about the whole app
  • UIApplication still stores the state of the whole app
  • any lifecycle events that concern only a specific scene are instead handled by UIWindowSceneDelegate using UIWindowScene & UISceneSession
  • methods like application will enter foreground/background, applicationDidFinishLaunching, application open URL etc. go to scene delegate

New state restoration API based on NSUserActivity:

  • UISceneSession.stateRestorationActivity
  • UISceneSession.persistentIdentifier – can be used to integrate with custom state restoration code
  • UISceneSession.userInfo – for storing simple settings like what element is currently selected or what mode an element is in
  • SceneDelegate: stateRestorationActivity(for scene:) -> NSUserActivity?
  • SceneDelegate: scene(_: willConnectTo session: options:) – initializer like in UIApplicationDelegate, options include userActivities for activities passed e.g. from Handoff
  • AppDelegate: application(_: didDiscardSceneSessions:) – use this if needed to clean up saved data for a session when it gets destroyed

How to implement multi-window support:

  1. 1. Check “Supports multiple windows” in the target settings
  2. 2. Set up Scene Configuration / Application Session Role list in Info.plist with one or more session roles (name of delegate class and storyboard file)
  3. 3. Implement the two necessary delegate methods in UIWindowSceneDelegate
  4. 4. If needed, add ways to create new windows, e.g. using drag & drop or custom buttons

APIs in UIApplication to programatically manage sessions:

  • requestSceneSessionActivation(_:userActivity:options:errorHandler:) – creates a new scene for a given user activity, or activates an existing session
  • requestSceneSessionRefresh(_:) – requests an update to a scene
  • requestSceneSessionDestruction(_:options:errorHandler:) – deletes a scene (you can select one of 3 animations)

Be prepared for this change to check some assumptions you might have made, that there will be only one instance of a given VC at a time etc… :)

Watch out for global variables, singletons and other shared data

Keeping UserDefaults up to date between scenes: use KVO observation on UserDefaults

UIApplication statusBar* and keyWindow methods and open(_: options: completionHandler:) callback are deprecated, replaced by equivalents on UIWindowScene (statusBarManager, interfaceOrientation)

  • this is even for apps that will not use multiple scenes