竟赛萌芽书意项目经验

Swift

添加自定义字体

获取自定义字体的名称

1
2
3
4
5
6
7
8
init() {
UIFont.familyNames.forEach { familyName in
print("Font Family: \(familyName)")
UIFont.fontNames(forFamilyName: familyName).forEach { fontName in
print("\t\(fontName)")
}
}
}

使用:

1

修改Tabview颜色

更改Assets里面的AccentColor就行

TabItem自动填充选中图标的颜色颜色

参考:[SwiftUI Tabview:如何自定义标签栏 - swiftyplace](https://www.swiftyplace.com/blog/tabview-in-swiftui-styling-navigation-and-more#:~:text=How can I add icons to the tabs,Text(“Second View”).tabItem { Label(“Tab 2”%2C systemImage%3A “2.circle”) })

Assets里面Render As属性改为Template Image

xcode 代码格式化 快捷键

  1. 在代码编辑器中,按 Command + A 选择整个文件。

  2. 然后按 Control + I 来格式化所有选定的代码。

swiftui中如何实现HStack内的内容为flex布局的justify-between的效果

Spacer()基本上用一个就够了

正确代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
HStack(spacing: 30) {
Text("全部")
.font(.custom("AlimamaShuHeiTi-Bold", size: 16))
.padding(.horizontal, 15)
.padding(.vertical, 10)
.foregroundColor(.white)
.background(Color("accent-100"))
.cornerRadius(12)

Spacer()

HStack {
Text("流行")
.foregroundColor(Color("accent-100"))

Spacer()

Text("最近")
.foregroundColor(Color("accent-100"))

Spacer()

Text("推荐")
.foregroundColor(Color("accent-100"))
}
}
.frame(maxWidth: .infinity, maxHeight: 50)

错误代码:

每个 Spacer 会尽可能多地占据可用空间,导致 Text 视图之间的空间被过度分配,挤压 Text 视图,从而导致布局变形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct ContentView: View {
var body: some View {
HStack {
Text("全部")
.font(.custom("AlimamaShuHeiTi-Bold", size: 16))
.padding(.horizontal, 15)
.padding(.vertical, 10)
.foregroundColor(.white)
.background(Color("accent-100"))
.cornerRadius(12)

Spacer()

HStack {
Text("流行")
.foregroundColor(Color("accent-100"))

Spacer()

Text("最近")
.foregroundColor(Color("accent-100"))

Spacer()

Text("推荐")
.foregroundColor(Color("accent-100"))
}
}
.frame(maxWidth: .infinity, maxHeight: 50)
}
}

内部渐变效果

1
2
3
4
5
6
7
8
9
10
11
Image("navigation_ai")
.resizable()
.cornerRadius(20) // 添加圆角,半径为20
.overlay(
LinearGradient(
gradient: Gradient(colors: [.black.opacity(0.3), .black.opacity(0)]),
startPoint: .bottomLeading,
endPoint: UnitPoint(x: 0.5, y: 0.0) // 设置终点为上方中间
)
.cornerRadius(20) // 让渐变也具有圆角效果
)

让中间的HStack宽度为占满剩下的屏幕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GeometryReader { geometry in
HStack {
HStack {
Image("recommend_search")
.resizable()
.frame(width: 30, height: 30)

Text("搜索")
.font(.custom("Alibaba-PuHuiTi-R", size: 15))
.foregroundColor(Color("text-200"))

Spacer() // 让元素靠左排列
}
.padding()
.frame(width: geometry.size.width, height: 40) // 指定高度
.background(Color("bg-200"))
.cornerRadius(12)
}
.frame(height: 40) // 指定GeometryReader的高度
}

PencilKit

PKCanvasView为画布

PKToolPicker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import SwiftUI
import PencilKit

struct PracticeWithPencilView: View {
@State private var canvasView = PKCanvasView()
@State private var toolPicker = PKToolPicker()

var body: some View {
ZStack {
Image("Practice-02") // 确保您有一个包含田字格和文字的图片
.resizable()
.scaledToFit()
.frame(width: geometry.size.width * 0.97, height: geometry.size.height * 0.92)

CanvasView(canvasView: $canvasView, toolPicker: $toolPicker)
.frame(width: geometry.size.width * 0.97, height: geometry.size.height * 0.92)
.background(Color.clear) // 确保背景透明
}
.frame(height: geometry.size.height * 0.86)
}
}

struct CanvasView: UIViewRepresentable {
@Binding var canvasView: PKCanvasView
@Binding var toolPicker: PKToolPicker

func makeUIView(context: Context) -> PKCanvasView {
canvasView.drawingPolicy = .anyInput
canvasView.backgroundColor = .clear // 使画布背景透明
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
return canvasView
}

func updateUIView(_ uiView: PKCanvasView, context: Context) {
// 更新视图的逻辑(如果有)
}
}

Git

回溯版本

参考:Git恢复之前版本的两种方法reset、revert(图文详解)_git回退到某个版本-CSDN博客

1
2
git reset --hard c7c4e1d88e36f97a0c456a82cc2d5bd16a9633c2
git push -f

通过PencilKit获取单字字迹

依据时间顺序总结

好的,这里是按照时间顺序列出的各个方法的调用过程,并在每个事件点后附上对应的代码和详细解释:

1. 开始下第一笔(触摸开始)

当用户开始在画布上绘制时,touchesBegan 方法会被调用。

1
2
3
4
5
6
7
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touch = touches.first {
let point = touch.location(in: self)
updateCurrentStrokeRect(with: point) // 更新笔画矩形区域
}
}

解释:

  • touchesBegan 是 UIResponder 类的一部分,UIResponder 是 iOS 事件响应链中的基类。UIView 以及其子类都继承自 UIResponder,因此可以重载这些方法来处理触摸事件
  • touchesBegan 方法通过touch.location(in: self)记录了用户触摸的位置,并调用 updateCurrentStrokeRect(with:) 方法。
  • updateCurrentStrokeRect(with:) 方法更新 currentStrokeRect,用来记录当前笔画的矩形区域。

updateCurrentStrokeRect(with point: CGPoint) 方法被调用

1
2
3
4
5
6
7
func updateCurrentStrokeRect(with point: CGPoint) {
if let currentRect = currentStrokeRect {
currentStrokeRect = currentRect.union(CGRect(origin: point, size: .zero)) // 更新矩形区域
} else {
currentStrokeRect = CGRect(origin: point, size: .zero) // 初始化矩形区域
}
}

解释:

  • updateCurrentStrokeRect(with point: CGPoint) 方法用于更新当前笔画的矩形区域。
  • currentStrokeRect 是一个可选的 CGRect 类型变量(包含了矩形的原点(即左上角的坐标)和尺寸(宽度和高度)),用于记录当前笔画的矩形区域。
  • 如果 currentStrokeRect 已经存在,则使用 CGRect(origin: point, size: .zero) 创建一个新的零大小的矩形,并使用 union 方法将其与当前矩形区域合并,扩大 currentStrokeRect 以包含新的点。
  • 如果 currentStrokeRect 不存在(即当前是第一笔),则使用 CGRect(origin: point, size: .zero) 创建一个新的零大小的矩形,并将其赋值给 currentStrokeRect。

2. 笔画移动(触摸移动)

当用户移动笔画时,touchesMoved 方法会被调用。

1
2
3
4
5
6
7
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if let touch = touches.first {
let point = touch.location(in: self)
updateCurrentStrokeRect(with: point) // 更新笔画矩形区域
}
}

解释:

  • touchesMoved 方法记录了用户移动的位置,并再次调用 updateCurrentStrokeRect(with:) 方法。
  • updateCurrentStrokeRect(with:) 方法会更新 currentStrokeRect,扩大当前笔画的矩形区域以包含移动后的点。

3. 笔画结束(触摸结束)

当用户完成一个笔画时,touchesEnded 方法会被调用。

1
2
3
4
5
6
7
8
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if let touch = touches.first {
let point = touch.location(in: self)
updateCurrentStrokeRect(with: point) // 更新笔画矩形区域
}
resetTimer() // 重置定时器
}

解释:

  • touchesEnded 方法记录了用户触摸结束的位置,并调用 updateCurrentStrokeRect(with:) 方法。
  • resetTimer() 方法会启动一个2秒的定时器,定时器触发时会调用 handleTimer 方法。

4. 触发定时器(间隔2秒后)

如果用户在2秒内没有再进行新的绘制操作,定时器触发,handleTimer 方法会被调用。

1
2
3
4
5
@objc func handleTimer() {
guard let currentStrokeRect = currentStrokeRect else { return }
saveCurrentDrawingAsImage(in: currentStrokeRect.insetBy(dx: -margin, dy: -margin)) // 保存当前笔画为图像
self.currentStrokeRect = nil // 重置当前笔画矩形区域
}

