I'm trying to create a ViewModifier that moves a field to the top of the screen when it becomes focused. The effect I want is achieved when I pass through boolean binding that gets toggled using a button, like so:
import SwiftUI
let movableFieldID = "MyTextField"
struct TopMovable: ViewModifier {
@Binding var text: String
@Binding var active: Bool
var namespace: Namespace.ID
func body(content: Content) -> some View {
if active {
VStack {
HStack {
Spacer()
Button("Dismiss") {
withAnimation {
active = false
}
}
}
TextField("Enter text", text: $text)
.textFieldStyle(.roundedBorder)
.matchedGeometryEffect(id: movableFieldID, in: namespace)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
content
}
}
}
extension View {
func topMovable(text: Binding<String>,
active: Binding<Bool>,
namespace: Namespace.ID) -> some View {
self.modifier(TopMovable(text: text,
active: active,
namespace: namespace))
}
}
struct ContentView: View {
@Namespace private var namespace
@State var active: Bool = false
@State var text: String = ""
var body: some View {
VStack {
Text("Hello, world!")
Text("Hi!")
TextField("Enter text", text: $text)
.matchedGeometryEffect(id: movableFieldID, in: namespace)
.textFieldStyle(.roundedBorder)
Text("Howdy!")
// State changes HERE
Button("Move the field") {
withAnimation {
active.toggle()
}
}
//
}
.topMovable(text: $text, active: $active, namespace: namespace)
.padding()
}
}
When I try to change this example to use FocusState to determine the value of active, the text field becomes completely unresponsive. With various incantations I can get the text field responding again, but it never animates like it does in the first example.
import SwiftUI
let movableFieldID = "MyTextField"
struct TopMovable: ViewModifier {
@Binding var text: String
@FocusState var active: Bool
var namespace: Namespace.ID
func body(content: Content) -> some View {
if active {
VStack {
HStack {
Spacer()
Button("Dismiss") {
withAnimation {
active = false
}
}
}
TextField("Enter text", text: $text)
.textFieldStyle(.roundedBorder)
.matchedGeometryEffect(id: movableFieldID, in: namespace)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
content
}
}
}
extension View {
func topMovable(text: Binding<String>,
active: FocusState<Bool>,
namespace: Namespace.ID) -> some View {
self.modifier(TopMovable(text: text,
active: active,
namespace: namespace))
}
}
struct ContentView: View {
@Namespace private var namespace
@FocusState var active: Bool
@State var text: String = ""
var body: some View {
VStack {
Text("Hello, world!")
Text("Hi!")
TextField("Enter text", text: $text)
// Ideally, state would change HERE when the field is tapped into
.focused($active)
//
.matchedGeometryEffect(id: movableFieldID, in: namespace)
.textFieldStyle(.roundedBorder)
Text("Howdy!")
}
.topMovable(text: $text, active: _active, namespace: namespace)
.padding()
}
}
Any ideas how I can fix this?


I found a solution that works without resorting to
NotificationCenter, although I'm still not totally happy with it. It feels likeFocusState<Bool>.Bindingought to be enough without the wrapper, but this will do for now: