一、以KimiAI举例,在用户中心界面申请自己的APIKey
二、根据官方提供的文档,创建几个结构体用来解析API响应和存储应用消息。
struct ChatCompletionResponse: Codable {
let choices: [Choice]
struct Choice: Codable {
let message: APIMessage
}
}
struct APIMessage: Codable {
let role: String
let content: String
}
struct AppMessage {
let role: String
let content: String
let timestamp: Date
let isIncoming: Bool
}
三、创建VC类 :
1.创建一个数组messages用来储存信息,再创建一个AVSpeechSynthesizer
实例,用于语音合成。
private var messages: [AppMessage] = []
private let speechSynthesizer = AVSpeechSynthesizer()
2.用懒加载创建表格、文本试图和一个发送按钮。
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
tableView.register(ChatCell.self, forCellReuseIdentifier: "ChatCell")
tableView.tableFooterView = UIView() // Hide extra separators
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
return tableView
}()
private lazy var messageInputView: UITextView = {
let textView = UITextView()
textView.layer.borderWidth = 1
textView.layer.borderColor = UIColor.lightGray.cgColor
textView.layer.cornerRadius = 5
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
private lazy var sendButton: UIButton = {
let button = UIButton()
button.setTitle("发送", for: .normal)
button.backgroundColor = .blue
button.layer.cornerRadius = 5
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(handleSend), for: .touchUpInside)
return button
}()
3.定义setupLayout
方法,用于将表格视图、文本输入视图和发送按钮添加到视图中,并设置它们的布局约束。
private func setupLayout() {
view.addSubview(tableView)
view.addSubview(messageInputView)
view.addSubview(sendButton)
NSLayoutConstraint.activate([
// 省略了具体的约束代码
])
}
4.处理发送的消息。
@objc private func handleSend() {
guard let text = messageInputView.text, !text.isEmpty else { return }
let outgoingMessage = AppMessage(role: "User", content: text, timestamp: Date(), isIncoming: true)
messages.append(outgoingMessage)
tableView.reloadData()
messageInputView.text = ""
APIManager.shared.sendChatRequest(userInput: text) { result in
switch result {
case .success(let responseContent):
let incomingMessage = AppMessage(role: "moonshot-v1-8k", content: responseContent, timestamp: Date(), isIncoming: false)
DispatchQueue.main.async {
self.messages.append(incomingMessage)
self.tableView.reloadData()
// 调用语音合成器读取返回的文本
self.speak(text: responseContent)
}
case .failure(let error):
DispatchQueue.main.async {
print("请求错误: \(error.localizedDescription)")
}
}
}
}
5.将文本转化成语言播放。
private func speak(text: String) {
let utterance = AVSpeechUtterance(string: text)
utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN") // 设置为中文(也可以改为其他语言)
speechSynthesizer.speak(utterance)
}
6.给TableView配置背景图片
private func TableViewBackground() {
let backgroundImage = UIImage(named: "backgroundimage")
let backgroundImageView = UIImageView(image: backgroundImage)
backgroundImageView.contentMode = .scaleAspectFill
tableView.backgroundView = backgroundImageView
}
7.根据message给表格视图提供数据源和行数。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatCell", for: indexPath) as! ChatCell
let message = messages[indexPath.row]
cell.configure(with: message)
return cell
}
四、自定义ChatCell类
ChatCell
类定义了一个自定义的聊天单元格,包含一个气泡视图bubbleView
和一个消息标签messageLabel
。configure(with:)
方法用于设置消息内容和样式。
class ChatCell: UITableViewCell {
private let messageLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 16)
label.lineBreakMode = .byWordWrapping
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let bubbleView: UIView = {
let view = UIView()
view.layer.cornerRadius = 15
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(bubbleView)
bubbleView.addSubview(messageLabel)
NSLayoutConstraint.activate([
bubbleView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
bubbleView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
bubbleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
bubbleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
messageLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 10),
messageLabel.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -10),
messageLabel.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 15),
messageLabel.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -15)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with message: AppMessage) {
messageLabel.text = message.content
messageLabel.textAlignment = message.isIncoming ? .left : .right
bubbleView.backgroundColor = message.isIncoming ? UIColor.lightGray.withAlphaComponent(0.2) : UIColor.green.withAlphaComponent(0.7)
}
}
五、处理API请求,将用户的输入发送到服务器并处理相应,这一部分需要参考API的官方文档
class APIManager {
static let shared = APIManager()
private let apiURL = "https://api.moonshot.cn/v1/chat/completions"
private let apiKey = "sk-SCqR38PT6Sr8qW9wcF2AvdgOCNi8jYcf7NeELSifdgNJtmFL"
private init() {}
func sendChatRequest(userInput: String, completion: @escaping (Result<String, Error>) -> Void) {
guard let url = URL(string: apiURL) else {
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestBody: [String: Any] = [
"model": "moonshot-v1-8k",
"messages": [
[
"role": "system",
"content": "你是 Kimi,由 Moonshot AI 提供的人工智能助手。"
],
[
"role": "user",
"content": userInput
]
],
"temperature": 0.3
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
completion(.failure(error))
return
}
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
let statusCodeError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Server error or invalid response"])
completion(.failure(statusCodeError))
return
}
guard let data = data else {
let noDataError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"])
completion(.failure(noDataError))
return
}
do {
let chatResponse = try JSONDecoder().decode(ChatCompletionResponse.self, from: data)
let messageContent = chatResponse.choices.first?.message.content ?? "No content"
completion(.success(messageContent))
} catch {
completion(.failure(error))
}
}
task.resume()
}
}
完整代码如下:
import UIKit
import Foundation
import AVFoundation
struct ChatCompletionResponse: Codable {
let choices: [Choice]
struct Choice: Codable {
let message: APIMessage
}
}
struct APIMessage: Codable {
let role: String
let content: String
}
struct AppMessage {
let role: String
let content: String
let timestamp: Date
let isIncoming: Bool
}
class ViewController2: UIViewController, UITableViewDelegate, UITableViewDataSource {
private var messages: [AppMessage] = []
private let speechSynthesizer = AVSpeechSynthesizer() // 创建语音合成器实例
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
tableView.register(ChatCell.self, forCellReuseIdentifier: "ChatCell")
tableView.tableFooterView = UIView() // Hide extra separators
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
return tableView
}()
private lazy var messageInputView: UITextView = {
let textView = UITextView()
textView.layer.borderWidth = 1
textView.layer.borderColor = UIColor.lightGray.cgColor
textView.layer.cornerRadius = 5
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
private lazy var sendButton: UIButton = {
let button = UIButton()
button.setTitle("发送", for: .normal)
button.backgroundColor = .blue
button.layer.cornerRadius = 5
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(handleSend), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Chat"
view.backgroundColor = .gray
setupLayout()
TableViewBackground()
}
private func TableViewBackground() {
let backgroundImage = UIImage(named: "backgroundimage")
let backgroundImageView = UIImageView(image: backgroundImage)
backgroundImageView.contentMode = .scaleAspectFill
tableView.backgroundView = backgroundImageView
}
private func setupLayout() {
view.addSubview(tableView)
view.addSubview(messageInputView)
view.addSubview(sendButton)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: messageInputView.topAnchor),
messageInputView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
messageInputView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10),
messageInputView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -10),
messageInputView.heightAnchor.constraint(equalToConstant: 40),
sendButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
sendButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10),
sendButton.widthAnchor.constraint(equalToConstant: 60),
sendButton.heightAnchor.constraint(equalToConstant: 40),
])
}
@objc private func handleSend() {
guard let text = messageInputView.text, !text.isEmpty else { return }
let outgoingMessage = AppMessage(role: "User", content: text, timestamp: Date(), isIncoming: true)
messages.append(outgoingMessage)
tableView.reloadData()
messageInputView.text = ""
APIManager.shared.sendChatRequest(userInput: text) { result in
switch result {
case .success(let responseContent):
let incomingMessage = AppMessage(role: "moonshot-v1-8k", content: responseContent, timestamp: Date(), isIncoming: false)
DispatchQueue.main.async {
self.messages.append(incomingMessage)
self.tableView.reloadData()
// 调用语音合成器读取返回的文本
self.speak(text: responseContent)
}
case .failure(let error):
DispatchQueue.main.async {
print("请求错误: \(error.localizedDescription)")
}
}
}
}
// 语音合成
private func speak(text: String) {
let utterance = AVSpeechUtterance(string: text)
utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN") // 设置为中文,也可以改为其他语言
speechSynthesizer.speak(utterance)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatCell", for: indexPath) as! ChatCell
let message = messages[indexPath.row]
cell.configure(with: message)
return cell
}
}
class ChatCell: UITableViewCell {
private let messageLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 16)
label.lineBreakMode = .byWordWrapping
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let bubbleView: UIView = {
let view = UIView()
view.layer.cornerRadius = 15
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(bubbleView)
bubbleView.addSubview(messageLabel)
NSLayoutConstraint.activate([
bubbleView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
bubbleView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
bubbleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
bubbleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
messageLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 10),
messageLabel.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -10),
messageLabel.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 15),
messageLabel.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -15)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with message: AppMessage) {
messageLabel.text = message.content
messageLabel.textAlignment = message.isIncoming ? .left : .right
bubbleView.backgroundColor = message.isIncoming ? UIColor.lightGray.withAlphaComponent(0.2) : UIColor.green.withAlphaComponent(0.7)
}
}
class APIManager {
static let shared = APIManager()
private let apiURL = "https://api.moonshot.cn/v1/chat/completions"
private let apiKey = "sk-SCqR38PT6Sr8qW9wcF2AvdgOCNi8jYcf7NeELSifdgNJtmFL"
private init() {}
//处理用户输入
func sendChatRequest(userInput: String, completion: @escaping (Result<String, Error>) -> Void) {
guard let url = URL(string: apiURL) else {
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestBody: [String: Any] = [
"model": "moonshot-v1-8k",
"messages": [
[
"role": "system",
"content": "你是 Kimi,由 Moonshot AI 提供的人工智能助手。"
],
[
"role": "user",
"content": userInput
]
],
"temperature": 0.3
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
completion(.failure(error))
return
}
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
let statusCodeError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Server error or invalid response"])
completion(.failure(statusCodeError))
return
}
guard let data = data else {
let noDataError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"])
completion(.failure(noDataError))
return
}
do {
let chatResponse = try JSONDecoder().decode(ChatCompletionResponse.self, from: data)
let messageContent = chatResponse.choices.first?.message.content ?? "No content"
completion(.success(messageContent))
} catch {
completion(.failure(error))
}
}
task.resume()
}
}