将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
76 lines
2.3 KiB
Swift
76 lines
2.3 KiB
Swift
//
|
||
// AppContentUnavailableView.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/26.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// iOS 16 兼容的空状态视图;iOS 17+ 自动使用系统 AppContentUnavailableView。
|
||
struct AppContentUnavailableView<LabelContent: View, DescriptionContent: View, ActionsContent: View>: View {
|
||
private let label: LabelContent
|
||
private let description: DescriptionContent
|
||
private let actions: ActionsContent
|
||
|
||
init(
|
||
@ViewBuilder label: () -> LabelContent,
|
||
@ViewBuilder description: () -> DescriptionContent,
|
||
@ViewBuilder actions: () -> ActionsContent
|
||
) {
|
||
self.label = label()
|
||
self.description = description()
|
||
self.actions = actions()
|
||
}
|
||
|
||
var body: some View {
|
||
if #available(iOS 17.0, *) {
|
||
AppContentUnavailableView {
|
||
label
|
||
} description: {
|
||
description
|
||
} actions: {
|
||
actions
|
||
}
|
||
} else {
|
||
VStack(spacing: AppMetrics.Spacing.small) {
|
||
label
|
||
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
|
||
.foregroundStyle(AppDesign.textPrimary)
|
||
description
|
||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
.multilineTextAlignment(.center)
|
||
actions
|
||
.padding(.top, AppMetrics.Spacing.xSmall)
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.padding(AppMetrics.Spacing.large)
|
||
}
|
||
}
|
||
}
|
||
|
||
extension AppContentUnavailableView where LabelContent == Label<Text, Image>, DescriptionContent == EmptyView, ActionsContent == EmptyView {
|
||
init(_ title: String, systemImage: String) {
|
||
self.init {
|
||
Label(title, systemImage: systemImage)
|
||
} description: {
|
||
EmptyView()
|
||
} actions: {
|
||
EmptyView()
|
||
}
|
||
}
|
||
}
|
||
|
||
extension AppContentUnavailableView where LabelContent == Label<Text, Image>, DescriptionContent == Text, ActionsContent == EmptyView {
|
||
init(_ title: String, systemImage: String, description: Text) {
|
||
self.init {
|
||
Label(title, systemImage: systemImage)
|
||
} description: {
|
||
description
|
||
} actions: {
|
||
EmptyView()
|
||
}
|
||
}
|
||
}
|