feat: 移除CreateFeedView-Analysis文档并新增用户协议组件以增强用户体验
- 删除CreateFeedView-Analysis.md文档以简化项目结构。 - 新增UserAgreementComponent以处理用户协议的显示和交互。 - 更新多个视图中的onChange逻辑以兼容iOS 17的新API用法,确保代码一致性和可维护性。 - 在Localizable.strings中新增用户协议相关的本地化文本,提升多语言支持。
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
# CreateFeedView UI 结构分析与执行计划
|
||||
|
||||
## UI 结构分析
|
||||
|
||||
根据设计稿,CreateFeedView 应包含以下UI元素:
|
||||
|
||||
### 1. 顶部导航栏
|
||||
- 左侧:返回按钮
|
||||
- 中间:"图文发布" 标题
|
||||
- 右侧:"发布" 按钮
|
||||
|
||||
### 2. 主要内容区域
|
||||
- 文本输入框:"Enter Content" 占位符,支持多行输入,最大500字符
|
||||
- 字符计数显示:"0/500" 格式
|
||||
- 图片添加区域:
|
||||
- 默认显示一个 "+" 按钮(使用 "add photo" 图片资源)
|
||||
- 支持添加最多9张图片
|
||||
- 图片以网格形式排列
|
||||
- 每张图片可以删除
|
||||
|
||||
### 3. 底部发布按钮
|
||||
- 紫色渐变背景的"发布"按钮
|
||||
- 占据屏幕底部,固定位置
|
||||
|
||||
## 执行计划
|
||||
|
||||
### 第一步:创建 CreateFeedFeature
|
||||
- 定义状态管理结构
|
||||
- 实现文本输入、图片选择、发布等Action
|
||||
- 添加表单验证逻辑
|
||||
- 集成图片选择器
|
||||
|
||||
### 第二步:创建 CreateFeedView
|
||||
- 实现顶部导航栏
|
||||
- 创建文本输入区域
|
||||
- 实现图片选择和展示网格
|
||||
- 添加发布按钮
|
||||
- 应用深色主题样式
|
||||
|
||||
### 第三步:集成到 FeedView
|
||||
- 修改 FeedView 中的加号按钮点击事件
|
||||
- 添加导航到 CreateFeedView 的逻辑
|
||||
- 确保返回时能刷新动态列表
|
||||
|
||||
### 第四步:创建发布API模型
|
||||
- 定义发布动态的请求和响应模型
|
||||
- 添加API端点定义
|
||||
- 实现发布逻辑(模拟或真实API)
|
||||
|
||||
### 第五步:测试和优化
|
||||
- 测试各种输入场景
|
||||
- 验证图片选择和预览功能
|
||||
- 确保UI响应和交互流畅
|
||||
|
||||
## 技术要点
|
||||
|
||||
1. **状态管理**:使用 ComposableArchitecture 模式
|
||||
2. **图片选择**:使用 PhotosUI 框架
|
||||
3. **UI样式**:保持与现有深色主题一致
|
||||
4. **表单验证**:实时字符计数和输入限制
|
||||
5. **导航管理**:使用 NavigationStack 或 sheet 展示
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
yana/
|
||||
├── Features/
|
||||
│ └── CreateFeedFeature.swift # 新建
|
||||
├── Views/
|
||||
│ └── CreateFeedView.swift # 新建
|
||||
├── APIs/
|
||||
│ ├── APIEndpoints.swift # 修改:添加发布端点
|
||||
│ └── DynamicsModels.swift # 修改:添加发布模型
|
||||
└── Assets.xcassets/
|
||||
└── Home/
|
||||
└── add photo.imageset/ # 已存在
|
||||
```
|
||||
|
||||
开始实施第一步:创建 CreateFeedFeature。
|
67
issues/onChange iOS17 迁移.md
Normal file
67
issues/onChange iOS17 迁移.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# onChange iOS 17 迁移总结
|
||||
|
||||
## 概述
|
||||
将项目中所有使用已弃用的 `onChange(of:perform:)` API 的代码修改为 iOS 17 建议的新用法。
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 修改规则
|
||||
- **旧用法**: `onChange(of: value) { newValue in ... }`
|
||||
- **新用法**: `onChange(of: value) { oldValue, newValue in ... }`
|
||||
|
||||
### 修改的文件列表
|
||||
|
||||
1. **LoginView.swift** - 3处修改
|
||||
- `store.isAnyLoginCompleted` 监听
|
||||
- `showIDLogin` 监听
|
||||
- `showEmailLogin` 监听
|
||||
|
||||
2. **MainView.swift** - 3处修改
|
||||
- `store.isLoggedOut` 监听
|
||||
- `path` 监听
|
||||
- `store.navigationPath` 监听
|
||||
|
||||
3. **EMailLoginView.swift** - 4处修改
|
||||
- `store.loginStep` 监听
|
||||
- `email` 监听
|
||||
- `verificationCode` 监听
|
||||
- `store.isCodeLoading` 监听
|
||||
|
||||
4. **RecoverPasswordView.swift** - 4处修改
|
||||
- `email` 监听
|
||||
- `verificationCode` 监听
|
||||
- `newPassword` 监听
|
||||
- `store.isResetSuccess` 监听
|
||||
|
||||
5. **ImagePickerWithPreviewView.swift** - 2处修改
|
||||
- `viewStore.inner.isLoading` 监听
|
||||
- `viewStore.inner.selectedPhotoItems` 监听
|
||||
|
||||
6. **EditFeedView.swift** - 1处修改
|
||||
- `store.shouldDismiss` 监听
|
||||
|
||||
7. **DetailView.swift** - 1处修改
|
||||
- `store.shouldDismiss` 监听
|
||||
|
||||
8. **MeView.swift** - 1处修改
|
||||
- `detailStore.shouldDismiss` 监听
|
||||
|
||||
9. **IDLoginView.swift** - 1处修改
|
||||
- `store.loginStep` 监听
|
||||
|
||||
10. **ContentView.swift** - 1处修改
|
||||
- `selectedLogLevel` 监听
|
||||
|
||||
## 总计
|
||||
- **修改文件数**: 10个
|
||||
- **修改处数**: 20处
|
||||
- **状态**: ✅ 完成
|
||||
|
||||
## 验证结果
|
||||
通过 grep 搜索确认所有 `onChange(of:perform:)` 调用都已成功迁移到新 API。
|
||||
|
||||
## 注意事项
|
||||
1. 新 API 提供了 `oldValue` 和 `newValue` 两个参数
|
||||
2. 在大多数情况下,我们只使用了 `newValue` 参数,`oldValue` 用 `_` 忽略
|
||||
3. 所有原有逻辑保持不变,只是 API 调用方式更新
|
||||
4. 修改后的代码完全兼容 iOS 17+ 的要求
|
16
ui-demo.swift
Normal file
16
ui-demo.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
let label = UILabel()
|
||||
let attrString = NSMutableAttributedString(string: "Agree to the "User Service Agreement" and "Privacy Policy"")
|
||||
label.frame = CGRect(x: 71, y: 735, width: 256, height: 34)
|
||||
label.numberOfLines = 0
|
||||
let attr: [NSAttributedString.Key : Any] = [.font: UIFont(name: "PingFang SC-Regular", size: 12),.foregroundColor: UIColor(red: 1, green: 1, blue: 1, alpha: 1)]
|
||||
attrString.addAttributes(attr, range: NSRange(location: 0, length: attrString.length))
|
||||
view.addSubview(label)
|
||||
let strSubAttr1: [NSMutableAttributedString.Key: Any] = [.font: UIFont(name: "PingFang SC-Regular", size: 12),.foregroundColor: UIColor(red: 1, green: 1, blue: 1,alpha:1)]
|
||||
attrString.addAttributes(strSubAttr1, range: NSRange(location: 0, length: 13))
|
||||
let strSubAttr2: [NSMutableAttributedString.Key: Any] = [.font: UIFont(name: "PingFang SC-Regular", size: 12),.foregroundColor: UIColor(red: 0.78, green: 0.35, blue: 1,alpha:1)]
|
||||
attrString.addAttributes(strSubAttr2, range: NSRange(location: 13, length: 24))
|
||||
let strSubAttr3: [NSMutableAttributedString.Key: Any] = [.font: UIFont(name: "PingFang SC-Regular", size: 12),.foregroundColor: UIColor(red: 1, green: 1, blue: 1,alpha:1)]
|
||||
attrString.addAttributes(strSubAttr3, range: NSRange(location: 37, length: 5))
|
||||
let strSubAttr4: [NSMutableAttributedString.Key: Any] = [.font: UIFont(name: "PingFang SC-Regular", size: 12),.foregroundColor: UIColor(red: 0.78, green: 0.35, blue: 1,alpha:1)]
|
||||
attrString.addAttributes(strSubAttr4, range: NSRange(location: 42, length: 16))
|
||||
label.attributedText = attrString
|
@@ -187,7 +187,7 @@ struct ContentView: View {
|
||||
}
|
||||
.tag(1)
|
||||
}
|
||||
.onChange(of: selectedLogLevel) {
|
||||
.onChange(of: selectedLogLevel) { _, selectedLogLevel in
|
||||
APILogger.logLevel = selectedLogLevel
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,9 @@
|
||||
"login.agreement_policy" = "Agree to the \"User Service Agreement\" and \"Privacy Policy\"";
|
||||
"login.agreement" = "User Service Agreement";
|
||||
"login.policy" = "Privacy Policy";
|
||||
"login.agreement_alert_title" = "Notice";
|
||||
"login.agreement_alert_message" = "Please agree to the User Service Agreement and Privacy Policy first";
|
||||
"login.agreement_alert_confirm" = "OK";
|
||||
|
||||
// MARK: - Common Buttons
|
||||
"common.login" = "Login";
|
||||
|
@@ -13,6 +13,9 @@
|
||||
"login.agreement_policy" = "同意《用戶服務協議》和《隱私政策》";
|
||||
"login.agreement" = "《用戶服務協議》";
|
||||
"login.policy" = "《隱私政策》";
|
||||
"login.agreement_alert_title" = "提示";
|
||||
"login.agreement_alert_message" = "请先同意用户服务协议和隐私政策";
|
||||
"login.agreement_alert_confirm" = "确定";
|
||||
|
||||
// MARK: - 通用按钮
|
||||
"common.login" = "登录";
|
||||
|
@@ -22,13 +22,13 @@ public struct ImagePickerWithPreviewView: View {
|
||||
ZStack {
|
||||
Color.clear
|
||||
}
|
||||
.background(.clear)
|
||||
.background(.red)
|
||||
.ignoresSafeArea()
|
||||
.modifier(CameraSheetModifier(viewStore: viewStore, onCancel: onCancel))
|
||||
.modifier(PhotosPickerModifier(viewStore: viewStore, loadedImages: $loadedImages, isLoadingImages: $isLoadingImages, loadingId: $loadingId, onCancel: onCancel))
|
||||
.modifier(PreviewCoverModifier(viewStore: viewStore, loadedImages: loadedImages, onUpload: onUpload, loadingId: $loadingId, onCancel: onCancel))
|
||||
.modifier(ErrorToastModifier(viewStore: viewStore))
|
||||
.onChange(of: viewStore.inner.isLoading) { isLoading in
|
||||
.onChange(of: viewStore.inner.isLoading) { _, isLoading in
|
||||
if isLoading && loadingId == nil {
|
||||
loadingId = APILoadingManager.shared.startLoading()
|
||||
} else if !isLoading, let id = loadingId {
|
||||
@@ -92,7 +92,7 @@ private struct PhotosPickerModifier: ViewModifier {
|
||||
}(),
|
||||
matching: .images
|
||||
)
|
||||
.onChange(of: viewStore.inner.selectedPhotoItems) { items in
|
||||
.onChange(of: viewStore.inner.selectedPhotoItems) { _, items in
|
||||
guard !items.isEmpty else { return }
|
||||
isLoadingImages = true
|
||||
loadedImages = []
|
||||
|
165
yana/Views/Components/UserAgreementComponent.swift
Normal file
165
yana/Views/Components/UserAgreementComponent.swift
Normal file
@@ -0,0 +1,165 @@
|
||||
import SwiftUI
|
||||
|
||||
struct UserAgreementComponent: View {
|
||||
@Binding var isAgreed: Bool
|
||||
let onAgreementTap: @Sendable () -> Void
|
||||
let onPolicyTap: @Sendable () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
// 左侧按钮
|
||||
Button(action: {
|
||||
isAgreed.toggle()
|
||||
}) {
|
||||
Image(isAgreed ? "selected icon" : "unselected icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
// 右侧富文本
|
||||
RichTextAgreementView(
|
||||
isAgreed: $isAgreed,
|
||||
onAgreementTap: onAgreementTap,
|
||||
onPolicyTap: onPolicyTap
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RichTextAgreementView: UIViewRepresentable {
|
||||
@Binding var isAgreed: Bool
|
||||
let onAgreementTap: @Sendable () -> Void
|
||||
let onPolicyTap: @Sendable () -> Void
|
||||
|
||||
func makeUIView(context: Context) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 3
|
||||
label.isUserInteractionEnabled = true
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
|
||||
label.addGestureRecognizer(tapGesture)
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
func updateUIView(_ label: UILabel, context: Context) {
|
||||
let fullText = LocalizedString("login.agreement_policy", comment: "")
|
||||
let agreementText = LocalizedString("login.agreement", comment: "")
|
||||
let policyText = LocalizedString("login.policy", comment: "")
|
||||
|
||||
let attrString = NSMutableAttributedString(string: fullText)
|
||||
|
||||
// 设置基础样式
|
||||
let baseAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.systemFont(ofSize: 12),
|
||||
.foregroundColor: UIColor.white
|
||||
]
|
||||
attrString.addAttributes(baseAttributes, range: NSRange(location: 0, length: attrString.length))
|
||||
|
||||
// 查找并设置协议文本的样式和点击区域
|
||||
if let agreementRange = fullText.range(of: agreementText) {
|
||||
let nsRange = NSRange(agreementRange, in: fullText)
|
||||
let agreementAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.systemFont(ofSize: 12),
|
||||
.foregroundColor: UIColor(red: 0.78, green: 0.35, blue: 1, alpha: 1),
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
]
|
||||
attrString.addAttributes(agreementAttributes, range: nsRange)
|
||||
context.coordinator.agreementRange = nsRange
|
||||
}
|
||||
|
||||
if let policyRange = fullText.range(of: policyText) {
|
||||
let nsRange = NSRange(policyRange, in: fullText)
|
||||
let policyAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.systemFont(ofSize: 12),
|
||||
.foregroundColor: UIColor(red: 0.78, green: 0.35, blue: 1, alpha: 1),
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
]
|
||||
attrString.addAttributes(policyAttributes, range: nsRange)
|
||||
context.coordinator.policyRange = nsRange
|
||||
}
|
||||
|
||||
label.attributedText = attrString
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onAgreementTap: onAgreementTap, onPolicyTap: onPolicyTap)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject {
|
||||
let onAgreementTap: @Sendable () -> Void
|
||||
let onPolicyTap: @Sendable () -> Void
|
||||
var agreementRange: NSRange?
|
||||
var policyRange: NSRange?
|
||||
|
||||
init(onAgreementTap: @escaping @Sendable () -> Void, onPolicyTap: @escaping @Sendable () -> Void) {
|
||||
self.onAgreementTap = onAgreementTap
|
||||
self.onPolicyTap = onPolicyTap
|
||||
}
|
||||
|
||||
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
|
||||
// 在方法开始时捕获需要的值,避免在Task中访问self
|
||||
let agreementRange = self.agreementRange
|
||||
let policyRange = self.policyRange
|
||||
let onAgreementTap = self.onAgreementTap
|
||||
let onPolicyTap = self.onPolicyTap
|
||||
|
||||
Task { @MainActor in
|
||||
guard let label = gesture.view as? UILabel,
|
||||
let attributedText = label.attributedText else { return }
|
||||
|
||||
let location = gesture.location(in: label)
|
||||
|
||||
// 创建文本容器
|
||||
let textContainer = NSTextContainer(size: label.bounds.size)
|
||||
textContainer.lineFragmentPadding = 0
|
||||
textContainer.maximumNumberOfLines = label.numberOfLines
|
||||
textContainer.lineBreakMode = label.lineBreakMode
|
||||
|
||||
// 创建布局管理器
|
||||
let layoutManager = NSLayoutManager()
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
|
||||
// 创建文本存储
|
||||
let textStorage = NSTextStorage(attributedString: attributedText)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
// 计算点击位置对应的字符索引
|
||||
let characterIndex = layoutManager.characterIndex(
|
||||
for: location,
|
||||
in: textContainer,
|
||||
fractionOfDistanceBetweenInsertionPoints: nil
|
||||
)
|
||||
|
||||
// 检查是否点击了协议文本
|
||||
if let agreementRange = agreementRange,
|
||||
NSLocationInRange(characterIndex, agreementRange) {
|
||||
await MainActor.run {
|
||||
onAgreementTap()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let policyRange = policyRange,
|
||||
NSLocationInRange(characterIndex, policyRange) {
|
||||
await MainActor.run {
|
||||
onPolicyTap()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
UserAgreementComponent(
|
||||
isAgreed: .constant(true),
|
||||
onAgreementTap: {},
|
||||
onPolicyTap: {}
|
||||
)
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
}
|
@@ -67,7 +67,7 @@ struct DetailView: View {
|
||||
debugInfoSync("🔍 DetailView: onAppear - moment.uid: \(store.moment.uid)")
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.onChange(of: store.shouldDismiss) { shouldDismiss in
|
||||
.onChange(of: store.shouldDismiss) { _, shouldDismiss in
|
||||
if shouldDismiss {
|
||||
debugInfoSync("🔍 DetailView: shouldDismiss = true, 调用onDismiss")
|
||||
// 移除 onDismiss?() 移除此行,因为我们现在使用 dismiss
|
||||
|
@@ -51,7 +51,7 @@ struct EMailLoginView: View {
|
||||
getCodeButtonText: getCodeButtonText,
|
||||
isCodeButtonEnabled: isCodeButtonEnabled
|
||||
)
|
||||
.onChange(of: store.loginStep) { newStep in
|
||||
.onChange(of: store.loginStep) { _, newStep in
|
||||
debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)")
|
||||
if newStep == .completed {
|
||||
debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身")
|
||||
@@ -77,17 +77,17 @@ struct EMailLoginView: View {
|
||||
stopCountdown()
|
||||
}
|
||||
}
|
||||
.onChange(of: email) { newEmail in
|
||||
.onChange(of: email) { _, newEmail in
|
||||
let _ = WithPerceptionTracking {
|
||||
store.send(.emailChanged(newEmail))
|
||||
}
|
||||
}
|
||||
.onChange(of: verificationCode) { newCode in
|
||||
.onChange(of: verificationCode) { _, newCode in
|
||||
let _ = WithPerceptionTracking {
|
||||
store.send(.verificationCodeChanged(newCode))
|
||||
}
|
||||
}
|
||||
.onChange(of: store.isCodeLoading) { isCodeLoading in
|
||||
.onChange(of: store.isCodeLoading) { _, isCodeLoading in
|
||||
let _ = WithPerceptionTracking {
|
||||
if !isCodeLoading && store.errorMessage == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
|
@@ -44,8 +44,8 @@ struct EditFeedView: View {
|
||||
.onAppear {
|
||||
store.send(.clearError)
|
||||
}
|
||||
.onChange(of: store.shouldDismiss) {
|
||||
if store.shouldDismiss {
|
||||
.onChange(of: store.shouldDismiss) { _, shouldDismiss in
|
||||
if shouldDismiss {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
@@ -299,7 +299,7 @@ struct IDLoginView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.onChange(of: store.loginStep) { newStep in
|
||||
.onChange(of: store.loginStep) { _, newStep in
|
||||
debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)")
|
||||
if newStep == .completed {
|
||||
debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身")
|
||||
|
@@ -2,14 +2,6 @@ import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Perception
|
||||
|
||||
// PreferenceKey 用于传递图片高度
|
||||
struct ImageHeightPreferenceKey: PreferenceKey {
|
||||
static let defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value = max(value, nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginView: View {
|
||||
let store: StoreOf<LoginFeature>
|
||||
let onLoginSuccess: () -> Void
|
||||
@@ -20,16 +12,44 @@ struct LoginView: View {
|
||||
@State private var showLanguageSettings: Bool = false
|
||||
@State private var showUserAgreement: Bool = false
|
||||
@State private var showPrivacyPolicy: Bool = false
|
||||
@State private var isAgreementAccepted: Bool = true // 默认选中
|
||||
@State private var showAgreementAlert: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
backgroundView
|
||||
mainContentView(geometry: geometry)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
|
||||
Image("top")
|
||||
.resizable()
|
||||
.aspectRatio(375/400, contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack {
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom, 20) // 距离 top 图片底部的间距
|
||||
|
||||
Spacer()
|
||||
|
||||
bottomSection
|
||||
}
|
||||
|
||||
// 语言设置按钮 - 固定在页面右上角
|
||||
languageSettingsButton
|
||||
.position(x: geometry.size.width - 40, y: 60)
|
||||
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.navigationBarHidden(true)
|
||||
.navigationDestination(isPresented: $showIDLogin) {
|
||||
IDLoginView(
|
||||
@@ -68,17 +88,22 @@ struct LoginView: View {
|
||||
isPresented: $showPrivacyPolicy,
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
.onChange(of: store.isAnyLoginCompleted) {
|
||||
if store.isAnyLoginCompleted {
|
||||
.alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $showAgreementAlert) {
|
||||
Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { }
|
||||
} message: {
|
||||
Text(LocalizedString("login.agreement_alert_message", comment: ""))
|
||||
}
|
||||
.onChange(of: store.isAnyLoginCompleted) { _, isAnyLoginCompleted in
|
||||
if isAnyLoginCompleted {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
.onChange(of: showIDLogin) {
|
||||
.onChange(of: showIDLogin) { _, showIDLogin in
|
||||
if showIDLogin == false && store.isAnyLoginCompleted {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
.onChange(of: showEmailLogin) {
|
||||
.onChange(of: showEmailLogin) { _, showEmailLogin in
|
||||
if showEmailLogin == false && store.isAnyLoginCompleted {
|
||||
onLoginSuccess()
|
||||
}
|
||||
@@ -95,44 +120,13 @@ struct LoginView: View {
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
|
||||
private func mainContentView(geometry: GeometryProxy) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
topSection(geometry: geometry)
|
||||
bottomSection
|
||||
}
|
||||
}
|
||||
|
||||
private func topSection(geometry: GeometryProxy) -> some View {
|
||||
ZStack {
|
||||
Image("top")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, -100)
|
||||
.background(
|
||||
GeometryReader { topImageGeometry in
|
||||
Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height)
|
||||
}
|
||||
)
|
||||
|
||||
HStack {
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 100) // 简化计算逻辑
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomSection: some View {
|
||||
VStack(spacing: 20) {
|
||||
loginButtons
|
||||
bottomButtons
|
||||
userAgreementComponent
|
||||
}
|
||||
.padding(.horizontal, 28)
|
||||
.padding(.bottom, 140)
|
||||
.padding(.bottom, 48)
|
||||
}
|
||||
|
||||
private var loginButtons: some View {
|
||||
@@ -142,7 +136,11 @@ struct LoginView: View {
|
||||
iconColor: .blue,
|
||||
title: LocalizedString("login.id_login", comment: ""),
|
||||
action: {
|
||||
showIDLogin = true
|
||||
if isAgreementAccepted {
|
||||
showIDLogin = true
|
||||
} else {
|
||||
showAgreementAlert = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -151,39 +149,43 @@ struct LoginView: View {
|
||||
iconColor: .green,
|
||||
title: LocalizedString("login.email_login", comment: ""),
|
||||
action: {
|
||||
showEmailLogin = true
|
||||
if isAgreementAccepted {
|
||||
showEmailLogin = true
|
||||
} else {
|
||||
showAgreementAlert = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomButtons: some View {
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {
|
||||
showLanguageSettings = true
|
||||
}) {
|
||||
Text(LocalizedString("setting.language", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showUserAgreement = true
|
||||
}) {
|
||||
Text(LocalizedString("login.agreement", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showPrivacyPolicy = true
|
||||
}) {
|
||||
Text(LocalizedString("login.policy", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
private var languageSettingsButton: some View {
|
||||
Button(action: {
|
||||
showLanguageSettings = true
|
||||
}) {
|
||||
Image(systemName: "globe")
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
}
|
||||
|
||||
private var userAgreementComponent: some View {
|
||||
UserAgreementComponent(
|
||||
isAgreed: $isAgreementAccepted,
|
||||
onAgreementTap: {
|
||||
Task { @MainActor in
|
||||
showUserAgreement = true
|
||||
}
|
||||
},
|
||||
onPolicyTap: {
|
||||
Task { @MainActor in
|
||||
showPrivacyPolicy = true
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(height: 40)
|
||||
.padding(.horizontal, -20)
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
|
@@ -8,8 +8,8 @@ struct MainView: View {
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
InternalMainView(store: store)
|
||||
.onChange(of: store.isLoggedOut) {
|
||||
if store.isLoggedOut {
|
||||
.onChange(of: store.isLoggedOut) { _, isLoggedOut in
|
||||
if isLoggedOut {
|
||||
onLogout?()
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,12 @@ struct InternalMainView: View {
|
||||
.navigationDestination(for: MainFeature.Destination.self) { destination in
|
||||
DestinationView(destination: destination, store: self.store)
|
||||
}
|
||||
.onChange(of: path) {
|
||||
.onChange(of: path) { _, path in
|
||||
store.send(.navigationPathChanged(path))
|
||||
}
|
||||
.onChange(of: store.navigationPath) {
|
||||
if path != store.navigationPath {
|
||||
path = store.navigationPath
|
||||
.onChange(of: store.navigationPath) { _, navigationPath in
|
||||
if path != navigationPath {
|
||||
path = navigationPath
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
@@ -72,7 +72,7 @@ struct MeView: View {
|
||||
}
|
||||
|
||||
DetailView(store: detailStore)
|
||||
.onChange(of: detailStore.shouldDismiss) { shouldDismiss in
|
||||
.onChange(of: detailStore.shouldDismiss) { _, shouldDismiss in
|
||||
if shouldDismiss {
|
||||
store.send(.detailDismissed)
|
||||
}
|
||||
|
@@ -135,16 +135,16 @@ struct RecoverPasswordView: View {
|
||||
.onDisappear {
|
||||
stopCountdown()
|
||||
}
|
||||
.onChange(of: email) { newEmail in
|
||||
.onChange(of: email) { _, newEmail in
|
||||
store.send(.emailChanged(newEmail))
|
||||
}
|
||||
.onChange(of: verificationCode) { newCode in
|
||||
.onChange(of: verificationCode) { _, newCode in
|
||||
store.send(.verificationCodeChanged(newCode))
|
||||
}
|
||||
.onChange(of: newPassword) { newPassword in
|
||||
.onChange(of: newPassword) { _, newPassword in
|
||||
store.send(.newPasswordChanged(newPassword))
|
||||
}
|
||||
.onChange(of: store.isResetSuccess) { isResetSuccess in
|
||||
.onChange(of: store.isResetSuccess) { _, isResetSuccess in
|
||||
if isResetSuccess {
|
||||
onBack()
|
||||
}
|
||||
|
Reference in New Issue
Block a user