MacKuba

Kuba Suder's blog on Mac & iOS development

Swift

App ClipsAppKitExtensionsFoundationLocationMacMapsPhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 19WWDC 20

View as index

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

Modern Swift API Design

Categories: Swift, WWDC 19 0 comments Watch the video

Your most important goal as an API designer: Clarity at the point of use

Make it obvious when reading the API what it’s doing and how to use it correctly

Apple will not use prefixes in new Swift-only frameworks

Be careful – imported names are brought into a single namespace, so a very general name will require your users to manually disambiguate names in case of conflicts

Structs vs. classes:

  • both have their roles
  • prefer structs by default over classes, unless you have a good reason
  • use classes if you need the reference semantics, reference counting and deinitialization
  • use classes if the value is held centrally and shared
  • use classes if there is a separate notion of identity and equality

RealityKit: entities are references, handles into shared objects that live inside the rendering engine, so classes make sense

Objects used to configure entities – locations, materials etc. – are mostly structs

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

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:

extension Command {
  static let showExplore = Command(Selector((“showExplore:”)))
  …
}

view.onCommand(.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  ⭢  tapAction(count: 2) { … }

watchOS:

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

.digitalCrownRotation modifier

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

Integrating SwiftUI

Categories: SwiftUI, WWDC 19 0 comments Watch the video

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 @ObjectBinding and BindableObject protocol, which requires a didChange 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.:

.onExit { ... } – menu button on tvOS, Esc on macOS

.onPlayPause { ... } – play/pause button on tvOS

Generic command:

.onCommand(someCommand) { ... }

Commands are defined using the Command type:

Command(#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, WWDC 19 0 comments Watch the video

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 a BindableObject