iOS 实现 AI 智能体界面探究

AI 智能体界面探究

Swift实现一个类似ChatGPT简单的AI智能体聊天界面Demo。这个界面包含了消息列表、输入框、发送按钮等基本元素,并实现了键盘响应、自动滚动等交互功能。

功能特点

  1. 类ChatGPT的对话界面布局
  2. 支持用户输入和AI回复的交互
  3. 消息气泡样式的展示
  4. 智能体的列表自动滚动,手动滑动时停止
  5. 手动滑动到底部时继续自动滚动

核心实现

1. 视图结构

主要包含以下UI组件:

  • TableView:用于显示对话消息列表
  • InputContainerView:底部输入区域容器
  • InputTextView:消息输入框
  • SendButton:发送按钮
  • ThinkingIndicatorView:AI思考时的加载指示器

2. 布局实现

使用SnapKit进行约束布局

3. 消息处理

实现了消息发送和接收的基本流程:

  • 用户消息的发送
  • AI回复的模拟
  • 消息列表的模拟更新
  • 自动滚动到最新消息

关键技术

生成文字自动滚动:

    /// 滚动到对话列表底部
    private func scrollToBottom(animated: Bool) {
        guard !messages.isEmpty && shouldAutoScroll else { return }
        
        DispatchQueue.main.async { [weak self] in
            guard let self = self,
                  self.messages.count > 0,
                  let lastSection = self.tableView.numberOfSections > 0 ? self.tableView.numberOfSections - 1 : nil,
                  let lastRow = self.tableView.numberOfRows(inSection: lastSection) > 0 ? self.tableView.numberOfRows(inSection: lastSection) - 1 : nil else {
                return
            }
            
            let lastIndex = IndexPath(row: lastRow, section: lastSection)
            
            self.tableView.scrollToRow(at: lastIndex, at: .bottom, animated: false)

        }
    }

UIScrollView代理方法,实现手动滚动查看历史时停止滚动,滑动到最底部时继续自动滚动:


    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        isUserScrolling = true
        shouldAutoScroll = false
    }
    

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        isUserScrolling = false
        
        // 检查是否滚动到接近底部的位置
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        let scrollViewHeight = scrollView.frame.height
        let buffer: CGFloat = 20 // 增加缓冲区域为可视区域的一半
        
        // 检测滚动方向
        let direction = offsetY - lastContentOffset1
        lastContentOffset1 = offsetY
        
        // 向上滑动时停止自动滚动
        if direction < 0 {
            shouldAutoScroll = false
            return
        }
        
        // 向下滑动且接近底部时启用自动滚动
        if direction > 0 && offsetY + scrollViewHeight >= contentHeight - buffer {
            shouldAutoScroll = true
            // 如果接近底部,立即触发一次滚动
            if !messages.isEmpty {
                scrollToBottom(animated: true)
            }
        }
    }
        
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // 检测滚动方向
        let currentOffset = scrollView.contentOffset.y
        let direction = currentOffset - lastContentOffset
        lastContentOffset = currentOffset
        
        // 计算距离底部的距离
        let offsetFromBottom = scrollView.contentSize.height - (currentOffset + scrollView.frame.height)
        
        // 向上滑动时停止自动滚动
        if direction < 0 {
            shouldAutoScroll = false
        }
        // 当用户滚动到接近底部时启用自动滚动,增加判断条件确保内容完整显示
        else if offsetFromBottom <= 20 && direction > 0 {
            shouldAutoScroll = true
            // 立即触发一次滚动以确保显示最新内容
            if !messages.isEmpty {
                scrollToBottom(animated: true)
            }
        }
    }
    

AI智能体

📄 完整实现代码

import UIKit
import SnapKit

class AIAssistantViewController: UIViewController {
    
