SwiftUI(二) 使用 MarkdownUI 实现 DeepSeek 回答效果

246 阅读3分钟

本文主要介绍如何使用 MarkdownUI 实现类似 DeepSeek 回答问题的效果。
支持显示:文本、代码、表格、图片、超链接等效果。

(文中使用的ColorEx是用来适配暗黑模式的,具体可以查看:SwiftUI(一) 暗黑模式

运行效果:
IMB_KbYSLa.GIF

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的空白区域,处理最后一行打印显示不全的问题)
         }
      }
   }
}