关于ChatGPT
ChatGPT是OpenAI公司开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,真正像人类一样来聊天交流,甚至能完成撰写邮件、视频脚本、文案、翻译、代码,写论文等任务。
界面开发
先看看整体的界面效果:

这是一个macOS系统上的应用程序,界面分为两栏,左边是会话栏,显示我们创建的会话;右边是聊天界面,显示当前会话的内容,即我们跟ChatGPT的对话。点击侧栏按钮可以打开或关闭左边的会话栏。

App入口:
@main
struct ChatgptApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
代码简析:
在SwiftUI中,我们的程序入口必须要遵循App协议,这样SwiftUI才能知道从哪个位置启动我们的应用。
多栏导航:
struct ContentView: View {
var manager = SessionManager()
var body: some View {
NavigationSplitView {
SessionView(manager: manager)
} detail: {
ChatView(manager: manager)
}
}
}
代码简析:
长久以来,开发者对 SwiftUI 的导航系统颇有微词,原本很基本功能,如跳转到根视图、返回视图栈中任意层级的视图等,开发者需要使用各种技巧乃至黑科技才能实现,让人不禁吐槽和抓狂。
SwiftUI 4.0( iOS 16+ 、macOS 13+ )对导航系统作出了重大改变,提供了以视图堆栈为管理对象的新 API ,让开发者可以轻松实现编程式导航。
其中最直接的变化是废弃了 NavigationView,将其功能分成了两个单独的控件 NavigationStack 和 NavigationSplitView。
会话栏界面:

对应的代码:
struct SessionView: View {
@ObservedObject var manager: SessionManager
@State private var showAlert = false
var body: some View {
VStack {
// 新建聊天
Button {
manager.createNewSession()
} label: {
Image("NewChat")
.resizable()
.frame(width: 12, height: 12)
Spacer()
.frame(width: 8)
Text("新建聊天")
.font(.system(size: 14))
}
.buttonStyle(.plain)
.frame(height: 24)
.keyboardShortcut("n")
Spacer()
.frame(height: 16)
// 列表
List(manager.sessions) { model in
SessionCell(model: model)
.contentShape(Rectangle())
.onTapGesture {
manager.sessionId = model.id
}
.listRowBackground(manager.sessionId == model.id ? Color("ListColor") : Color.clear)
}
.background(.clear)
.scrollContentBackground(.hidden)
Spacer()
.frame(height: 16)
// 清除记录
Button {
showAlert = true
} label: {
Image("ClearRecord")
.resizable()
.frame(width: 12, height: 12)
Spacer()
.frame(width: 8)
Text("清除记录")
.font(.system(size: 14))
}
.frame(maxWidth: .infinity, maxHeight: 56)
.buttonStyle(.plain)
.background(Color("ListColor"))
}
.alert("您确定要清除所有会话记录吗", isPresented: $showAlert) {
Button("清除", role: .destructive) {
manager.removeAllSessions()
}
Button("取消", role: .cancel) {
}
}
}
}
代码简析:
1、别看这段代码有点长,其实组成结构很简单,就是一个垂直布局(VStack),自上而下:Button(新建聊天)、List(会话列表)、Button(清除记录)。
2、关于contentShape(Rectangle())。SwiftUI的onTapGesture方法默认的点击区域是有效内容区域,即SessionCell中的图标和文字,其他空白区域是不响应点击的,所以我们需要用contentShape指定内容区域为整个矩形区。
3、关于keyboardShortcut。它是SwiftUI下的快捷键设置,比之前的cocoa框架设置快捷键的方式简单多了。
会话列表中的Cell:

对应的代码:
struct SessionCell: View {
var model: SessionModel
var body: some View {
HStack {
Image("Chat")
.resizable()
.frame(width: 14, height: 14)
Text(model.title)
.font(.system(size: 14))
Spacer()
}
.padding(.vertical, 12)
}
}
代码简析:
水平布局(HStack),自左向右:图片、文本。
聊天界面:

