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

SwiftUI实现类Slack输入附件栏:绑定键盘与拖拽展开

SwiftUI 实现类似Slack的绑定键盘可拖拽展开输入视图

结合UIKit的键盘管理是实现这个需求的最优方案,以下是具体实现思路和代码示例:

核心实现思路

  • 键盘状态全局管理:用ObservableObject封装键盘通知监听,统一同步键盘的显示状态和高度到SwiftUI视图。
  • 可拖拽输入容器:通过UIViewRepresentable封装UIKit视图,添加拖拽手势实现高度调整,同时支持点击唤起键盘。
  • 视图布局联动:将输入容器固定在页面底部,通过键盘高度的占位视图保证列表内容不被键盘遮挡,拖拽时动态调整输入容器高度。

完整代码实现

1. 键盘状态管理器

import SwiftUI
import UIKit
import Combine

class KeyboardManager: ObservableObject {
    @Published var keyboardHeight: CGFloat = 0
    @Published var isKeyboardVisible: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 监听键盘弹出通知
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
            .compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
            .map { $0.height }
            .sink { [weak self] height in
                self?.keyboardHeight = height
                self?.isKeyboardVisible = true
            }
            .store(in: &cancellables)
        
        // 监听键盘收起通知
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            .sink { [weak self] _ in
                self?.keyboardHeight = 0
                self?.isKeyboardVisible = false
            }
            .store(in: &cancellables)
    }
}

2. 可拖拽输入容器封装

struct DraggableInputContainer: UIViewRepresentable {
    @Binding var inputHeight: CGFloat
    @Binding var isKeyboardVisible: Bool
    let content: () -> AnyView
    
    func makeUIView(context: Context) -> UIView {
        let containerView = UIView()
        containerView.backgroundColor = .systemBackground
        
        // 嵌入SwiftUI内容
        let hostingController = UIHostingController(rootView: content())
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(hostingController.view)
        
        // 设置约束
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60) // 输入框最小高度
        ])
        
        // 添加拖拽手势
        let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.handlePan(_:)))
        containerView.addGestureRecognizer(panGesture)
        
        // 添加点击唤起键盘手势
        let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.handleTap(_:)))
        containerView.addGestureRecognizer(tapGesture)
        
        return containerView
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        // 键盘收起时重置输入框高度
        if !isKeyboardVisible {
            inputHeight = 60
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject {
        let parent: DraggableInputContainer
        
        init(_ parent: DraggableInputContainer) {
            self.parent = parent
        }
        
        @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
            guard let view = gesture.view else { return }
            let translation = gesture.translation(in: view.superview)
            
            switch gesture.state {
            case .changed:
                // 计算新高度,限制在最小60和最大屏幕高度-导航栏-键盘高度之间
                let newHeight = max(60, parent.inputHeight - translation.y)
                let maxAvailableHeight = UIScreen.main.bounds.height - 44 - parent.keyboardHeight
                parent.inputHeight = min(newHeight, maxAvailableHeight)
                gesture.setTranslation(.zero, in: view.superview)
            default:
                break
            }
        }
        
        @objc func handleTap(_ gesture: UITapGestureRecognizer) {
            // 主动唤起键盘
            UIApplication.shared.sendAction(#selector(UIResponder.becomeFirstResponder), to: nil, from: nil, for: nil)
        }
    }
}

// 扩展View方便转换为AnyView
extension View {
    func anyView() -> AnyView {
        AnyView(self)
    }
}

3. 主视图使用示例

struct ChatView: View {
    @State private var messageText = ""
    @State private var inputHeight: CGFloat = 60
    @StateObject private var keyboardManager = KeyboardManager()
    
    var body: some View {
        NavigationView {
            List {
                // 模拟聊天内容列表
                ForEach(0..<20) { index in
                    Text("消息 \(index)")
                        .padding(.vertical, 8)
                }
            }
            .navigationTitle("Slack风格聊天")
            .safeAreaInset(edge: .bottom) {
                VStack(spacing: 0) {
                    // 可拖拽输入容器
                    DraggableInputContainer(inputHeight: $inputHeight, isKeyboardVisible: $keyboardManager.isKeyboardVisible) {
                        HStack(spacing: 12) {
                            Button(action: { /* 附件按钮逻辑 */ }) {
                                Image(systemName: "plus.circle")
                                    .font(.title)
                                    .foregroundColor(.blue)
                            }
                            
                            TextField("输入消息...", text: $messageText)
                                .textFieldStyle(.roundedBorder)
                            
                            Button(action: {
                                // 发送消息逻辑
                                messageText = ""
                            }) {
                                Text("发送")
                                    .foregroundColor(.blue)
                            }
                        }
                        .padding(.horizontal)
                        .frame(height: inputHeight)
                        .background(Color.systemBackground)
                        .anyView()
                    }
                    
                    // 键盘占位视图,避免列表被键盘遮挡
                    if keyboardManager.isKeyboardVisible {
                        Color.clear.frame(height: keyboardManager.keyboardHeight)
                    }
                }
                .background(Color.systemBackground)
                .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: -2)
            }
            .onTapGesture {
                // 点击空白处收起键盘
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
            }
        }
    }
}

关键细节说明

  • 键盘状态同步:通过KeyboardManager统一处理键盘通知,确保SwiftUI视图能实时响应键盘的显示/收起和高度变化。
  • 拖拽逻辑:利用UIPanGestureRecognizer监听拖拽动作,动态调整输入容器高度,同时限制高度范围保证交互合理性。
  • 唤起键盘:点击输入容器时,通过UIApplication的全局方法让输入框成为第一响应者,实现点击唤起键盘的效果。
  • 布局适配:使用safeAreaInset将输入容器固定在底部,配合键盘高度的占位视图,保证列表内容始终处于可见区域。

内容的提问来源于stack exchange,提问作者Vignesh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 22:50:21