SwiftUI View Preferences Tutorial for iOS

Learn how SwiftUI view preferences allow views to send information back up the view hierarchy and the possibilities that opens up for your apps. By Andrew Tetlaw.

5 (13) · 2 Reviews

Download materials
Save for later
Share

Much has been said about the declarative, top-down nature of SwiftUI. You write a view, which contains other views, which contains further views, and so on. Relevant information flows from the root view down to the descendant views; a list knows about all of the entries it contains, but a single cell only knows about one entry, and the name label in that cell only knows about a single piece of text. Now and then, however, it’s really useful for an ancestor view to get some information back in the opposite direction. There’s no responder chain, delegates or superview referencing in SwiftUI like there is in UIKit. Instead, there are SwiftUI view preferences. At first glance, it’s an oddly named, sparsely documented and easily dismissed part of the framework. But view preferences are the solution to sending data the “wrong” way in SwiftUI.

In this tutorial, you’ll learn how to use view preferences to achieve the following:

  1. Report layout information from child views to ancestors.
  2. Use that information to position views from separate parts of the hierarchy relative to each other.
  3. Create a dynamic view overlay which updates with your main view automatically.
  4. Encapsulate a dynamic overlay into a custom view modifier.
Note: This intermediate-level tutorial assumes you’re comfortable using SwiftUI and that you have previously built an iOS app using Xcode and Swift.

Getting Started

Meet Buzzy, a sweet new game you’re developing! Buzzy is an intrepid worker bee looking for pollen-filled flowers, and she needs the player’s help to find them. Once you’re done, this game is sure to create a buzz.

Download the starter project using the Download Materials button at the top or bottom of this tutorial. You’re going to take a quick tour around the project to get familiar with it.

Exploring Buzzy

Build and run the project, and you’ll see Buzzy hovering near her hive, waiting for directions. Below her, there’s a field of flowers.

Buzzy opening game screen

ContentView is the main view of the app. It contains a Bee, a Hive, and a FlowerField, which contains a number of Flowers. Each flower has a unique identifier.

Right now, tapping a flower just prints that flower’s ID to the debug console. What you want it to do is tell Buzzy to fly down to the flower you tapped. To make that work, you’ll need the flower’s frame within the game screen. You also want the number of flowers to be dynamic and their layout unpredictable.

Your head might be abuzz with ways to do this! The bee is part of the main content view, as is the flower field. How do you tell the bee where to go when the player taps? SwiftUI is doing layout for you — will you need to take over that work and generate the layout and push that data down from the top? You could. And that’s probably the style you’re used to. But you’ll learn how to do this a different way, an opposite way.

Creating View Preferences to Switch Data Directions

The most common direction data flows in SwiftUI is top-down — for example, @Environment and @State. If you want a bidirectional data flow, you can use bindings, but you still have to pass the binding down from the top. Enter view preferences. View preferences offer a different way for data to flow, and you may have already used them.

If you’ve ever used preferredColorScheme(_:) on a view, you’ve used view preferences. In preferredColorScheme(_:), the child view communicates up to a presenting ancestor view, sending a color scheme preference. That’s the only publicly documented preference key, but there are undoubtedly others in use behind the scenes. Anywhere you specify information on a view that actually takes effect further up the tree (for example, adding toolbar items), SwiftUI is probably using the preferences mechanism behind the scenes.

How do preferences work? Imagine the view hierarchy of an app: At the top sits the queen view and down at the bottom are all the worker views. View preferences allow those little worker views to raise their hands and make suggestions. If the queen is listening, those suggestions will be heard.

There are three parts to making and using a view preference:

  1. Defining the preference key and the type of value it represents.
  2. Reporting a value for the preference key by a child view.
  3. Listening for those values on an ancestor view.

First, you’ll define the preference key.

Making a Preference Key

Start a new Swift file called TargetPreferenceKey.swift and add the following:

import SwiftUI

The value type you’ll need for the preference key needs to store an identifier for the flower and information for its frame:

struct TargetModel: Equatable, Identifiable {
  let id: Int
  let anchor: Anchor<CGRect>
}

TargetModel is a simple structure that stores an Int to match the flower’s ID and an Anchor containing a CGRect for the bounds of the view.

Anchor is a convenient type for obtaining geometry information from a view. You can use that information in different contexts to obtain a specific, relative geometry value for that view. You’ll see how useful it can be shortly.

TargetModel conforms to Equatable and Identifiable which you’ll need to support observing changes in the value.

Next, add a new PreferenceKey:

// 1.
struct TargetPreferenceKey: PreferenceKey {
  // 2.
  static var defaultValue: [TargetModel] = []
  // 3.
  static func reduce(
    value: inout [TargetModel], 
    nextValue: () -> [TargetModel]
  ) {
    value.append(contentsOf: nextValue())
  }
}
  1. Your preference key must conform to PreferenceKey.
  2. The first requirement of the protocol is a static defaultValue to set the initial value for the key.
  3. The second requirement is the static reduce(value:nextValue:). Whenever a view sets a value for a preference key, SwiftUI calls this method, passing in the key’s current value as an inout. It’s your job to call nextValue to obtain the new preference value and store it appropriately. The plan is that each flower will set a value for the key, reporting its ID and Anchor. The reducer gathers all of the values into a single array, which becomes the value for the preference key.

Your key’s type — TargetPreferenceKey.self in this case — is its global identity. There is only one value for the key across your entire app. Any number of descendant views may set a value for the key, and any number of ancestor views may be observing the key.

SwiftUI calls reduce(value:nextValue:) in view-tree order, building a single value from every view that reports a preference value. Reporting a preference value is what you’re doing in the next section.