@faceaisdk/react-native-face-sdk 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +252 -0
  3. package/android/build.gradle +53 -0
  4. package/android/libs/FaceSDKLib-release.aar +0 -0
  5. package/android/proguard-rules.pro +3 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/com/faceaisdk/reactnative/FaceRNModule.kt +383 -0
  8. package/android/src/main/java/com/faceaisdk/reactnative/FaceRNPackage.kt +16 -0
  9. package/ios/FaceAISDK/CustomToastView.swift +34 -0
  10. package/ios/FaceAISDK/FaceAINaviView.swift +212 -0
  11. package/ios/FaceAISDK/FaceSDKCameraView.swift +40 -0
  12. package/ios/FaceAISDK/FaceSDKLocalizer.swift +21 -0
  13. package/ios/FaceAISDK/LivenessDetectView.swift +317 -0
  14. package/ios/FaceAISDK/ScreenBrightnessHelper.swift +100 -0
  15. package/ios/FaceAISDK/TTSPlayer.swift +357 -0
  16. package/ios/FaceAISDK/VerifyFaceView.swift +284 -0
  17. package/ios/FaceAISDK/addFace/AddFaceByCamera.swift +207 -0
  18. package/ios/FaceAISDK/addFace/AddFaceByImage.swift +174 -0
  19. package/ios/FaceAISDK/addFace/ImagePicker.swift +52 -0
  20. package/ios/FaceAISDK/addFace/VerifyTwoFaceSimiView.swift +210 -0
  21. package/ios/FaceColorExtensions.swift +10 -0
  22. package/ios/FaceRNModule.h +9 -0
  23. package/ios/FaceRNModule.m +197 -0
  24. package/ios/FaceSDKSwiftManager.swift +277 -0
  25. package/ios/Resources/en.lproj/Localizable.strings +51 -0
  26. package/ios/Resources/light_too_high.png +0 -0
  27. package/ios/Resources/zh-Hans.lproj/Localizable.strings +51 -0
  28. package/lib/index.d.ts +22 -0
  29. package/lib/index.js +112 -0
  30. package/lib/types.d.ts +39 -0
  31. package/lib/types.js +2 -0
  32. package/package.json +88 -0
  33. package/react-native-face-sdk.podspec +28 -0
  34. package/src/index.ts +184 -0
  35. package/src/types.ts +90 -0
