Migrate from iOS 17 Observation to iOS 16-compatible Combine architecture.

Lower deployment target to iOS 16, replace @Observable with ObservableObject, add navigation and UI compatibility shims, and include login smoke UI tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 10:16:35 +08:00
parent c32a610ee0
commit 26f4d0e671
127 changed files with 2320 additions and 1465 deletions

View File

@ -0,0 +1,75 @@
//
// 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()
}
}
}