@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.
- package/LICENSE +1 -0
- package/README.md +252 -0
- package/android/build.gradle +53 -0
- package/android/libs/FaceSDKLib-release.aar +0 -0
- package/android/proguard-rules.pro +3 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/faceaisdk/reactnative/FaceRNModule.kt +383 -0
- package/android/src/main/java/com/faceaisdk/reactnative/FaceRNPackage.kt +16 -0
- package/ios/FaceAISDK/CustomToastView.swift +34 -0
- package/ios/FaceAISDK/FaceAINaviView.swift +212 -0
- package/ios/FaceAISDK/FaceSDKCameraView.swift +40 -0
- package/ios/FaceAISDK/FaceSDKLocalizer.swift +21 -0
- package/ios/FaceAISDK/LivenessDetectView.swift +317 -0
- package/ios/FaceAISDK/ScreenBrightnessHelper.swift +100 -0
- package/ios/FaceAISDK/TTSPlayer.swift +357 -0
- package/ios/FaceAISDK/VerifyFaceView.swift +284 -0
- package/ios/FaceAISDK/addFace/AddFaceByCamera.swift +207 -0
- package/ios/FaceAISDK/addFace/AddFaceByImage.swift +174 -0
- package/ios/FaceAISDK/addFace/ImagePicker.swift +52 -0
- package/ios/FaceAISDK/addFace/VerifyTwoFaceSimiView.swift +210 -0
- package/ios/FaceColorExtensions.swift +10 -0
- package/ios/FaceRNModule.h +9 -0
- package/ios/FaceRNModule.m +197 -0
- package/ios/FaceSDKSwiftManager.swift +277 -0
- package/ios/Resources/en.lproj/Localizable.strings +51 -0
- package/ios/Resources/light_too_high.png +0 -0
- package/ios/Resources/zh-Hans.lproj/Localizable.strings +51 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +112 -0
- package/lib/types.d.ts +39 -0
- package/lib/types.js +2 -0
- package/package.json +88 -0
- package/react-native-face-sdk.podspec +28 -0
- package/src/index.ts +184 -0
- 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
|
+
}
|