@@ -0,0 +1,284 @@
1
+ import SwiftUI
2
+ import FaceAISDK_Core
3
+
4
+ /**
5
+ * 1:1 Face Verification and Liveness Detection
6
+ * 1:1 人脸识别以及活体检测
7
+ */
8
+ struct VerifyFaceView: View {
9
+ @StateObject private var viewModel: VerifyFaceModel = VerifyFaceModel()
10
+ @Environment(\.dismiss) private var dismiss
11
+ // Prompt that the ambient light is too bright
12
+ // 提示环境光太亮
13
+ @State private var showLightHighDialog = false
14
+ @State private var showToast = false
15
+ @State private var toastViewTips: String = ""
16
+ @State private var isTipAppeared = false
17
+
18
+
19
+ // Automatically control screen brightness
20
+ // 自动控制屏幕亮度
21
+ var autoControlBrightness: Bool = true
22
+ var retryTime:Int = 0; //记录失败尝试的次数
23
+
24
+ let faceID: String
25
+ let threshold: Float
26
+
27
+ // 0. No liveness detection 1. Motion only 2. Motion + Color flash 3. Color flash only
28
+ // 0.无需活体检测 1.仅仅动作 2.动作+炫彩 3.炫彩
29
+ let livenessType:Int
30
+
31
+ // Types of motion liveness: 1. Open mouth 2. Smile 3. Blink 4. Shake head 5. Nod
32
+ // 动作活体种类:1. 张张嘴 2.微笑 3.眨眨眼 4.摇摇头 5.点头
33
+ let motionLiveness:String
34
+
35
+ // Motion liveness timeout (seconds)
36
+ // 动作活体超时(秒)
37
+ let motionLivenessTimeOut:Int
38
+
39
+ // Number of motion liveness steps
40
+ // 动作活体步骤个数
41
+ let motionLivenessSteps:Int
42
+
43
+ // Callback status, face similarity, liveness score
44
+ // 返回状态,人脸相似度,活体分数
45
+ let onDismiss: (Int, Float, Float) -> Void
46
+
47
+ // Multi-language tips
48
+ // 多语言提示
49
+ private func localizedTip(for code: Int) -> String {
50
+ let key = "Face_Tips_Code_\(code)"
51
+ let defaultValue = "VerifyFace Tips Code=\(code)"
52
+ let tipsString = NSLocalizedString(key, value: defaultValue, comment: "")
53
+ if code != 0 && code != 1 && code != 3 {
54
+ TTSPlayer.shared.speak(tipsString)
55
+ }
56
+ return tipsString
57
+ }
58
+
59
+ var body: some View {
60
+ ZStack {
61
+ VStack {
62
+ HStack {
63
+ Button(action: {
64
+ // 0 represents user cancellation 代表用户取消
65
+ onDismiss(0, 0.0, 0.0)
66
+ dismiss()
67
+ }) {
68
+ Image(systemName: "chevron.left")
69
+ .font(.system(size: 16, weight: .semibold))
70
+ .foregroundColor(.black)
71
+ .padding(10)
72
+ .background(Color.gray.opacity(0.1))
73
+ .clipShape(Circle())
74
+ }
75
+ Spacer()
76
+ }
77
+ .padding(.horizontal, 2)
78
+ .padding(.top, 10)
79
+
80
+ if isTipAppeared {
81
+ Text(localizedTip(for: viewModel.sdkInterfaceTips.code))
82
+ .font(.system(size: 20).bold())
83
+ .padding(.horizontal, 20)
84
+ .padding(.vertical, 8)
85
+ .foregroundColor(.white)
86
+ .background(Color.faceMain)
87
+ .cornerRadius(20)
88
+ .id(viewModel.sdkInterfaceTips.code)
89
+ .transition(.asymmetric(
90
+ insertion: .scale(scale: 0.8).combined(with: .opacity),
91
+ removal: .opacity
92
+ ))
93
+ .animation(.spring(response: 0.4, dampingFraction: 0.6), value: viewModel.sdkInterfaceTips.code)
94
+ }
95
+
96
+ Text(localizedTip(for: viewModel.sdkInterfaceTipsExtra.code))
97
+ .font(.system(size: 20).bold())
98
+ .padding(.bottom, 6)
99
+ .frame(minHeight: 30)
100
+ .foregroundColor(.black)
101
+
102
+ FaceSDKCameraView(session: viewModel.captureSession, cameraSize: FaceCameraSize)
103
+ .frame(
104
+ width: FaceCameraSize,
105
+ height: FaceCameraSize
106
+ )
107
+ .padding(.vertical, 8)
108
+ .aspectRatio(1.0, contentMode: .fit)
109
+ .clipShape(Circle())
110
+ .overlay(Circle().stroke(Color.gray, lineWidth: 1))
111
+
112
+ Spacer()
113
+ }
114
+ .padding()
115
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
116
+ .background(viewModel.colorFlash.ignoresSafeArea())
117
+ // Hide system navigation bar
118
+ // 隐藏系统导航栏
119
+ .navigationBarBackButtonHidden(true)
120
+ .navigationBarHidden(true)
121
+
122
+ if showToast {
123
+
124
+ let similarity = String(format: "%.2f", viewModel.faceVerifyResult.similarity)
125
+ // Prefer manually set toastViewTips (for handling missing feature values), otherwise use tips returned by SDK
126
+ // 优先使用手动设置的 toastViewTips (用于处理无特征值的情况),否则使用 SDK 返回的 tips
127
+ let displayTips = toastViewTips.isEmpty ? viewModel.faceVerifyResult.tips : toastViewTips
128
+ let displayMessage = (toastViewTips.isEmpty) ? "\(displayTips)" : displayTips
129
+
130
+ // Calculate style: If it's a missing feature error or low similarity, it's a failure
131
+ // 计算样式:如果是无特征值错误,或者相似度低,则为 failure
132
+ let isSuccess = viewModel.faceVerifyResult.similarity > threshold && viewModel.faceVerifyResult.liveness>0.72
133
+ let toastStyle: ToastStyle = isSuccess ? .success : .failure
134
+
135
+ VStack {
136
+ Spacer()
137
+ CustomToastView(
138
+ message: displayMessage,
139
+ style: toastStyle
140
+ )
141
+ .padding(.bottom, 77)
142
+ }
143
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
144
+ .transition(.move(edge: .bottom).combined(with: .opacity))
145
+ .zIndex(1)
146
+ }
147
+
148
+ // Custom dialog for high light levels
149
+ // 光线过强自定义弹窗 (Dialog)
150
+ if showLightHighDialog {
151
+ ZStack {
152
+ VStack(spacing: 22) {
153
+ Text(viewModel.faceVerifyResult.tips)
154
+ .font(.system(size: 16).bold())
155
+ .fontWeight(.semibold)
156
+ .multilineTextAlignment(.center)
157
+ .foregroundColor(.black)
158
+ .padding(.horizontal,25)
159
+
160
+
161
+ if let uiImage = UIImage(named: "light_too_high") {
162
+ Image(uiImage: uiImage)
163
+ .resizable()
164
+ .scaledToFit()
165
+ .frame(maxHeight: 120)
166
+ .padding(.horizontal,1)}
167
+
168
+ Button(action: {
169
+ withAnimation {
170
+ showLightHighDialog = false
171
+ onDismiss(viewModel.faceVerifyResult.code,viewModel.faceVerifyResult.similarity,viewModel.faceVerifyResult.liveness)
172
+ dismiss()
173
+ }
174
+ }) {
175
+ Text("Confirm")
176
+ .font(.system(size: 18).bold())
177
+ .foregroundColor(.white)
178
+ .frame(maxWidth: .infinity)
179
+ .padding(.vertical, 10)
180
+ .background(Color.faceMain)
181
+ .cornerRadius(10)
182
+ }
183
+ .padding(.horizontal, 30)
184
+ }
185
+ .padding(.vertical, 22)
186
+ .background(Color.white)
187
+ .cornerRadius(20)
188
+ .shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 10)
189
+ .padding(.horizontal, 30)
190
+ }
191
+ .zIndex(2)
192
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
193
+ }
194
+ }
195
+ .onAppear {
196
+
197
+ withAnimation(.spring(response: 0.6, dampingFraction: 0.7).delay(0.9)) {
198
+ isTipAppeared = true
199
+ }
200
+
201
+ if autoControlBrightness {
202
+ ScreenBrightnessHelper.shared.maximizeBrightness()
203
+ }
204
+
205
+ withAnimation(.easeInOut(duration: 0.3)) {
206
+ UIScreen.main.brightness = 1.0
207
+ }
208
+
209
+ // Check if there is a local feature value
210
+ // 校验本地是否有特征值
211
+ guard let faceFeature = UserDefaults.standard.string(forKey: faceID) else {
212
+ toastViewTips = "No Face Feature for : \(faceID)"
213
+ showToast = true
214
+
215
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
216
+ showToast = false
217
+ // Callback NO_FACE_FEATURE
218
+ // 返回无特征值状态
219
+ onDismiss(6,0.0,0.0)
220
+ dismiss()
221
+ }
222
+ return
223
+ }
224
+
225
+
226
+ guard faceFeature.count >= 1024 else {
227
+ toastViewTips = "Invalid Feature length for : \(faceID)"
228
+ showToast = true
229
+
230
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
231
+ showToast = false
232
+ onDismiss(6, 0.0, 0.0)
233
+ dismiss()
234
+ }
235
+ return
236
+ }
237
+
238
+ viewModel.initFaceAISDK(
239
+ faceIDFeature: faceFeature,
240
+ threshold: threshold,
241
+ livenessType: livenessType,
242
+ onlyLiveness: false,
243
+ motionLiveness: motionLiveness,
244
+ motionLivenessTimeOut:motionLivenessTimeOut,
245
+ motionLivenessSteps:motionLivenessSteps
246
+ )
247
+ }
248
+ //和Android 一样允许重试,而不是立即结束整个流程
249
+ .onChange(of: viewModel.faceVerifyResult.code) { newValue in
250
+ // Clear manual tips, use SDK results
251
+ // 清空手动的 tips,使用 SDK 的结果
252
+ toastViewTips = ""
253
+
254
+ if newValue == VerifyResultCode.COLOR_LIVENESS_LIGHT_TOO_HIGH{
255
+ // Light is too strong 光线太强了
256
+ withAnimation {
257
+ showLightHighDialog = true
258
+ }
259
+ }else{
260
+ showToast = true
261
+
262
+ if FaceImageManager.saveFaceImage(faceName: faceID, faceImage: viewModel.faceVerifyResult.faceImage){
263
+ print("saveFaceImage success ")
264
+ }
265
+
266
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
267
+ withAnimation {
268
+ showToast = false
269
+ }
270
+ onDismiss(viewModel.faceVerifyResult.code,viewModel.faceVerifyResult.similarity,viewModel.faceVerifyResult.liveness)
271
+ dismiss()
272
+ }
273
+ }
274
+ }
275
+ .onDisappear {
276
+ if autoControlBrightness {
277
+ ScreenBrightnessHelper.shared.restoreBrightness()
278
+ }
279
+
280
+ viewModel.stopFaceVerify()
281
+ }
282
+ .animation(.easeInOut(duration: 0.3), value: showToast)
283
+ }
284
+ }
@@ -0,0 +1,207 @@
1
+ import SwiftUI
2
+ import AVFoundation
3
+ import FaceAISDK_Core
4
+
5
+ @MainActor
6
+ var FaceCameraSize: CGFloat {
7
+ 14 * min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) / 20
8
+ }
9
+
10
+ public struct AddFaceByCamera: View {
11
+ let faceID: String
12
+ let addFacePerformanceMode: Int //Alternate fields备用字段
13
+ let needShowConfirmDialog: Bool
14
+
15
+ // callback Status , FaceFeature
16
+ let onDismiss: (Int, String) -> Void //status 0 cancel, 1 success
17
+
18
+ var autoControlBrightness: Bool = true
19
+
20
+ @Environment(\.dismiss) private var dismiss
21
+
22
+ @StateObject private var viewModel: AddFaceByCameraModel = AddFaceByCameraModel()
23
+
24
+ // 根据状态码转换为对应的文字提示
25
+ private func localizedTip(for code: Int) -> String {
26
+ let key = "Face_Tips_Code_\(code)"
27
+ let defaultValue = "Add Face Tips Code=\(code)"
28
+ let tipsString = NSLocalizedString(key, value: defaultValue, comment: "")
29
+ if code != 0 && code != 1 && code != 11 {
30
+ TTSPlayer.shared.speak(tipsString)
31
+ }
32
+ return tipsString
33
+ }
34
+
35
+ // 统一处理人脸录入成功的逻辑
36
+ private func handleFaceAddSuccess() {
37
+ // Optional
38
+ if FaceImageManager.saveFaceImage(faceName: faceID, faceImage: viewModel.croppedFaceImage) {
39
+ print("saveFaceImage success")
40
+ }
41
+
42
+ // Save face feature 保存人脸特征信息,
43
+ UserDefaults.standard.set(viewModel.faceFeatureBySDKCamera, forKey: faceID)
44
+
45
+ // Close Page, CallBack
46
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
47
+ onDismiss(1, viewModel.faceFeatureBySDKCamera)
48
+ dismiss()
49
+ }
50
+
51
+ }
52
+
53
+ public var body: some View {
54
+ ZStack {
55
+ VStack(spacing: 20) {
56
+ HStack {
57
+ Button(action: {
58
+ onDismiss(0, "")
59
+ dismiss()
60
+ }) {
61
+ Image(systemName: "chevron.left")
62
+ .font(.system(size: 16, weight: .semibold))
63
+ .foregroundColor(.black)
64
+ .padding(10)
65
+ .background(Color.gray.opacity(0.1))
66
+ .clipShape(Circle())
67
+ }
68
+ Spacer()
69
+ }
70
+ .padding(.horizontal, 2)
71
+ .padding(.top, 10)
72
+
73
+ // Status Tips
74
+ Text(localizedTip(for: viewModel.sdkInterfaceTips.code))
75
+ .font(.system(size: 19).bold())
76
+ .padding(.horizontal, 20)
77
+ .padding(.vertical, 8)
78
+ .foregroundColor(.white)
79
+ .background(Color.faceMain)
80
+ .cornerRadius(20)
81
+
82
+ ZStack {
83
+ // Camera
84
+ FaceSDKCameraView(session: viewModel.captureSession, cameraSize: FaceCameraSize)
85
+ .aspectRatio(1.0, contentMode: .fit)
86
+ .clipShape(Circle())
87
+ .background(Circle().fill(Color.white))
88
+ .overlay(Circle().stroke(Color.gray, lineWidth: 1))
89
+
90
+ // Confirm Add Face
91
+ if viewModel.readyConfirmFace && needShowConfirmDialog {
92
+ Color.black.opacity(0.3)
93
+ .clipShape(Circle())
94
+
95
+ ConfirmAddFaceDialog(
96
+ viewModel: viewModel,
97
+ cameraSize: FaceCameraSize,
98
+ onConfirm: {
99
+ handleFaceAddSuccess()
100
+ }
101
+ )
102
+ .transition(.scale.combined(with: .opacity))
103
+ }
104
+ }
105
+ .frame(width: FaceCameraSize, height: FaceCameraSize)
106
+ .animation(.easeInOut(duration: 0.25), value: viewModel.readyConfirmFace)
107
+
108
+ Spacer()
109
+ }
110
+ .padding()
111
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
112
+ .background(Color.white.ignoresSafeArea())
113
+ .navigationBarBackButtonHidden(true)
114
+ .navigationBarHidden(true)
115
+
116
+ .onAppear {
117
+ if autoControlBrightness {
118
+ ScreenBrightnessHelper.shared.maximizeBrightness()
119
+ }
120
+ viewModel.initAddFace()
121
+ }
122
+ .onDisappear {
123
+ if autoControlBrightness {
124
+ ScreenBrightnessHelper.shared.restoreBrightness()
125
+ }
126
+ viewModel.stopAddFace()
127
+ }
128
+ .onChange(of: viewModel.sdkInterfaceTips.code) { newValue in
129
+ print("🔔 AddFaceBySDKCamera: \(viewModel.sdkInterfaceTips.message)")
130
+ }
131
+ .onChange(of: viewModel.readyConfirmFace) { isReady in
132
+ if isReady && !needShowConfirmDialog {
133
+ handleFaceAddSuccess()
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+
141
+ struct ConfirmAddFaceDialog: View {
142
+ let viewModel: AddFaceByCameraModel
143
+ let cameraSize: CGFloat
144
+ let onConfirm: () -> Void
145
+
146
+ var body: some View {
147
+ VStack(alignment: .center, spacing: 15) {
148
+
149
+ Text("Confirm Add Face")
150
+ .font(.system(size: 19, weight: .semibold))
151
+ .foregroundColor(Color.faceMain)
152
+ .padding(.top, 18)
153
+
154
+ //
155
+ Image(uiImage: viewModel.originFaceImage)
156
+ .resizable()
157
+ .scaledToFill()
158
+ .frame(width: 190, height: 220)
159
+ .clipShape(RoundedRectangle(cornerRadius: 12))
160
+ .overlay(
161
+ RoundedRectangle(cornerRadius: 12)
162
+ .stroke(Color.gray.opacity(0.2), lineWidth: 1)
163
+ )
164
+ .shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
165
+
166
+ Text("Ensure face is clear")
167
+ .font(.system(size: 15))
168
+ .foregroundColor(.gray)
169
+ .multilineTextAlignment(.center)
170
+ .padding(.horizontal)
171
+
172
+ // 按钮组
173
+ HStack(spacing: 12) {
174
+ Button(action: {
175
+ viewModel.reInit()
176
+ }) {
177
+ Text("Retry")
178
+ .font(.system(size: 16, weight: .medium))
179
+ .frame(maxWidth: .infinity)
180
+ .frame(height: 45)
181
+ .background(Color.gray.opacity(0.6))
182
+ .foregroundColor(.primary)
183
+ .cornerRadius(8)
184
+ }
185
+
186
+ Button(action: {
187
+ onConfirm()
188
+ }) {
189
+ Text("Confirm")
190
+ .font(.system(size: 16, weight: .bold))
191
+ .frame(maxWidth: .infinity)
192
+ .frame(height: 44)
193
+ .background(Color.faceMain)
194
+ .foregroundColor(.white)
195
+ .cornerRadius(8)
196
+ }
197
+ }
198
+ .padding(.horizontal, 16)
199
+ .padding(.bottom, 16)
200
+ .padding(.top, 8)
201
+ }
202
+ .frame(width: cameraSize * 1.22)
203
+ .background(Color.white)
204
+ .cornerRadius(16)
205
+ .shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 5)
206
+ }
207
+ }
@@ -0,0 +1,174 @@
1
+ import SwiftUI
2
+ import PhotosUI
3
+ import FaceAISDK_Core
4
+
5
+ // 从相册添加人脸
6
+ public struct AddFaceByImage: View {
7
+
8
+ @State private var showImagePicker = false
9
+ @State private var isLoading = false
10
+ @State private var canSave = false
11
+
12
+ // 用于显示和处理的 Image
13
+ @State private var selectedImage: UIImage?
14
+
15
+ @StateObject private var viewModel: AddFaceByImageModel = AddFaceByImageModel()
16
+
17
+ let faceID: String
18
+ let onDismiss: (Int, String?) -> Void // 0 用户取消, 1 添加成功
19
+
20
+ @Environment(\.dismiss) private var dismiss
21
+
22
+
23
+ private func localizedTip(for code: Int) -> String {
24
+ let key = "Face_Tips_Code_\(code)"
25
+ let defaultValue = "LivenessDetect Tips Code=\(code)"
26
+ return NSLocalizedString(key, value: defaultValue, comment: "")
27
+ }
28
+
29
+ public var body: some View {
30
+ ZStack {
31
+ VStack(spacing: 20) {
32
+
33
+ HStack {
34
+ // 左侧返回按钮
35
+ Button(action: {
36
+ onDismiss(0, nil)
37
+ dismiss()
38
+ }) {
39
+ Image(systemName: "chevron.left")
40
+ .font(.system(size: 16, weight: .semibold))
41
+ .foregroundColor(.black)
42
+ .padding(10)
43
+ .background(Color.gray.opacity(0.1))
44
+ .clipShape(Circle())
45
+ }
46
+
47
+ // 中间标题
48
+ Text("Add Face From Album")
49
+ .font(.system(size: 18, weight: .bold))
50
+ .foregroundColor(.black)
51
+
52
+ Spacer()
53
+
54
+ }
55
+ .padding(.horizontal, 20)
56
+ .padding(.top, 10)
57
+
58
+ ScrollView {
59
+ VStack(spacing: 25) {
60
+
61
+ Text(viewModel.message)
62
+ .font(.system(size: 17).bold())
63
+ .padding(.vertical, 12)
64
+ .padding(.horizontal, 24)
65
+ .foregroundColor(Color.faceMain)
66
+ .cornerRadius(20)
67
+ .shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
68
+
69
+ // 2. 图片预览区 (作为点击触发热区)
70
+ Group {
71
+ if let selectedImage {
72
+ ZStack {
73
+ Image(uiImage: selectedImage)
74
+ .resizable()
75
+ .scaledToFit()
76
+ .frame(maxWidth: 166, maxHeight: 166)
77
+ .clipShape(RoundedRectangle(cornerRadius: 16))
78
+ .shadow(radius: 8)
79
+
80
+ if isLoading {
81
+ ZStack {
82
+ Color.black.opacity(0.4)
83
+ .clipShape(RoundedRectangle(cornerRadius: 16))
84
+ ProgressView()
85
+ .scaleEffect(1.5)
86
+ .tint(.white)
87
+ }
88
+ .frame(maxWidth: 166, maxHeight: 166)
89
+ }
90
+ }
91
+ } else {
92
+ VStack(spacing: 12) {
93
+ Image(systemName: "photo.fill")
94
+ .resizable()
95
+ .scaledToFit()
96
+ .frame(width: 80, height: 80)
97
+ .foregroundStyle(.tertiary)
98
+
99
+ Text("Select from album")
100
+ .font(.system(size: 13))
101
+ .foregroundStyle(.secondary)
102
+ }
103
+ .frame(width: 166, height: 166)
104
+ .background(Color.gray.opacity(0.05))
105
+ .cornerRadius(16)
106
+ .overlay(
107
+ RoundedRectangle(cornerRadius: 16)
108
+ .stroke(Color.gray.opacity(0.2), style: StrokeStyle(lineWidth: 1, dash: [5]))
109
+ )
110
+ }
111
+ }
112
+ .onTapGesture {
113
+ showImagePicker = true
114
+ }
115
+
116
+ Button(action: {
117
+ // 此时 viewModel.croppedFaceImage 已经被 async 方法更新为对齐后的图
118
+ let feature = viewModel.getFaceFeature(faceUIImage: viewModel.croppedFaceImage)
119
+ if !feature.isEmpty {
120
+
121
+ //保存人脸特征信息,Save face feature
122
+ UserDefaults.standard.set(feature, forKey: faceID)
123
+ onDismiss(1, feature)
124
+ dismiss()
125
+ }
126
+
127
+ }) {
128
+ Text("Save Face Feature")
129
+ .font(.headline)
130
+ .frame(maxWidth: .infinity)
131
+ .frame(height: 36)
132
+ }
133
+ .buttonStyle(.borderedProminent)
134
+ .tint(canSave ? .green : .gray)
135
+ .disabled(!canSave)
136
+ .padding(.horizontal, 40)
137
+ }
138
+ .padding(.bottom, 20)
139
+ }
140
+ }
141
+ .background(Color.white.ignoresSafeArea())
142
+ .navigationBarBackButtonHidden(true)
143
+ .navigationBarHidden(true)
144
+
145
+ .onChange(of: viewModel.croppedFaceImage) { newValue in
146
+ withAnimation {
147
+ selectedImage = newValue
148
+ isLoading = false
149
+ canSave = true
150
+ }
151
+ }
152
+
153
+ .sheet(isPresented: $showImagePicker) {
154
+ ImagePicker(selectedImage: $selectedImage) { uiImage in
155
+ isLoading = true
156
+ canSave = false
157
+
158
+ // 异步方法必须在 Task 中调用
159
+ Task {
160
+ await viewModel.addFaceByUIImageAsync(faceUIImage: uiImage)
161
+ }
162
+
163
+
164
+ // Task {
165
+ // let faceFeature = await viewModel.addFaceByBase64Async(base64: "your Base64 String")
166
+ // print("return faceFeature:"+faceFeature)
167
+ // }
168
+
169
+ }
170
+ }
171
+
172
+ }
173
+ }
174
+ }