In my last post, I wrote about using preferences in SwiftUI to make the shape of the graph do work for you. This post is something of an addendum to that, focused on focused values. We’ll look at how focused values work, and then see a few ways of taking advantage of them.
Here’s the one sentence explainer: the FocusedValues
system is like preferences but where the reducer function is “whichever values originates from a focused view wins.”
And if that doesn’t sell you, here’s the pitch for people familiar with AppKit or UIKit: a useful way of thinking about the FocusedValues
system is as a strongly typed, generalized version of the responder chain.
Focused values are declared much like environment values. With a key type and a computed property, or the @Entry
macro:
extension FocusedValues {
@Entry var myValue: Int?
}
The main caveat they have is that the property’s type needs to be optional and its default value is always nil
.
An actual value for a key is attached to a view using the .focusedValue
modifier:
TextField("Username", text: $username)
.focusedValue(\.myValue, 42)
Whenever the TextField
has focus, the value 42 will be written for the myValue
key. This doesn’t just apply at the leaf nodes, though: you could attach the .focusedValue
modifier to, say, a VStack
, and that value would be written whenever any child of the VStack
(or any child of their children, etc.) has focus.
Finally, reading a focused value takes place using the @FocusedValue
property wrapper:
struct MyView: View {
@FocusedValue(\.myValue) var myValue: Int?
var body: some View {
// some view that contains the TextField above
let _ = print(myValue) // => Optional(42)
}
}
As is (hopefully) clear, the API for focused values is fairly straightforward. Next, let’s look at a few ways of using them effectively.
Validate Menu Items
Suppose you have menu commands which are defined at the scene level but whose enabled/disabled state depends on data from the currently edited object in your UI. You could try to hoist all the necessary state up to level where the menu command is added, or you could move into a view model or some other such object to make it easier to share. But better would be to define a focused value representing whether the command is enabled and read that from the commands content.
struct MyCommands: Commands {
@FocusedValue(\.myCommandEnabled) var myCommandEnabled: Bool?
var body: some Commands {
CommandMenu("Editor") {
Button("Do Something") {
// ...
}
.disabled(myCommandEnabled != true)
}
}
}
Literally Like the Responder Chain
Take it one step beyond validating, and put the action itself into a focused value. The type of your focused value doesn’t have to be plain-old-data, it can be more complex. Your focused value could literally be the closure that’s used as the action for a menu item or a button elsewhere in your UI. By doing this, you’re effectively using the focused values system exactly as you would the responder chain.
struct MyCommands: Commands {
@FocusedValue(\.myAction) var myAction: (() -> Void)?
var body: some Commands {
CommandMenu("Editor") {
Button("Do Something") {
myAction?()
}
.disabled(myAction == nil)
}
}
}
Sneakily Access a Platform View
This one’s a little more niche, but I still find it useful. In Tusker’s Compose screen, there are multiple fields in which you can use custom emoji (using :shortcodes:
), including the body of the post of course, but also the content warning, as well as any poll options. The Tusker UI includes a button to show the custom emoji picker, for when you can’t remember the shortcode. Implementing this is tricky since it relies on the state of the underlying UIKit text editing controls—namely, the cursor position (that being where a custom emoji should be inserted).
The way I tackle this is by using a focused value which holds a reference to the UITextField
/UITextView
itself. This requires a little bit of ceremony, but it means that, elsewhere, the UI has access to all the information it needs to make custom emoji autocomplete work smoothly.
The common thread among all of these suggestions is that, as with preferences, you’re using the structure of the graph that SwiftUI manages for you to carry information. The .focusedValue
modifier tells you when something inside of it has focus, so take advantage of that rather than trying to convey the same information up the view hierarchy yourself.
Comments
Reply to this post via the Fediverse.