为 TabBar 和扫码页补充 accessibilityIdentifier,并在模拟器/UI Test 中禁用真实相机采集。
预初始化各 Tab 路由容器,修正 iOS 17 ContentUnavailableView 调用,提升自定义 TabBar 与扫码流程的 UI Test 稳定性。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -81,16 +81,22 @@ final class AppRouter: ObservableObject {
|
||||
@Published var selectedTab: AppTab = .home
|
||||
@Published var selectedOrdersEntry: OrdersEntry = .storeOrders
|
||||
@Published private(set) var pendingOrderScanCode: String?
|
||||
@Published private var routers: [AppTab: RouterPath] = [:]
|
||||
private let routers: [AppTab: RouterPath]
|
||||
|
||||
/// 获取指定 Tab 对应的路由路径容器,不存在时自动创建。
|
||||
init() {
|
||||
routers = Dictionary(
|
||||
uniqueKeysWithValues: AppTab.allCases.map { tab in
|
||||
(tab, RouterPath())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取指定 Tab 对应的路由路径容器。
|
||||
func router(for tab: AppTab) -> RouterPath {
|
||||
if let router = routers[tab] {
|
||||
return router
|
||||
guard let router = routers[tab] else {
|
||||
assertionFailure("Missing router path for tab: \(tab)")
|
||||
return RouterPath()
|
||||
}
|
||||
|
||||
let router = RouterPath()
|
||||
routers[tab] = router
|
||||
return router
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// iOS 16 兼容的空状态视图;iOS 17+ 自动使用系统 AppContentUnavailableView。
|
||||
/// iOS 16 兼容的空状态视图;iOS 17+ 自动使用系统 ContentUnavailableView。
|
||||
struct AppContentUnavailableView<LabelContent: View, DescriptionContent: View, ActionsContent: View>: View {
|
||||
private let label: LabelContent
|
||||
private let description: DescriptionContent
|
||||
@ -25,7 +25,7 @@ struct AppContentUnavailableView<LabelContent: View, DescriptionContent: View, A
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 17.0, *) {
|
||||
AppContentUnavailableView {
|
||||
ContentUnavailableView {
|
||||
label
|
||||
} description: {
|
||||
description
|
||||
|
||||
@ -106,6 +106,7 @@ private struct CustomMainTabBarItem: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(item.title)
|
||||
.accessibilityIdentifier("main.tab.\(item.tab.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +129,7 @@ private struct CustomMainTabBarCenterAction: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("扫码核销")
|
||||
.accessibilityIdentifier("main.scan")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,23 @@ struct OrderScannerPage: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
content
|
||||
.navigationTitle("扫码核销")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("关闭") {
|
||||
onClose()
|
||||
}
|
||||
.accessibilityIdentifier("scanner.close")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if shouldUseCameraPreview {
|
||||
ZStack(alignment: .bottom) {
|
||||
OrderCodeScannerView { result in
|
||||
switch result {
|
||||
@ -53,18 +70,40 @@ struct OrderScannerPage: View {
|
||||
.background(.black.opacity(0.55), in: Capsule())
|
||||
.padding(.bottom, AppMetrics.Spacing.xxLarge)
|
||||
}
|
||||
.navigationTitle("扫码核销")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("关闭") {
|
||||
onClose()
|
||||
}
|
||||
.accessibilityIdentifier("scanner.close")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ScannerUnavailableContent()
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldUseCameraPreview: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return false
|
||||
#else
|
||||
guard !AppUITestLaunchState.isRunningUITests else { return false }
|
||||
return AVCaptureDevice.default(for: .video) != nil
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// 无可用摄像头时展示的扫码占位内容,避免在模拟器和 UI Test 中启动真实采集会话。
|
||||
private struct ScannerUnavailableContent: View {
|
||||
var body: some View {
|
||||
VStack(spacing: AppMetrics.Spacing.small) {
|
||||
Image(systemName: "camera.viewfinder")
|
||||
.font(.system(size: 48, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text("当前环境不可用相机")
|
||||
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text("请在真机上使用扫码核销")
|
||||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||||
.foregroundStyle(.white.opacity(0.72))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black)
|
||||
}
|
||||
}
|
||||
|
||||
/// AVFoundation 扫码桥接视图,负责把相机识别结果回传给 SwiftUI。
|
||||
@ -229,7 +268,7 @@ final class OrderScannerViewController: UIViewController, AVCaptureMetadataOutpu
|
||||
session.addOutput(metadataOutput)
|
||||
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
||||
metadataOutput.metadataObjectTypes = [
|
||||
let desiredTypes: [AVMetadataObject.ObjectType] = [
|
||||
.qr,
|
||||
.ean8,
|
||||
.ean13,
|
||||
@ -242,6 +281,12 @@ final class OrderScannerViewController: UIViewController, AVCaptureMetadataOutpu
|
||||
.itf14,
|
||||
.upce
|
||||
]
|
||||
let availableTypes = Set(metadataOutput.availableMetadataObjectTypes)
|
||||
let supportedTypes = desiredTypes.filter { availableTypes.contains($0) }
|
||||
guard !supportedTypes.isEmpty else {
|
||||
throw OrderScannerError.invalidMetadata
|
||||
}
|
||||
metadataOutput.metadataObjectTypes = supportedTypes
|
||||
|
||||
let preview = AVCaptureVideoPreviewLayer(session: session)
|
||||
preview.videoGravity = .resizeAspectFill
|
||||
|
||||
Reference in New Issue
Block a user