    // MARK: - 常量定义
    private enum Constants {
        static let inputViewHeight: CGFloat = 50
        static let inputViewBottomPadding: CGFloat = 10
        static let sendButtonWidth: CGFloat = 60
        static let inputTextViewInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12)
    }
    
    // MARK: - UI组件
    private lazy var tableView: UITableView = {
        let tableView = UITableView(frame: .zero, style: .plain)
        tableView.delegate = self
        tableView.dataSource = self
        tableView.separatorStyle = .none
        tableView.backgroundColor = .systemBackground
        tableView.keyboardDismissMode = .onDrag
        tableView.contentInset = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
        return tableView
    }()
    
    private lazy var inputContainerView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBackground
        return view
    }()
    
    private lazy var inputTextView: UITextView = {
        let textView = UITextView()
        textView.font = .systemFont(ofSize: 16)
        textView.layer.cornerRadius = 18
        textView.layer.borderWidth = 1
        textView.layer.borderColor = UIColor.systemGray5.cgColor
        textView.textContainerInset = Constants.inputTextViewInsets
        return textView
    }()
    
    private lazy var sendButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("发送", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        button.addTarget(self, action: #selector(sendButtonTapped), for: .touchUpInside)
        return button
    }()
    
    private lazy var thinkingIndicatorView: UIActivityIndicatorView = {
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.hidesWhenStopped = true
        return indicator
    }()
    
    // MARK: - 数据属性
    private var messages: [(isUser: Bool, text: String)] = []
    private var keyboardHeight: CGFloat = 0
    private var isUserScrolling = false
    private var shouldAutoScroll = true
    private var lastContentOffset: CGFloat = 0
    
    // MARK: - 生命周期
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupKeyboardObservers()
    }
    
    // MARK: - UI配置
    private func setupUI() {
        view.backgroundColor = .systemBackground
        title = "AI智能体"
        
        view.addSubview(tableView)
        view.addSubview(inputContainerView)
        inputContainerView.addSubview(inputTextView)
        inputContainerView.addSubview(sendButton)
        view.addSubview(thinkingIndicatorView)
        
        inputContainerView.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview()
            make.bottom.equalTo(view.safeAreaLayoutGuide)
            make.height.equalTo(Constants.inputViewHeight + Constants.inputViewBottomPadding)
        }
        
        tableView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide)
            make.leading.trailing.equalToSuperview()
            make.bottom.equalTo(inputContainerView.snp.top)
        }
        
        inputTextView.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(12)
            make.top.equalToSuperview().offset(6)
            make.bottom.equalToSuperview().offset(-Constants.inputViewBottomPadding)
        }
        
        sendButton.snp.makeConstraints { make in
            make.leading.equalTo(inputTextView.snp.trailing).offset(8)
            make.trailing.equalToSuperview().offset(-12)
            make.centerY.equalTo(inputTextView)
            make.width.equalTo(Constants.sendButtonWidth)
        }
        
        thinkingIndicatorView.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.bottom.equalTo(inputContainerView.snp.top).offset(-20)
        }
    }
    
    // MARK: - 消息处理
    @objc private func sendButtonTapped() {
        guard let text = inputTextView.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return }
        
        messages.append((isUser: true, text: text))
        tableView.reloadData()
        shouldAutoScroll = true
        scrollToBottom(animated: true)
        
        inputTextView.text = ""
        thinkingIndicatorView.startAnimating()
        
        let aiMessage = (isUser: false, text: "")
        messages.append(aiMessage)
        var currentText = ""
        var count = 0
        
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
            guard let self = self else {
                timer.invalidate()
                return
            }
            
            count += 1
            currentText += "这是第\(count)秒生成的内容..."
            
            self.messages[self.messages.count - 1].text = currentText
            self.tableView.reloadData()
            self.scrollToBottom(animated: true)
            
            if count >= 3 {
                timer.invalidate()
                self.thinkingIndicatorView.stopAnimating()
            }
        }
    }
    
    // MARK: - 滚动控制
    private func scrollToBottom(animated: Bool) {
        guard !messages.isEmpty && shouldAutoScroll else { return }
        
        DispatchQueue.main.async { [weak self] in
            guard let self = self, messages.count > 0 else { return }
            let indexPath = IndexPath(row: messages.count - 1, section: 0)
            self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: animated)
        }
    }
}

// MARK: - UITableView 数据源和代理
extension AIAssistantViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messages.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
        let message = messages[indexPath.row]
        
        let bubbleView = UIView()
        bubbleView.layer.cornerRadius = 16
        bubbleView.backgroundColor = message.isUser ? .systemBlue : .systemGray6
        
        let label = UILabel()
        label.text = message.text
        label.numberOfLines = 0
        label.textColor = message.isUser ? .white : .label
        
        cell.contentView.addSubview(bubbleView)
        cell.contentView.addSubview(label)
        
        label.snp.makeConstraints { make in
            make.top.bottom.equalToSuperview().inset(8)
            make.width.lessThanOrEqualToSuperview().multipliedBy(0.75)
            message.isUser ? make.trailing.equalToSuperview().offset(-20) : make.leading.equalToSuperview().offset(20)
        }
        
        bubbleView.snp.makeConstraints { make in
            make.edges.equalTo(label).inset(UIEdgeInsets(top: -8, left: -12, bottom: -8, right: -12))
        }
        
        return cell
    }
}

// MARK: - 键盘处理扩展
extension AIAssistantViewController {
    private func setupKeyboardObservers() {
        NotificationCenter.default.addObserver(self,
                                             selector: #selector(keyboardWillShow),
                                             name: UIResponder.keyboardWillShowNotification,
                                             object: nil)
        NotificationCenter.default.addObserver(self,
                                             selector: #selector(keyboardWillHide),
                                             name: UIResponder.keyboardWillHideNotification,
                                             object: nil)
    }
    
    @objc private func keyboardWillShow(_ notification: Notification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        let keyboardHeight = keyboardFrame.height
        
        UIView.animate(withDuration: 0.3) {
            self.inputContainerView.transform = CGAffineTransform(translationX: 0, y: -keyboardHeight)
            self.tableView.contentInset.bottom = keyboardHeight
            self.tableView.scrollIndicatorInsets.bottom = keyboardHeight
        }
    }
    
    @objc private func keyboardWillHide(_ notification: Notification) {
        UIView.animate(withDuration: 0.3) {
            self.inputContainerView.transform = .identity
            self.tableView.contentInset.bottom = 0
            self.tableView.scrollIndicatorInsets.bottom = 0
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容