解释:

  • handleTimer 方法检查 currentStrokeRect 是否存在,如果存在,则调用 saveCurrentDrawingAsImage(in:) 方法保存当前笔画的图像。
  • saveCurrentDrawingAsImage(in:) 方法从画布中截取指定区域的图像,并调用 analyzeImage(_:) 方法进行图像分析。

5. 保存图像并分析

在保存图像时,saveCurrentDrawingAsImage(in:) 方法会被调用。

1
2
3
4
func saveCurrentDrawingAsImage(in rect: CGRect) {
let image = drawing.image(from: rect, scale: 1.0) // 从画布中截取图像
analyzeImage(image) // 分析图像
}

解释:

  • saveCurrentDrawingAsImage(in:) 方法从画布中截取指定矩形区域的图像,并调用 analyzeImage(_:) 方法进行图像分析。

6. 分析图像

最后,analyzeImage(_:) 方法会被调用。

1
2
3
4
5
6
7
8
9
func analyzeImage(_ image: UIImage) {
// 这里可以添加图像分析的逻辑
analyzeImageOnServer(image) // 将图像上传到服务器进行分析
}

func analyzeImageOnServer(_ image: UIImage) {
print("开始分析图像...") // 打印提示信息
// 这里可以添加上传逻辑
}

解释:

  • analyzeImage(_:) 方法中调用 analyzeImageOnServer(_:) 方法将图像上传到服务器进行分析。
  • analyzeImageOnServer(_:) 方法中实际应用中可以添加图像上传逻辑。

总结起来,以下是完整的调用顺序:

  1. 开始下第一笔(触摸开始)touchesBegan -> updateCurrentStrokeRect(with:)
  2. 笔画移动(触摸移动)touchesMoved -> updateCurrentStrokeRect(with:)
  3. 笔画结束(触摸结束)touchesEnded -> updateCurrentStrokeRect(with:) -> resetTimer()
  4. 触发定时器(间隔2秒后)handleTimer -> saveCurrentDrawingAsImage(in:) -> analyzeImage(_:) -> analyzeImageOnServer(_:)

通过这些步骤,代码实现了对单个字的捕捉、保存和分析。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// SwiftUI中的一个代表画布视图的结构
struct CanvasView: UIViewRepresentable {
@Binding var canvasView: CustomPKCanvasView // 绑定的自定义PKCanvasView
@Binding var toolPicker: PKToolPicker // 绑定的PKToolPicker

// 创建UIView的方法
func makeUIView(context: Context) -> CustomPKCanvasView {
canvasView.drawingPolicy = .anyInput // 设置画布的绘制策略
canvasView.backgroundColor = .clear // 设置画布背景颜色为透明
toolPicker.setVisible(true, forFirstResponder: canvasView) // 使工具选择器在画布上可见
toolPicker.addObserver(canvasView) // 添加观察者
canvasView.becomeFirstResponder() // 使画布成为第一响应者
canvasView.delegate = context.coordinator // 设置画布的代理
return canvasView // 返回自定义的画布视图
}

// 更新UIView的方法
func updateUIView(_ uiView: CustomPKCanvasView, context: Context) {}

// 创建协调器的方法
func makeCoordinator() -> Coordinator {
Coordinator(self) // 返回一个协调器实例
}

// 协调器类,负责处理画布视图的代理方法
class Coordinator: NSObject, PKCanvasViewDelegate {
var parent: CanvasView // 保存父视图
var timer: Timer? // 定时器,用于延时处理

// 初始化协调器
init(_ parent: CanvasView) {
self.parent = parent
}

// 当画布内容改变时调用的方法
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
resetTimer() // 重置定时器
}

// 重置定时器的方法
func resetTimer() {
timer?.invalidate() // 使之前的定时器失效
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
self.parent.canvasView.analyzeDrawing() // 调用画布视图的分析方法
}
}
}
}

// 自定义的PKCanvasView类,继承自PKCanvasView
class CustomPKCanvasView: PKCanvasView {
private let speechHelper = SpeechSynthesizerHelper() // 语音助手

var drawingChangedTimer: Timer? // 定时器
var lastTouchPoint: CGPoint? // 最后触摸点
var currentStrokeRect: CGRect? // 当前笔画的矩形区域
let margin: CGFloat = 10.0 // 边距值

// 重置定时器的方法
func resetTimer() {
drawingChangedTimer?.invalidate() // 使之前的定时器失效
drawingChangedTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: false)
}

// 定时器触发时调用的方法
@objc func handleTimer() {
guard let currentStrokeRect = currentStrokeRect else { return }
saveCurrentDrawingAsImage(in: currentStrokeRect.insetBy(dx: -margin, dy: -margin)) // 保存当前笔画为图像
self.currentStrokeRect = nil // 重置当前笔画矩形区域
}

