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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Setting the Child’s View Preference Key Value

You’ll now add a way for each flower to report a value for TargetPreferenceKey. Open Flower.swift and add the following modifier after the onTapGesture closure:

.anchorPreference(key: TargetPreferenceKey.self, value: .bounds) { anchor in
  [TargetModel(id: id, anchor: anchor)]
}

anchorPreference(key:value:) is a convenient view modifier that allows you to obtain an Anchor for the view and transform it into a value you can store in a preference key. In the code above, you’re taking the view’s bounds as the Anchor and returning an array of [TargetModel], which is what your preference key will append to the global array for the key. If you were interested in single points instead of the entire rectangle, there are also anchors for the center, top, bottom and so on which return single points.

OK. You have two out of three pieces in place. The last piece is the handler to observe changes to your preference key.

Adding a Handler to Observe Preference Key Values

Open ContentView.swift and add the following modifier to ZStack in body:

.onPreferenceChange(TargetPreferenceKey.self) { value in
  print(value)
}

This is how you define your preference key handler. Now, each time the key’s value changes, SwiftUI calls this closure. Right now, all the closure does is print the value.

Build and run. Watch the Xcode console and you’ll see the value printed:

[
Buzzy.TargetModel(id: 0, anchor: SwiftUI.Anchor<__C.CGRect>(...)), 
Buzzy.TargetModel(id: 1, anchor: SwiftUI.Anchor<__C.CGRect>(...)), 
Buzzy.TargetModel(id: 2, anchor: SwiftUI.Anchor<__C.CGRect>(...)), 
Buzzy.TargetModel(id: 3, anchor: SwiftUI.Anchor<__C.CGRect>(...)), 
Buzzy.TargetModel(id: 4, anchor: SwiftUI.Anchor<__C.CGRect>(...)), 
Buzzy.TargetModel(id: 5, anchor: SwiftUI.Anchor<__C.CGRect>(...))
]

You can see an ID value for each flower, then a lot of nonsense representing the anchor. You’re seeing information being sent back up the view tree — nowhere in ContentView have you specified how many flowers should be in the app, or what their identifiers are. The preference key is working! Next, you need to turn those anchors into something useful.

Using Preference Key Values

In this section, you’re going to use your reported preference values to finally get Buzzy flying around her field.

Start by adding a new property to ContentView:

@State var targets: [TargetModel] = []

This is the state property that stores the preference key value when it updates.

Next, remove print(value) from onPreferenceChange(_:perform:) and replace it with:

targets = value

This ensures ContentView updates the state property for all the flower locations. Any time the layout of the flowers changes, you’ll have the latest information ready to use.

Using Preference Key Values to Help Buzzy Navigate

Currently, a tap signals to Buzzy that she should go to the tapped flower, but she doesn’t know how to get there. Next, you’ll help Buzzy find her way to the tapped flower.

In ContentView, update the onChange(of:) modifier where you listen to changes of selectedTargetId. Replace the print statement with the following code:

// 1.
guard let target = targets.first(where: { $0.id == newValue }) else { 
  return 
}
// 2.
let targetFrame = geometry[target.anchor]
// 3.
beePosition = CGPoint(x: targetFrame.midX, y: targetFrame.midY)

Here’s all that’s happening:

1. The handler uses the new ID value to find the TargetModel that matches the flower.
2. geometry is the GeometryProxy for the current view. This is why you are using anchors in the preference value — you can use an anchor as a subscript on geometry and get back the flower’s frame converted to the local coordinate space. Isn’t that just amazingly useful?
3. Using the flower’s frame, you’re calculating the midpoint of the flower and setting beePosition to match. The existing animation takes over and Buzzy takes flight.

Build and run. Tap all the flowers and check if Buzzy makes it around the field.

Buzzy travelling across the flower field

Pretty sweet! (Well, for Buzzy anyway.)

Change the number of flowers in FlowerField or rotate the simulator or device so the layout changes. No matter where the flowers are on the screen, Buzzy will always know where to go now, thanks to your use of view preferences.

Buzzy flying over five flowers

In the case of Buzzy, you’re just storing the value until a tap uses it so you’re safe here. Just be aware. If you’re not careful, you’ll find out that PreferenceKey has a little stinger in its tail!

The stinger in PreferenceKey‘s tail: Be careful how you use PreferenceKey. You may have noticed when you printed the value to the console that all six flower values were output at once. That’s because when SwiftUI computes the body of the whole view graph, it includes the preference keys. If you take a preference key value and use it in a way that triggers another reevaluation of the view body, that changes the preference value again, and you’ll find yourself in a real hornets’ nest of trouble. You may trigger SwiftUI exceptions for updating the view graph mid-update or potentially create an infinite loop.

Showing Buzzy the Way Home With View Preferences

You may have noticed a glaring omission: Buzzy is stuck going from flower to flower and can’t go home to her hive. Considering that Hive is also a view, you can set a view preference for it to handle taps so Buzzy can get home.

First, you need a special ID for the hive. Open TargetPreferenceKey.swift and add a static property to TargetModel:

static let hiveID = 999

Next, open Hive.swift and add the required properties to Hive:

let onTap: Binding<Int>
private let id = TargetModel.hiveID

Hive now has an ID and a binding in the same way as the flowers do. You are using a let of type Binding instead of an @Binding property because you don’t need this view to be recomputed when the value changes. You’re only interested in sending the results of a tap up to the parent view.

Fix the build error in the preview by updating the initializer:

Hive(onTap: .constant(0))

Hive will also need to register its anchor preference and have a tap handler. Add the following modifiers to the ZStack in the view body:

.anchorPreference(key: TargetPreferenceKey.self, value: .bounds) { anchor in
  // 1
  [TargetModel(id: id, anchor: anchor)]
}
.onTapGesture {
  // 2
  onTap.wrappedValue = id
}

Here’s what’s happening in the code above:

  1. Just like the flowers, the hive registers its frame anchor and identifier with your preference key.
  2. When the user taps the hive, the hive’s identifier is sent up to the parent via the binding.

Open ContentView.swift and fix the compiler error by changing the line where Hive is initialized:

Hive(onTap: $selectedTargetID)

Build and run. Tap the hive and call Buzzy back to her home sweet honeycomb.

Buzzy returning home

Using view preferences has allowed you to gather information about the layout of your view tree, in order to tell Buzzy where to go. In the next section, you’ll use all of that information in another way.