最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - SwiftUI Chat View: Keyboard pushing entire view up instead of adjusting content area - Stack Overflow

programmeradmin4浏览0评论

I'm building a chat interface in SwiftUI similar to ChatGPT's app, and I want the keyboard to have a similar behaviour to iMessage when a user types the input box goes above the keyboard, the chat scrolls and the header stays at the top. But I'm having issues with keyboard handling. When the keyboard appears, it pushes the entire view up instead of just adjusting the chat content area while keeping the header fixed and input field above the keyboard.

Current Behavior

  • When the keyboard appears, the entire view (including header) moves up
  • The input field sometimes gets hidden behind the keyboard
  • The scrollable content area doesn't adjust properly

Code

Here's my current implementation for my ChatView (I do also have a SideMenuView which seems to be pushed up too)

import SwiftUI

struct ChatView: View {
    @State private var isMenuOpen = false
    @State private var showSettings = false
    @State private var showModelSelection = false
    @State private var dragOffset: CGFloat = 0
    @StateObject private var modelSelectionVM = ModelSelectionViewModel()
    @StateObject private var chatHistoryVM = ChatHistoryViewModel()
    @StateObject private var chatVM: ChatMessageViewModel
    @FocusState private var isTextFieldFocused: Bool

    private let menuWidth: CGFloat = UIScreen.main.bounds.width * 0.75

    init() {
        let modelVM = ModelSelectionViewModel()
        let historyVM = ChatHistoryViewModel()
        _modelSelectionVM = StateObject(wrappedValue: modelVM)
        _chatHistoryVM = StateObject(wrappedValue: historyVM)
        _chatVM = StateObject(wrappedValue: ChatMessageViewModel(modelSelectionVM: modelVM, chatHistoryVM: historyVM))
    }