// 分析画布内容的方法
func analyzeDrawing() {
guard let currentStrokeRect = currentStrokeRect else { return }
saveCurrentDrawingAsImage(in: currentStrokeRect.insetBy(dx: -margin, dy: -margin)) // 保存当前笔画为图像
}

// 保存当前画布内容为图像的方法
func saveCurrentDrawingAsImage(in rect: CGRect) {
let image = drawing.image(from: rect, scale: 1.0) // 从画布中截取图像
analyzeImage(image) // 分析图像
}

// 更新当前笔画矩形区域的方法
func updateCurrentStrokeRect(with point: CGPoint) {
if let currentRect = currentStrokeRect {
currentStrokeRect = currentRect.union(CGRect(origin: point, size: .zero)) // 更新矩形区域
} else {
currentStrokeRect = CGRect(origin: point, size: .zero) // 初始化矩形区域
}
}

func analyzeImage(_ image: UIImage) {
// self.speechHelper.speak(text: "保持专注,顿笔后向右下轻带出钩,钩不宜过长")

// guard let imageData = image.jpegData(compressionQuality: 0.8) else {
// print("无法转换图像为JPEG数据")
// return
// }
//
// let fileManager = FileManager.default
// let urls = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
// let documentsDirectory = urls[0]
// let fileName = "handwriting_\(UUID().uuidString).jpg"
// let fileURL = documentsDirectory.appendingPathComponent(fileName)
//
// do {
// try imageData.write(to: fileURL)
// print("图片已保存到本地: \(fileURL.path)")
// } catch {
// print("无法保存图片: \(error)")
// }

// DispatchQueue.main.async {
// let imageView = UIImageView(image: image)
// imageView.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
// if let window = UIApplication.shared.windows.first {
// window.rootViewController?.view.addSubview(imageView)
// }
// }

analyzeImageOnServer(image)
}

func analyzeImageOnServer(_ image: UIImage) {
print("开始分析图像...")
DispatchQueue.main.async {
self.speechHelper.speak(text: "开始分析您的笔迹,请耐心等待")
}
// 将图片转换为JPEG数据
guard let imageData = image.jpegData(compressionQuality: 0.8) else {
print("无法转换图像为JPEG数据")
return
}

// 构建URL请求
let url = URL(string: "https://api.gptapi.us/v1/chat/completions")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer sk-BhcZcQ4KbJW8wmgX96F33d91B14c41CaAf23C4F15d1483Ec", forHTTPHeaderField: "Authorization")

// 构建JSON体
let base64Image = imageData.base64EncodedString()

// 拆分成较小的子表达式
let textPart: [String: Any] = [
"type": "text",
"text": "针对我写的这个字的字迹给出建议,要求字数不超过50字"
]

let imagePart: [String: Any] = [
"type": "image_url",
"image_url": [
"url": "data:image/jpeg;base64,\(base64Image)",
"detail": "low"
]
]

let messageContent: [Any] = [textPart, imagePart]
let message: [String: Any] = ["role": "user", "content": messageContent]

let jsonBody: [String: Any] = [
"model": "gpt-4-turbo",
"messages": [message],
"max_tokens": 300
]

guard let jsonData = try? JSONSerialization.data(withJSONObject: jsonBody, options: []) else {
print("无法构建JSON体")
return
}

request.httpBody = jsonData

// 发送网络请求
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("请求错误: \(error)")
return
}

guard let data = data else {
print("未收到数据")
return
}

if let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let choices = responseJSON["choices"] as? [[String: Any]],
let message = choices.first?["message"] as? [String: Any],
let content = message["content"] as? String {
DispatchQueue.main.async {
self.speechHelper.speak(text: content)
}
print("识别结果: \(content)")
} else {
print("无法解析响应数据")
}
}

task.resume()
}

// 触摸开始时调用的方法
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touch = touches.first {
let point = touch.location(in: self)
updateCurrentStrokeRect(with: point) // 更新笔画矩形区域
}
}

// 触摸移动时调用的方法
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if let touch = touches.first {
let point = touch.location(in: self)
updateCurrentStrokeRect(with: point) // 更新笔画矩形区域
}
}

// 触摸结束时调用的方法
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if let touch = touches.first {
let point = touch.location(in: self)
updateCurrentStrokeRect(with: point) // 更新笔画矩形区域
}
resetTimer() // 重置定时器
}
}