I have a questions about syncing view positions during an animation change.
Here's the version I'm aiming for, but notice when the keyboard appears, the view height changes immediately with no animation.
This version is what I currently have, with the animation when view height shrinks, but notice on the swipe down there is a noticeable gap between the date header and the top of the list view. Kind of appears broken.
Here's the code:
.onReceive(keyboardHeightPublisher.removeDuplicates()) { height in
withAnimation {
let unadjustedKeyboardHeight = self.keyboardUnadjustedHeight - height
self.keyboardAdjustedListHeight = unadjustedKeyboardHeight
} completion: {
switch (self.modalState) {
case .didShow:
self.modalState = .didShow
default:
break
}
}
}
So the self.keyboardAdjustedListHeight
is attached to to the list view:
.frame(height: self.keyboardAdjustedListHeight)
.position(CGPoint(x: (geometry.size.width / 2), y: (self.gesturePosition.y + (self.dateHeaderRect.height / 2)) + (self.keyboardAdjustedListHeight / 2)))
The entire modal is is just
ZStack
- VStack (date header)
- drag gesture
- VStack (list view)
- positioned under the date header based on drag gesture position.
I tried matchedGeometryEffect
but that didn't do the trick.
Any clues?
EDIT: I'm aiming to get the second GIF (animated list view) but the only problem is the gap upon dragging down.
SOLUTION: Used a variation of @Benzy Neez's solution here.
This was the intended effect. Basically an animation of the list view shrinking but no separation of the date header from the list view on down drag. This code is beneath the list view VStack. So still:
ZStack
VStack (date view)
(drag gesture updates position)
VStack (list view)
(updates position based on drag gesture)
.animation(.easeInOut, value: self.animateShrinkModal)
.toolbar(content: {
ToolbarItem(placement: .keyboard) {
EmptyView()
}
})
.onReceive(keyboardHeightPublisher.removeDuplicates()) { height in
let unadjustedKeyboardHeight = self.keyboardUnadjustedHeight - height
self.keyboardAdjustedListHeight = unadjustedKeyboardHeight
if height > .zero {
self.animateShrinkModal.toggle()
}
switch (self.modalState) {
case .didShow:
self.modalState = .didShow
default:
break
}
}
As you can see I only trigger the animation when the keyboard height is not zero. The modal height is reset without an animation right after the keyboard height is zero which is well after the drag ends. Even dragging slowly toward the keyboard has the desired effect.
You explained in a comment that the main issue is that the background is becoming detached from the header during animation.
As a fix, it might help to apply .geometryGroup()
to the parent container.
In the question you included a snippet that shows how you are setting the height and position of the list view. This should not be necessary, as demonstrated in the simple example below. However, if you really want to do it that way, it may help to add alignment: .top
to the .frame
modifier.
struct ContentView: View {
@State private var text = ""
@State private var isExpanded = false
@FocusState private var isFocused: Bool
var body: some View {
ZStack(alignment: .bottom) {
VStack(alignment: .leading) {
Text("Page header")
.font(.largeTitle)
Text("Main content")
.padding(.bottom, 100)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding()
.ignoresSafeArea(.keyboard)
VStack(alignment: .leading) {
HStack {
Text("Header")
Spacer()
Image(systemName: "plus.square.fill")
.imageScale(.small)
.foregroundStyle(.gray)
}
.font(.title2)
.padding()
.background {
UnevenRoundedRectangle(topLeadingRadius: 10, topTrailingRadius: 10)
.fill(Color(white: 0.9))
}
.onTapGesture { isExpanded = true }
if isExpanded {
Form {
TextField("Note", text: $text)
.focused($isFocused)
}
.scrollDisabled(true)
.scrollContentBackground(.hidden)
}
}
.background {
RoundedRectangle(cornerRadius: 10)
.fill(.blue)
.padding(.bottom, isExpanded ? 20 : -300)
.shadow(color: .init(white: 0.4), radius: 6, y: 1)
}
.padding(.top, 100)
.geometryGroup()
.animation(.easeInOut, value: isExpanded)
.animation(.easeInOut, value: isFocused)
.gesture(
DragGesture()
.onChanged { val in
if val.translation.height > 0 {
isFocused = false
isExpanded = false
}
}
)
}
}
}
You will notice that easeInOut
is being used for the animation, instead of .spring
. This seems to stay more in sync with the keyboard movement.