本文主要介绍如何使用 MarkdownUI 实现类似 DeepSeek 回答问题的效果。
支持显示:文本、代码、表格、图片、超链接等效果。
(文中使用的ColorEx是用来适配暗黑模式的,具体可以查看:SwiftUI(一) 暗黑模式)
运行效果:
1、调用:
调用时自己加个定时器从头到尾加载contentStr的内容,就实现打字机的效果了。
let contentStr = "<think> \n 嗯,这是一个友好的打招呼,需要给出一个友好的回应\n </think> 你好呀,很高兴认识你"
MarkdownView(fullText:contentStr, showDot: $VM.showDot, isLast: true, toastText:$VM.toastText)
2、实现Markdown(导入三方库MarkdownUI)
// 加载Markdown文本(可长按复制)
import SwiftUI
import MarkdownUI
struct MarkdownView: View {
let fullText: String
@Binding var showDot: Bool //加载中指示器(小圆点)●
@State var isLast: Bool = false //是否为列表中最后一条数据
@Binding var toastText: String //toast提示文字 (文字非空时就显示提示,为空时就隐藏)
//思考内容 (计算属性:不直接存储值,而是通过代码动态计算返回值)
private var thinkStr: String {
var tempStr: String = ""
if (fullText.contains("<think>")) { //是否已深度思考
if (fullText.contains("</think>")) { //已思考完
let components = fullText.components(separatedBy: "</think>") //字符串分割
let filteredArr = components.filter {!$0.trimmingCharacters(in:.whitespaces).isEmpty } //过滤空字符串
if filteredArr.count >= 2 {
tempStr = filteredArr.first ?? ""
}
}else{ //没思考完
tempStr = fullText
}
}
return tempStr
}
//主体回答内容 (计算属性:不直接存储值,而是通过代码动态计算返回值)
private var contentStr: String {
var tempStr: String = ""
if (fullText.contains("<think>")) { //是否已深度思考
if (fullText.contains("</think>")) { //已思考完
let components = fullText.components(separatedBy: "</think>") //字符串分割
let filteredArr = components.filter {!$0.trimmingCharacters(in:.whitespaces).isEmpty } //过滤空字符串
if filteredArr.count >= 2 {
tempStr = filteredArr.last ?? ""
}
}
} else { //没深度思考
tempStr = fullText;
}
return tempStr
}
//页面入口
var body: some View {
ScrollView {
//深度思考内容
if (thinkStr.count > 0) {
HStack() {
Text(thinkStr + ((showDot && isLast && contentStr.count <= 0) ? "●" : ""))
.font(.system(size: 15))
.foregroundColor(ColorEx.shared.Subtitle())
Spacer()
}
}
//主体回答内容
if contentStr.count > 0 {
HStack() {
Markdown(contentStr + ((showDot && isLast) ? "●" : "")) //(文本可以写在{}中才,也可以写在()中)
//一、主题Theme(.gitHub:文本、表格、代码块都有灰色背景 .docC:打印代码时页面会抖、表格显示异常 .basic .init())
.markdownTheme(Theme.gitHub
//1、设置文本样式 .text
.text(text: {
ForegroundColor(ColorEx.shared.Title()) //文本颜色
BackgroundColor(ColorEx.shared.ContView()) //背景颜色
FontSize(17) //字体大小 (设置所有文本、代码的字体大小)
})
//2、设置代码样式 .codeBlock
.codeBlock{ configuration in
ScrollView(.horizontal) {
configuration.label
.relativeLineSpacing(.em(0.225)) //行间距
.markdownTextStyle {
FontFamilyVariant(.monospaced) //字体主题 monospaced:等宽(代码显示常用的字体)
FontSize(13) //字体大小
}
.padding(16)
}
.background(ColorEx.shared.Input()) //背景颜色
.clipShape(RoundedRectangle(cornerRadius: 12)) //圆角
.markdownMargin(top: 0, bottom: 16)
}
//3、设置高亮文本样式 .code
.code(code: {
FontSize(17) //字体大小
FontFamilyVariant(.monospaced) //字体主题 monospaced:等宽
BackgroundColor(ColorEx.shared.Input()) //背景颜色
})
//4、表格样式 (改造成可以左右滑动)
.table { configuration in
ScrollView(.horizontal) {
configuration.label
.fixedSize(horizontal: false, vertical: true) //固定宽度、固定高度
.markdownTableBorderStyle(.init(color: Color(ColorEx.shared.isDarkMode() ? "42444e":"e4e4e8"))) //边框样式
.markdownTableBackgroundStyle( //背景样式
.alternatingRows(Color(ColorEx.shared.isDarkMode() ? "18191d":"ffffff"), Color(ColorEx.shared.isDarkMode() ? "25262a":"f7f7f9")) //交替行
)
}
.markdownMargin(top: 0, bottom: 16)
}
.tableCell { configuration in
configuration.label
.markdownTextStyle {
if configuration.row == 0 { //第一行加粗
FontWeight(.semibold) //半黑体
}
BackgroundColor(nil)
}
.fixedSize(horizontal: false, vertical: true) //固定宽度、固定高度
.padding(.vertical, 6)
.padding(.horizontal, 13)
.relativeLineSpacing(.em(0.25)) //行间距
}
)
Spacer()
}
.onTapGesture { //(onTapGesture要写在onLongPressGesture前面,否则会被阻断,加了onTapGesture后滑动异常也解决了)
}
.onLongPressGesture { //长按
UIPasteboard.general.string = contentStr //剪切板
toastText = "复制成功"
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
toastText = ""
}
FeedbackManager.shared.start() //触觉反馈.light.medium.heavy
}
}
//小圆点
if isLast && showDot && fullText.count <= 0 {
HStack {
LottieView(name: ColorEx.shared.isDarkMode() ? "gif_chatdot_white" : "gif_chatdot") //"127399-cycle-rider"
.frame(width: 25, height: 25)
.padding(.leading, -4)
Spacer()
}
.frame(height: 25) //(最后高度一直保留25的空白区域,处理最后一行打印显示不全的问题)
}
}
}
}