对应的代码:
struct ChatView: View {
@ObservedObject var manager: SessionManager
@State private var inputText = ""
@State private var isInputEnable = true
@FocusState private var isInputFocused: Bool
var body: some View {
// 聊天列表
List(manager.chats) { model in
ChatCell(model: model)
}
.background(
Color("ListColor")
)
.scrollContentBackground(.hidden)
.safeAreaInset(edge: .bottom) {
// 输入框
InputView(
text: $inputText,
isEnable: $isInputEnable,
isFocused: $isInputFocused) {
manager.ask(prompt: inputText) { _ in
isInputEnable = true
} failure: { _ in
isInputEnable = true
}
inputText = ""
isInputFocused = false
isInputEnable = false
}
}
}
}
代码简析:
整个聊天界面由列表和输入框组成,用safeAreaInset(edge: .bottom)将输入框覆盖在列表安全区域的位置。如果不喜欢这种覆盖的方式,使用垂直布局(VStack)也是可以的。
聊天列表中的Cell:

对应的代码:
struct ChatCell: View {
var model: ChatModel
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 4) {
Image(model.avatar)
.padding(.leading, 16)
Text(model.name)
.foregroundColor(Color("NameColor"))
}
Text(model.content)
.textSelection(.enabled)
.font(.title3)
.padding(.leading, 16)
Spacer()
.frame(height: 24)
}
}
}
代码简析:
这个界面使用了两种布局的组合:垂直布局(VStack)中嵌套了水平布局(HStack)。其实这种布局的嵌套在SwiftUI中很常用的,不可能一种布局就吃遍天下。SwiftUI目前的布局组件有五种:HStack、VStack、ZStack、LazyHStack、LazyVStack。
输入框界面:

对应的代码:
struct InputView: View {
@Binding var text: String
@Binding var isEnable: Bool
var isFocused: FocusState<Bool>.Binding
var onSubmit: () -> Void
@State private var height: CGFloat = 64
private let kFontSize: CGFloat = 16
var body: some View {
GeometryReader { proxy in
ZStack {
// 换行键
Button("") {
text = text + "\n"
}
.hidden()
.keyboardShortcut(.return)
Rectangle()
.fill(Color("ListColor"))
Rectangle()
.fill(fillColor())
.cornerRadius(24)
.padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
.overlay {
TextField("", text: $text, axis: .vertical)
.lineLimit(20)
.font(.system(size: kFontSize))
.textFieldStyle(.plain)
.disableAutocorrection(true)
.disabled(!isEnable)
.focused(isFocused)
.background(fillColor())
.padding(.horizontal, 36)
.onChange(of: text, perform: { newValue in
let paddingOffset = CGFloat(36 + 16*2) // TextField is 36, Rectangle is 16*2 (leading & trailing).
let size = CGSize(width: proxy.size.width - paddingOffset, height: .infinity)
let h = sizeToFit(with: text, in: size)
height = max(h + 44, 64)
})
.onSubmit {
onSubmit()
}
}
}
}
.frame(height: height)
}
private func fillColor() -> Color {
return isEnable ? Color("EnableInputColor") : Color("DisableInputColor")
}
private func sizeToFit(with string: String, in limitSize: CGSize) -> CGFloat {
let range = NSMakeRange(0, string.count)
let attributeString = NSMutableAttributedString(string: string)
// 字体
let font = NSFont.systemFont(ofSize: kFontSize)
attributeString.addAttribute(.font, value: font, range: range)
return attributeString.boundingRect(with: limitSize, options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil).height
}
}
代码简析:
1、关于ZStack。前面我们使用过了HStack和VStack,分别表示水平布局和垂直布局,但有时候光有这两个还不行,无法处理叠加式的布局,于是就有了ZStack。ZStack可以实现组件在屏幕方向上的层级叠加。

2、关于GeometryReader。苹果官方是这样定义的:一个可以根据其自身大小和坐标空间定义其内容的容器视图。看不懂,有点抽象。举个例子:视图中有一个按钮,如果在程序运行的时候我们想知道这个按钮的坐标和大小,该怎么办呢? GeometryReader 就是干这个事的,它带一个 GeometryProxy 参数,从这个参数就可以读取到坐标和大小的信息。
3、为了解决输入框随着文字的输入而自适应增高的问题,SwiftUI 4.0为TextField增加了一个axis参数,指定自增高的方向,有两个值:vertical 和 horizontal。
结语
以上就是用SwiftUI开发macOS系统的ChatGPT应用程序界面部分的代码,是不是很简单呢,哈哈。随着SwiftUI的不断优化,现在已经能胜任很多场景下的开发需求了,更少的代码还能跨平台,why not 用呢。
传送门:SwiftChatGPT