- Published on
Creating an iMessage-Style Input Accessory View in SwiftUI
- Authors
- Name
- Mick MacCallum
- @0x7fs
Creating an input accessory view that stays above the keyboard is a common requirement for chat applications, note-taking apps, and other interfaces that need persistent input controls. SwiftUI provides a built-in solution using ToolbarItemPlacement.keyboard, but we can also create custom solutions for more complex requirements.
Using SwiftUI's Built-in Keyboard Toolbar
SwiftUI provides a native way to add toolbar items above the keyboard using ToolbarItemPlacement.keyboard
. This is the recommended approach for simple cases:
struct ChatView: View {
@State private var messageText = ""
@State private var messages: [String] = []
var body: some View {
NavigationStack {
VStack {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(messages, id: \.self) { message in
Text(message)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(16)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.padding()
}
}
.navigationTitle("Chat")
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
TextField("Message", text: $messageText)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Send") {
sendMessage()
}
.disabled(messageText.isEmpty)
}
}
}
}
private func sendMessage() {
guard !messageText.isEmpty else { return }
messages.append(messageText)
messageText = ""
}
}
Custom KeyboardToolbar for Complex Requirements
For more complex input bars with custom styling, multiple buttons, or specific layout requirements, you can create a custom ViewModifier
:
struct KeyboardToolbar<ToolbarView: View>: ViewModifier {
private let height: CGFloat
private let toolbarView: ToolbarView
init(height: CGFloat, @ViewBuilder toolbar: () -> ToolbarView) {
self.height = height
self.toolbarView = toolbar()
}
func body(content: Content) -> some View {
ZStack(alignment: .bottom) {
GeometryReader { geometry in
VStack {
content
}
.frame(width: geometry.size.width, height: geometry.size.height - height)
}
toolbarView
.frame(height: self.height)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
View Extension for Easy Usage
To make the custom toolbar easy to use, we can create a view extension:
extension View {
func keyboardToolbar<ToolbarView>(
height: CGFloat,
@ViewBuilder view: @escaping () -> ToolbarView
) -> some View where ToolbarView: View {
modifier(KeyboardToolbar(height: height, toolbar: view))
}
}
Creating an iMessage-Style Chat Interface
Here's how to use the custom keyboard toolbar to create a chat interface similar to iMessage:
struct CustomChatView: View {
@State private var messageText = ""
@State private var messages: [String] = []
var body: some View {
VStack {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(messages, id: \.self) { message in
Text(message)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(16)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.padding()
}
}
.keyboardToolbar(height: 60) {
ChatInputBar(text: $messageText) {
sendMessage()
}
}
}
private func sendMessage() {
guard !messageText.isEmpty else {
return
}
messages.append(messageText)
messageText = ""
}
}
Building the Chat Input Bar
The input bar component provides the iMessage-style interface:
struct ChatInputBar: View {
@Binding var text: String
let onSend: () -> Void
var body: some View {
HStack(spacing: 12) {
TextField("Message", text: $text, axis: .vertical)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(
Color(UIColor.separator).opacity(0.5),
lineWidth: 1
)
)
.lineLimit(1...4)
Button(action: onSend) {
Image(systemName: "arrow.up.circle.fill")
.font(.title2)
.foregroundColor(.blue)
}
.disabled(text.isEmpty)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
.overlay(
Rectangle()
.frame(height: 0.5)
.foregroundColor(Color(.separator)),
alignment: .top
)
}
}
Advanced Input Bar with Additional Features
You can extend the input bar to include more features like attachments, emoji picker, or voice recording:
struct AdvancedChatInputBar: View {
@Binding var text: String
@State private var showingAttachmentSheet = false
let onSend: () -> Void
var body: some View {
HStack(spacing: 12) {
Button(action: {
showingAttachmentSheet = true
}) {
Image(systemName: "plus.circle")
.font(.title)
.foregroundColor(.gray)
}
TextField("Message", text: $text, axis: .vertical)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(
Color(UIColor.separator).opacity(0.5),
lineWidth: 1
)
)
.lineLimit(1...4)
Button(action: onSend) {
Image(systemName: "arrow.up.circle.fill")
.font(.title)
.foregroundColor(.blue)
}
.disabled(text.isEmpty)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
.overlay(
Rectangle()
.frame(height: 0.5)
.foregroundColor(Color(.separator)),
alignment: .top
)
.confirmationDialog(
"Choose an attachment",
isPresented: $showingAttachmentSheet,
actions: {
Button("Photo") {
// Handle photo selection
}
Button("Camera") {
// Handle camera
}
Button("Document") {
// Handle document selection
}
Button("Cancel", role: .cancel) {
// Cancel
}
}
)
}
}
Handling Keyboard Appearance
To make the toolbar respond to keyboard appearance, you can combine it with keyboard observation:
struct KeyboardAwareChatView: View {
@State private var messageText = ""
@State private var keyboardHeight: CGFloat = 0
var body: some View {
VStack {
// Your content here
Text("Chat content")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.keyboardToolbar(height: 60 + keyboardHeight) {
ChatInputBar(text: $messageText) {
// Send message
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
keyboardHeight = keyboardFrame.height
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
keyboardHeight = 0
}
}
}