MacKuba

Kuba Suder's blog on Mac & iOS development

SwiftUI

App ClipsAppKitExtensionsFoundationLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 14WWDC 16WWDC 18WWDC 19WWDC 20

View as index

SwiftUI on watchOS

Categories: SwiftUI, WWDC 19, WatchKit 0 comments Watch the video

SwiftUI offers apps some capabilities that were not possible before, e.g. swipe to delete and reordering in lists, or an easy way to do custom graphics and animations

Fully integrated with WatchKit, both ways

SwiftUI allows you to use the same code on all platforms

However, Apple Watch is a very special platform; a watchOS app should not be just a tiny version of the iOS app, it should be specifically designed for the Watch by only picking the right elements of the experience

Building an Apple Watch app is building the whole experience, not just the main app, but also complications, notifications, Siri interface – depending on the app

Use subclasses of WKHostingController to embed SwiftUI views in an interface controller that WatchKit can use

.font(.system(.headline, design: .rounded)) – uses a rounded version of San Francisco

.listRowPlatterColor(topic.color) – sets the color of the list cell background

.listStyle(.carousel) – a list design which centers the currently focused item on the screen

.onMove { } – enables drag to reorder

.onDelete { } – enables swipe to delete

For notification UI, inherit from WKUserNotificationHostingController

When a notification is received (didReceive(_:)), the view body is automatically invalidated and reloaded

Using digital crown:

Fluent scrolling between the beginning and the end:

.digitalCrownRotation($binding, from:, through:)

→ lets you create some kind of custom scrollable container

Discrete values:

.digitalCrownRotation($binding, from:, through:, by:)

→ for building interfaces where e.g. some value moves up or down by 1 when scrolling

Going around in a circle:

.digitalCrownRotation($binding, from:, through:, by:, sensitivity:, isContinuous: true)

→ sensitivity says how fast it rotates

.focusable(true) – lets the user switch focus between elements; digital crown events go to the focused item

What's new in SwiftUI

Categories: SwiftUI, WWDC 20 0 comments Watch the video

App & scene definition:

The whole app can now be built with SwiftUI, including what was previously in app delegate:

@main
struct HelloWorld: App {
    var body: some Scene {
        WindowGroup {
            ...
        }
    }
}

A WindowGroup renders as a single window on iPhone and Watch, on iPad it supports the new multi-window scene system, and on the Mac it creates multiple Mac windows

Settings – creates Mac preferences windows:

#if os(macOS)
    Settings {
        PreferencesList()
    }
#endif

For document-based apps:

@main
struct ShapeEditApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: SketchDocument()) { file in
            DocumentView(file.$document)
        }
    }
}

Defining menu commands:

.commands {
    CommandMenu("Shape") {
        Button("Add Shape", action: { ... })
            .keyboardShortcut("N")
        ...
    }
}

New multiplatform Xcode project template that creates one SwiftUI app for all platforms

New “Launch Screen” Info.plist key

Lets you define launch screen elements like background, navigation bar, tab bar

Replacement for the launch storyboard for SwiftUI apps that don't otherwise have storyboards

New API for widgets:

@main
struct RecommendedAlbum: Widget {
    var body: some WidgetConfiguration {
        ...
    }
}

Complications for Apple Watch can now also be built using SwiftUI

Outlines (hierarchical lists):

List(graphics, children: \.children) { … }

Grids:

LazyVGrid(columns: [GridItem(.adaptive(minimum: 176))]) { … }
-> adapts number of columns to view size

LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) { }
-> fixed number of columns

Also horizontally loading grids

Lazy versions of the existing stack views: LazyVStack, LazyHStack

Toolbars:

