You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决ExyteChat中折叠头部展开时输入栏被顶至键盘后方的问题

解决ExyteChat中折叠头部展开时输入栏被顶至键盘后方的问题

嗨,我来帮你搞定这个ExyteChat的布局问题!你遇到的核心问题是把折叠头部放在了ChatView外部,导致头部展开时挤压ChatView的空间,进而让输入栏被键盘遮挡。咱们直接利用ExyteChat库本身的Header支持来调整布局,完美满足你的两个需求。

需求回顾

  • 需求1:头部展开时,输入栏始终固定在键盘上方不被遮挡,头部和消息列表一起滚动腾出空间
  • 需求2:头部展开后,底部至少保留足够的可滚动消息空间(我们会确保这一点)

修改后的完整代码

import SwiftUI
import ExyteChat

@main
struct ChatApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Open Chat") {
                ChatDetailView()
            }
            .navigationTitle("Home")
            .padding()
        }
    }
}

struct CollapsibleHeaderView: View {
    @Environment(\.colorScheme) private var cs
    @State private var isCollapsed = true
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(alignment: .top, spacing: 12) {
                Image(systemName: "person.crop.circle")
                    .resizable()
                    .frame(width: 46, height: 46)
                    .foregroundColor(.blue)
                
                VStack(alignment: .leading, spacing: 4) {
                    Text("John Doe")
                        .font(.headline)
                        .foregroundColor(.gray)
                }
                
                Spacer()
            }
            
            HStack {
                Text("Sample Title Goes Here")
                    .font(.title3)
                    .fontWeight(.semibold)
                    .foregroundColor(cs == .dark ? .white : .primary)
                
                Spacer()
                
                Button {
                    withAnimation(.easeInOut) {
                        isCollapsed.toggle()
                    }
                } label: {
                    Image(systemName: isCollapsed ? "chevron.down" : "chevron.up")
                        .imageScale(.medium)
                        .foregroundColor(cs == .dark ? .white : .primary)
                }
                .buttonStyle(.plain)
            }
            
            if !isCollapsed {
                Text("Category: Demo")
                    .font(.subheadline)
                    .foregroundColor(.gray)
                
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 8) {
                        ForEach(0..<3) { i in
                            ZStack {
                                RoundedRectangle(cornerRadius: 8)
                                    .fill(Color.gray.opacity(0.2))
                                    .frame(width: 120, height: 120)
                                
                                Text("Image \(i+1)")
                                    .font(.caption)
                                    .foregroundColor(.gray)
                            }
                        }
                    }
                }
                .padding(.vertical, 4)
            }
            
            Text("""
                 This is a placeholder for detailed descriptive text. test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
                 """)
            .font(.body)
            .foregroundColor(cs == .dark ? .white : .primary)
            .multilineTextAlignment(.leading)
            .fixedSize(horizontal: false, vertical: true)
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: 16, style: .continuous)
                .fill(cs == .dark ? Color(red: 35/255, green: 37/255, blue: 37/255) : Color.white)
        )
        .padding(.horizontal, 16)
        .padding(.top, 10)
    }
}

struct ChatDetailView: View {
    private let me = ExyteChat.User(
        id: UUID().uuidString,
        name: "You",
        avatarURL: nil,
        isCurrentUser: true
    )
    
    @State private var messages: [ExyteChat.Message] = []
    
    enum MessageAction: MessageMenuAction {
        case reply, copy, edit
        
        func title() -> String {
            switch self {
            case .reply: return "Reply"
            case .copy: return "Copy"
            case .edit: return "Edit"
            }
        }
        
        func icon() -> Image {
            switch self {
            case .reply: return Image(systemName: "arrowshape.turn.up.left")
            case .copy: return Image(systemName: "doc.on.doc")
            case .edit: return Image(systemName: "square.and.pencil")
            }
        }
        
        static func menuItems(for message: ExyteChat.Message) -> [MessageAction] {
            message.user.isCurrentUser ? [.reply, .copy, .edit] : [.reply, .copy]
        }
    }
    
    var body: some View {
        ExyteChat.ChatView(
            messages: messages,
            chatType: .conversation,
            replyMode: .quote,
            // 核心改动:把折叠头部作为ChatView的内置Header
            header: {
                CollapsibleHeaderView()
            }
        ) { draft in
            Task {
                let msg = await ExyteChat.Message.makeMessage(
                    id: UUID().uuidString,
                    user: me,
                    status: .sent,
                    draft: draft
                )
                messages.append(msg)
            }
        } messageMenuAction: { (action: MessageAction, defaultActionClosure, message) in
            switch action {
            case .reply:
                defaultActionClosure(message, .reply)
            case .copy:
                defaultActionClosure(message, .copy)
            case .edit:
                defaultActionClosure(message, .edit { editedText in
                    if let idx = messages.firstIndex(where: { $0.id == message.id }) {
                        messages[idx].text = editedText
                    }
                })
            }
        }
        .navigationTitle("Chat Detail")
        .navigationBarTitleDisplayMode(.inline)
        // 可选:强制消息列表至少保留500px滚动空间
        .frame(minHeight: 500)
    }
}

关键改动说明

  1. 利用ExyteChat的内置Header支持:
    原来的代码把折叠头部放在ChatView外部的VStack里,导致头部展开时挤压ChatView空间。现在我们直接将CollapsibleHeaderView传入ChatView的header参数,让库本身管理头部和消息列表的滚动联动。

  2. 自动处理键盘与输入栏联动:
    ExyteChat的ChatView内置了输入栏和键盘的联动逻辑,头部展开时消息列表会自动向上滚动腾空间,输入栏始终固定在键盘上方,完美解决第一个需求。

  3. 保证滚动空间:
    现在头部和消息列表在同一个滚动容器里,即使头部完全展开,消息列表依然有足够滚动空间。如果需要强制至少500px的滚动空间,只需给ChatView加上.frame(minHeight: 500)即可。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 12:04:29