    var body: some View {
        ZStack(alignment: .leading) {
            Color(.systemBackground)
                .ignoresSafeArea()

            // Dim background when menu is open
            if isMenuOpen {
                Color.black.opacity(0.4)
                    .ignoresSafeArea()
                    .onTapGesture { closeMenu() }
            }

            // Side Menu
            SideMenuView(isMenuOpen: $isMenuOpen,
                         showSettings: $showSettings,
                         dragOffset: $dragOffset,
                         chatHistoryVM: chatHistoryVM,
                         chatVM: chatVM)
            .offset(x: isMenuOpen ? 0 : -menuWidth)

            VStack(spacing: 0) {
                // **Fixed Header**
                HStack {
                    Button(action: toggleMenu) {
                        Image(systemName: "line.horizontal.3")
                            .font(.title)
                            .foregroundColor(.blue)
                    }
                    .frame(width: 44, height: 44)

                    Spacer()

                    Button(action: { showModelSelection = true }) {
                        Text(modelSelectionVM.selectedModel?.name ?? "Select Model")
                            .foregroundColor(modelSelectionVM.selectedModel == nil ? .red : .primary)
                    }

                    Spacer()

                    Button(action: {
                        if chatVM.messages.isEmpty {
                            chatVM.isTemporaryChat.toggle()
                        } else {
                            chatHistoryVM.selectedChatId = nil
                            chatVM.clearMessages()
                        }
                    }) {
                        Image(systemName: chatVM.messages.isEmpty ?
                            (chatVM.isTemporaryChat ? "timer.circle.fill" : "timer.circle") :
                            "plus.circle.fill")
                            .font(.title)
                            .foregroundColor(chatVM.isTemporaryChat ? .orange : .blue)
                    }
                    .frame(width: 44, height: 44)
                }
                .padding(.horizontal)
                .padding(.top, 50)
                .frame(height: 50) // Fix header height
                .background(Color(.systemBackground))
                .zIndex(1) // Ensure header stays fixed

                // **Scrollable Messages**
                ScrollViewReader { proxy in
                    ScrollView {
                        LazyVStack(spacing: 8) {
                            if chatVM.isTemporaryChat && chatVM.messages.isEmpty {
                                Text("This chat won't appear in history, use or create memories, or be used to train our models. For safety purposes, we may keep a copy of this chat for up to 30 days.")
                                    .foregroundColor(.secondary)
                                    .multilineTextAlignment(.center)
                                    .padding()
                            }

                            ForEach(chatVM.messages) { message in
                                ChatMessageView(message: message)
                                    .id(message.id)
                            }
                        }
                        .padding(.bottom, 8)
                    }
                    .onChange(of: chatVM.messages.count) { _ in
                        scrollToBottom(proxy: proxy)
                    }
                }
                .padding(.bottom, 10)

                // **Input Field**
                VStack(spacing: 0) {
                    Divider()
                    HStack(spacing: 16) {
                        TextField("Type a message...", text: $chatVM.currentInput)
                            .padding(.horizontal, 16)
                            .padding(.vertical, 12)
                            .background(Color(.systemGray6))
                            .cornerRadius(20)
                            .focused($isTextFieldFocused)
                            .submitLabel(.send)
                            .onSubmit {
                                chatVM.sendMessage()
                            }
                        
                        Button(action: {
                            chatVM.sendMessage()
                        }) {
                            Image(systemName: "arrow.up.circle.fill")
                                .font(.system(size: 30))
                                .foregroundColor(chatVM.currentInput.isEmpty || chatVM.isTyping ? .gray : .blue)
                        }
                        .disabled(chatVM.currentInput.isEmpty || chatVM.isTyping)
                    }
                    .padding(.horizontal)
                    .padding(.vertical, 8)
                }
                .background(Color(.systemBackground))
                .ignoresSafeArea(.keyboard, edges: .bottom)
            }
            .frame(width: UIScreen.main.bounds.width)
            .background(Color(.systemBackground))
            .offset(x: dragOffset + (isMenuOpen ? menuWidth : 0))
        }
        .gesture(
            DragGesture()
                .onChanged { gesture in
                    let translation = gesture.translation.width
                    if isMenuOpen {
                        dragOffset = Swift.min(0, translation)
                    } else {
                        dragOffset = Swift.max(0, translation)
                    }
                }
                .onEnded { gesture in
                    let translation = gesture.translation.width
                    let velocity = gesture.velocity.width

                    withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
                        if isMenuOpen {
                            isMenuOpen = translation > -menuWidth / 2 || velocity > 500
                        } else {
                            isMenuOpen = translation > menuWidth / 2 || velocity > 500
                        }
                        dragOffset = 0
                    }
                }
        )
        .overlay {
            if showSettings {
                SettingsView(isPresented: $showSettings)
                    .transition(.move(edge: .bottom))
            }

            if showModelSelection {
                ModelSelectionView(isPresented: $showModelSelection, viewModel: modelSelectionVM)
                    .transition(.move(edge: .bottom))
            }
        }
    }

Expected Behavior

  • Header should remain fixed at the top
  • Chat content area should adjust its size and scroll when keyboard appears
  • Input field should stay visible and move up with the keyboard
  • Similar to how iMessage handles keyboard appearance

What I've Tried

  1. Using .ignoresSafeArea(.keyboard, edges: .bottom)
  2. Adding keyboard observers and manually adjusting view positions
  3. Using GeometryReader to manage layout
  4. Various combinations of ZStack and VStack
  5. Different approaches with safeAreaInsets

Environment

  • iOS 17+
  • SwiftUI
  • Xcode 15.2

Question

How can I properly implement keyboard handling in this chat interface to:

  1. Keep the header fixed at the top
  2. Allow the chat content to adjust and scroll appropriately
  3. Keep the input field visible above the keyboard
  4. Maintain a smooth animation during keyboard appearance/disappearance

Any help or guidance would be greatly appreciated!

发布评论

评论列表(0)

  1. 暂无评论