.toolbar {
    Button(action: { ... }) { Label("Record", systemImage: ...)
    // placed automatically in an appropriate place

    ToolbarItem(placement: .primaryAction) {
        Button(...)
    }
}

ToolbarItem has various semantic names for placement like principal, confirmationAction, bottomBar

Labels:

Label(“Books”, systemImage: “book.circle”)
Label { Text(“…”) } icon: { Image(systemName: “…”) }
  • combines an icon with a title and shows them appropriately for the context

.help(“Does something”) – tooltips on macOS, voiceover comment on iOS

.keyboardShortcut(“X”)

.keyboardShortcut(.cancelAction / .defaultAction)

New support for focus on tvOS

ProgressView – spinner or progress bar

Gauge – on watchOS, the circular meter control from complications

.matchedGeometryEffect(id: albumId, in: namespace) – animates a view from one view hierarchy into another

.clipShape(ContainerRelativeShape()) – applies rounded corners matching the parent view

Text – custom fonts are now adapted to Dynamic Type

Images can be embedded inside text and are scaled with it

@ScaledMetric var padding – a value defined by you that is scales with Dynamic Type

App accent color can now be set in the asset catalog

Support for accent color on the Mac

.listItemTint() customizes the color for a section or item

Tint parameter in control styles: .toggleStyle(SwitchToggleStyle(tint: .accentColor))

Link(destination: url) { Label(…) } – a link label that opens a URL in Safari

@Environment(\.openURL) var openURL – provides a openURL(url) function

UniformTypeIdentifiers framework provides content type identifiers for drag & drop

SignInWithAppleButton()

SwiftUI views for: AVKit, MapKit, SceneKit, SpriteKit, QuickLook, HomeKit, ClockKit, StoreKit, WatchKit

Stacks, Grids, and Outlines in SwiftUI

Categories: SwiftUI, WWDC 20 0 comments Watch the video

Stacks

New lazy stack views: LazyVStack and LazyHStack

They work just like the existing horizontal and vertical stacks, but they render their content incrementally as it becomes visible

Useful for very long lists built using VStack/HStack that had performance issues previously

There’s no point using lazy stacks for small local stacks that just lay out single views that are all visible on the screen together

If you’re unsure, use standard HStack/VStack by default, and only use lazy stacks for long content that’s scrolling and becomes a performance bottleneck

Grids

New grid views: LazyHGrid and LazyVGrid

They lay out subviews in a grid like collection view

LazyVGrid(columns: columns, spacing: 0) { … }

Grids require a definition of columns:

Constant number of columns:

var columns = [
  GridItem(spacing: 0),
  GridItem(spacing: 0),
  GridItem(spacing: 0)
]

Adaptive columns, number depends on the window size:

var columns = [
  GridItem(.adaptive(minimum: 300), spacing: 0)
]

Lists and outlines

Lists are more than just stacks of content – they support selection and scrolling etc.

Lists don’t need a Lazy variant, because list contents are always loaded lazily

Lists can now show hierarchical trees of items (outlines)

To make an outline list, provide a “children” keypath:

List(graphics, children: \.children) { graphic in
    GraphicRow(graphic)
}

To make collapsible list sections, use an OutlineGroup:

ForEach(canvases) { canvas in
    Section(header: Text(canvas.name)) {
        OutlineGroup(items, children: \.children) { graphic in
            GraphicRow(graphic)
        }
    }
}

You can also implement other views with parts that the user can collapse and expand like in an outline using DisclosureGroup:

DisclosureGroup(isExpanded: $expanded) {
  Contents()
} label: {
  Label(“Name”)
}

or:

DisclosureGroup(“Label”) { contents }

Build document-based apps in SwiftUI

Categories: SwiftUI, WWDC 20 0 comments Watch the video

For document based apps, use the DocumentGroup scene as the main scene of the app:

DocumentGroup(newDocument: MyDocument()) { file in
  ContentView(document: file.$document)
}

The content view receives a binding to the document contents, so when it changes the contents, the system knows it was modified

Document based apps have a “Document Types” section in the Info.plist, where you declare Uniform Type Identifiers of document types associated with your app

Imported types  ⭢  documents from other apps or sources that your app is able to open

Exported types  ⭢  document types owned by your app

TextEditor() – built in text view type

The type that implements the document model conforms to FileDocument (for value types) and declares its own UTType instances that represent imported and exported file types:

import UniformTypeIdentifiers

extension UTType {
    static var exampleText: UTType {
        UTType(importedAs: "com.example.plain-text")
    }

    static let shapeEditDocument =
        UTType(exportedAs: "com.example.ShapeEdit.shapes")
}

Imported type needs to be a computed property, because the value returned from the constructor may change between calls while the app is running, depending on system configuration. Exported type can just be assigned once and stored.

The document type provides a list of types (own and generic) that it can accept:

static var readableContentTypes: [UTType] { [.exampleText] }

It also has methods for reading and writing its document to/from a file, which you need to implement:

init(fileWrapper: FileWrapper, contentType: UTType) throws { ... }

func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { ... }

In those methods, you can assume that the content type is one of those you declared as accepted by your app.

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

SwiftUI Essentials

Categories: SwiftUI, WWDC 19 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, WWDC 19 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, WWDC 19 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)