I’m creating a simple Todo App which behaves similar to every Chat App out there. The main screen consists of a List
of items with a AddNewItemView
on the bottom which acts as a main way to add new items to the list.
I’m using the .scrollDismissesKeyboard(.interactively)
modifier on List
in order to get the keyboard to dismiss when the user scrolls down. This works as expected, but poorly.
Please check the video to see the current result. (This is recorded on a simulator with slowed down animations, so it’s easier to spot the broken animations, but it works the exact same way on my device)
As you can see, there are a couple of issues that I’d like to address:
- As the keyboard is coming in, the
List
andAddNewItemView
kinda get overlapped (this is a minor issue though that I could live with) - If I interactively (and slowly) dismiss the keyboard, there will be a big open white space between the keyboard and the List until it completely disappears and the List animates back into place (this is my main issue)
In UIKit, I would have fixed this by listening to keyboard height changes and adjust the bottom constraint accordingly on-the-go. I’m fairly new in SwiftUI though, still getting used to the whole declarative concept, so I’m a bit more lost. How can I approach this? I want the keyboard dismissal to feel more natural.
I tried to do it by listening to UIResponder.keyboardWillShowNotification
and UIResponder.keyboardWillHideNotification
while mapping the UIResponder.keyboardFrameEndUserInfoKey
to a local keyboardHeight
variable and then adjusting the padding, but it didn’t work as the keyboardHeight was updated only when the keyboard was fully expanded or fully collapsed, there were not “mid-way” values.
In case it helps, here’s some of my main code:
Main View
var body: some View {
VStack(spacing: 0) {
// Header
TodoListHeader()
// Content
Group {
if hasData {
TodoList(
checkedItems: checkedItems,
uncheckedItems: uncheckedItems,
focusedItem: $focusedItem,
onCheckTap: { toggleCheck(item: $0) },
onContentTap: { focusedItem = $0 },
onDelete: { deleteItem(item: $0) },
onMove: move(from:to:)
)
} else {
TodoListEmptyState()
}
}
.background(Color("Grey200"))
// Add New Item View
AddNewItemView(addItem: addItem(text:))
}
}
TodoList
var body: some View {
List {
Group {
ForEach(uncheckedItems) { item in
TodoView(
item: item,
onCheckTap: onCheckTap,
onContentTap: onContentTap) {
TextItem(
item: item,
focusItem: focusedItem
)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets())
.frame(maxWidth: .infinity)
}
.onDelete(perform: deleteUncheckedItem)
.onMove(perform: onMove)
ForEach(checkedItems) { item in
TodoView(
item: item,
onCheckTap: onCheckTap,
onContentTap: onContentTap) {
TextItem(
item: item,
focusItem: focusedItem
)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets())
}
.onDelete(perform: deleteCheckedItem)
}
}
.scrollDismissesKeyboard(.interactively)
.listRowSeparator(.hidden)
.listStyle(.plain)
.animation(.default)
}
AddNewItemView
var body: some View {
VStack {
Divider()
HStack {
TextField(text: $newTodoText, prompt: Text("What's on your mind?"), axis: .vertical) {
Text("Label")
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.submitLabel(.send)
.onChange(of: newTodoText) {
if newTodoText.last == "n" {
HapticEngine.shared.playHaptic()
newTodoText.removeLast()
addItem(newTodoText)
newTodoText = ""
}
}
Image(systemName: "paperplane.fill")
.onTapGesture {
HapticEngine.shared.playHaptic()
addItem(newTodoText)
newTodoText = ""
}
}
.padding(.vertical, 8)
.padding(.horizontal, 32)
.padding(.bottom, 8)
}
}
My desired result is to have a smoother behaviour, like other chat apps that have a “reply view” on the bottom.
- On WhatsApp, the ReplyView will animate slowly along with the keyboard in case of “slow dismissal”
- On Facebook Messenger, the keyboard can’t even be dismissed slowly. When the user starts scrolling down the list, the keyboard just instantly animates back down