feat: 更新.gitignore,删除需求文档,优化API调试信息

- 在.gitignore中添加忽略项以排除不必要的文件。
- 删除架构分析需求文档以简化项目文档。
- 在APIEndpoints.swift和LoginModels.swift中移除调试信息的异步调用,提升代码简洁性。
- 在EMailLoginFeature.swift和HomeFeature.swift中新增登录流程状态管理,优化用户体验。
- 在多个视图中调整状态管理和导航逻辑,确保一致性和可维护性。
- 更新Xcode项目配置以增强调试信息的输出格式。
This commit is contained in:
edwinQQQ
2025-07-18 15:57:54 +08:00
parent 128bf36c88
commit fb7ae9e0ad
20 changed files with 562 additions and 720 deletions

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkp
yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
Doc Doc
DerivedData DerivedData
.kiro
yana.xcworkspace/xcuserdata

View File

@@ -1,51 +0,0 @@
# Requirements Document
## Introduction
This document outlines the requirements for analyzing the architecture of the Yana iOS application and providing recommendations for improvements. The analysis will focus on evaluating the current architecture, identifying strengths and weaknesses, and suggesting enhancements to improve code quality, maintainability, and scalability.
## Requirements
### Requirement 1
**User Story:** As a developer, I want to understand the current architecture of the Yana iOS application, so that I can identify areas for improvement.
#### Acceptance Criteria
1. WHEN analyzing the project structure THEN the system SHALL identify the main architectural patterns used
2. WHEN reviewing the codebase THEN the system SHALL document the key components and their relationships
3. WHEN examining the dependencies THEN the system SHALL list all major frameworks and libraries used
4. WHEN evaluating the project organization THEN the system SHALL assess the folder structure and file organization
### Requirement 2
**User Story:** As a developer, I want to identify strengths and weaknesses in the current architecture, so that I can leverage strengths and address weaknesses.
#### Acceptance Criteria
1. WHEN reviewing the architecture THEN the system SHALL highlight positive architectural decisions
2. WHEN analyzing the code structure THEN the system SHALL identify potential architectural issues
3. WHEN examining the codebase THEN the system SHALL evaluate code consistency and adherence to best practices
4. WHEN assessing the architecture THEN the system SHALL identify potential bottlenecks or scalability concerns
### Requirement 3
**User Story:** As a developer, I want specific recommendations for architectural improvements, so that I can enhance the application's maintainability and scalability.
#### Acceptance Criteria
1. WHEN providing recommendations THEN the system SHALL suggest specific architectural improvements
2. WHEN suggesting changes THEN the system SHALL explain the benefits of each recommendation
3. WHEN recommending improvements THEN the system SHALL consider the existing technology stack and constraints
4. WHEN proposing architectural changes THEN the system SHALL prioritize recommendations based on impact and effort
### Requirement 4
**User Story:** As a developer, I want to understand how to implement the recommended architectural improvements, so that I can effectively enhance the application.
#### Acceptance Criteria
1. WHEN recommending architectural changes THEN the system SHALL provide implementation guidance
2. WHEN suggesting improvements THEN the system SHALL include code examples where appropriate
3. WHEN proposing architectural changes THEN the system SHALL outline a phased approach for implementation
4. WHEN recommending improvements THEN the system SHALL consider backward compatibility and migration strategies

View File

@@ -471,6 +471,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = Z7UCRF23F3; DEVELOPMENT_TEAM = Z7UCRF23F3;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;

View File

@@ -1,178 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "A60FAB2A-3184-45B2-920F-A3D7A086CF95"
type = "0"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Views/FeedView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "102"
endingLineNumber = "102"
landmarkName = "body"
landmarkType = "24">
<Locations>
<Location
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12 - 6293d947a1803ee3"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (SwiftUI.GeometryProxy) -&gt; &lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0 in closure #1 () -&gt; SwiftUI.GeometryReader&lt;&lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0&gt; in yana.FeedView.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Views/FeedView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "102"
endingLineNumber = "102">
</Location>
<Location
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12 - ba104df0a01f94b"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #4 @Sendable () -&gt; Swift.Bool in closure #1 (SwiftUI.GeometryProxy) -&gt; &lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0 in closure #1 () -&gt; SwiftUI.GeometryReader&lt;&lt;&lt;opaque return type of SwiftUI.View.sheet&lt;&#x3c4;_0_0 where &#x3c4;_1_0: SwiftUI.View&gt;(isPresented: SwiftUI.Binding&lt;Swift.Bool&gt;, onDismiss: Swift.Optional&lt;() -&gt; ()&gt;, content: () -&gt; &#x3c4;_1_0) -&gt; some&gt;&gt;.0&gt; in yana.FeedView.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Views/FeedView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "102"
endingLineNumber = "102">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "3E663F1F-E6A0-45A6-87FC-B05E919ADDEB"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/SplashFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "52"
endingLineNumber = "52"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "B1F260B9-69B0-4607-AB2D-F9ECEC954EDF"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "65"
endingLineNumber = "65"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "2308DE52-487A-4A72-9377-A7C0C09DACD4"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "134"
endingLineNumber = "134"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "44D54396-6B42-4B2E-8621-CB59559FCDB1"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/LoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "168"
endingLineNumber = "168"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "8732BF66-8904-4DD4-9844-B30786433A70"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69"
landmarkName = "body"
landmarkType = "24">
<Locations>
<Location
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 33fd8ff0f3f68ab7"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "(1) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send&lt;yana.IDLoginFeature.Action&gt;) async -&gt; () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -&gt; ComposableArchitecture.Effect&lt;yana.IDLoginFeature.Action&gt; in yana.IDLoginFeature.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69">
</Location>
<Location
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 8f264cf5d91c4ec9"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "(3) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send&lt;yana.IDLoginFeature.Action&gt;) async -&gt; () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -&gt; ComposableArchitecture.Effect&lt;yana.IDLoginFeature.Action&gt; in yana.IDLoginFeature.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69">
</Location>
<Location
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 8f264cf5d91c4ec9"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "(3) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send&lt;yana.IDLoginFeature.Action&gt;) async -&gt; () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -&gt; ComposableArchitecture.Effect&lt;yana.IDLoginFeature.Action&gt; in yana.IDLoginFeature.body.getter : some"
moduleName = "yana.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "69"
endingLineNumber = "69">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@@ -100,20 +100,14 @@ struct APIConfiguration {
// headers AccountModel // headers AccountModel
if let userId = await UserInfoManager.getCurrentUserId() { if let userId = await UserInfoManager.getCurrentUserId() {
headers["pub_uid"] = userId headers["pub_uid"] = userId
#if DEBUG
debugInfoSync("🔐 添加认证 header: pub_uid = \(userId)") debugInfoSync("🔐 添加认证 header: pub_uid = \(userId)")
#endif
} }
if let userTicket = await UserInfoManager.getCurrentUserTicket() { if let userTicket = await UserInfoManager.getCurrentUserTicket() {
headers["pub_ticket"] = userTicket headers["pub_ticket"] = userTicket
#if DEBUG
debugInfoSync("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...") debugInfoSync("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...")
#endif
} }
} else { } else {
#if DEBUG
debugInfoSync("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)") debugInfoSync("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)")
#endif
} }
return headers return headers
} }

View File

@@ -188,11 +188,11 @@ struct LoginHelper {
return nil return nil
} }
await debugInfoSync("🔐 DES加密成功") debugInfoSync("🔐 DES加密成功")
await debugInfoSync(" 原始ID: \(userID)") debugInfoSync(" 原始ID: \(userID)")
await debugInfoSync(" 加密后ID: \(encryptedID)") debugInfoSync(" 加密后ID: \(encryptedID)")
await debugInfoSync(" 原始密码: \(password)") debugInfoSync(" 原始密码: \(password)")
await debugInfoSync(" 加密后密码: \(encryptedPassword)") debugInfoSync(" 加密后密码: \(encryptedPassword)")
return IDLoginAPIRequest( return IDLoginAPIRequest(
phone: userID, phone: userID,
@@ -405,10 +405,10 @@ extension LoginHelper {
return nil return nil
} }
await debugInfoSync("🔐 邮箱验证码登录DES加密成功") debugInfoSync("🔐 邮箱验证码登录DES加密成功")
await debugInfoSync(" 原始邮箱: \(email)") debugInfoSync(" 原始邮箱: \(email)")
await debugInfoSync(" 加密邮箱: \(encryptedEmail)") debugInfoSync(" 加密邮箱: \(encryptedEmail)")
await debugInfoSync(" 验证码: \(code)") debugInfoSync(" 验证码: \(code)")
return EmailLoginRequest(email: encryptedEmail, code: code) return EmailLoginRequest(email: encryptedEmail, code: code)
} }

View File

@@ -11,11 +11,20 @@ struct EMailLoginFeature {
var isCodeLoading: Bool = false var isCodeLoading: Bool = false
var errorMessage: String? = nil var errorMessage: String? = nil
var isCodeSent: Bool = false var isCodeSent: Bool = false
//
var loginStep: LoginStep = .initial
enum LoginStep: Equatable {
case initial
case authenticating
case completed
case failed
}
#if DEBUG #if DEBUG
init() { init() {
self.email = "exzero@126.com" self.email = "exzero@126.com"
self.verificationCode = "" self.verificationCode = ""
self.loginStep = .initial
} }
#endif #endif
} }
@@ -109,6 +118,7 @@ struct EMailLoginFeature {
state.isLoading = true state.isLoading = true
state.errorMessage = nil state.errorMessage = nil
state.loginStep = .authenticating
return .run { send in return .run { send in
do { do {
@@ -149,14 +159,16 @@ struct EMailLoginFeature {
case .loginResponse(.success(let accountModel)): case .loginResponse(.success(let accountModel)):
state.isLoading = false state.isLoading = false
state.loginStep = .completed
// Effect AccountModel // Effect AccountModel
return .run { _ in return .run { _ in
await UserInfoManager.saveAccountModel(accountModel) await UserInfoManager.saveAccountModel(accountModel)
NotificationCenter.default.post(name: .ticketSuccess, object: nil) // NotificationCenter.default.post(name: .ticketSuccess, object: nil)
} }
case .loginResponse(.failure(let error)): case .loginResponse(.failure(let error)):
state.isLoading = false state.isLoading = false
state.loginStep = .failed
if let apiError = error as? APIError { if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription state.errorMessage = apiError.localizedDescription
} else { } else {
@@ -174,6 +186,7 @@ struct EMailLoginFeature {
state.isCodeLoading = false state.isCodeLoading = false
state.errorMessage = nil state.errorMessage = nil
state.isCodeSent = false state.isCodeSent = false
state.loginStep = .initial
return .none return .none
} }
} }

View File

@@ -42,8 +42,7 @@ struct FeedFeature {
case .onAppear: case .onAppear:
#if DEBUG #if DEBUG
return .none return .none
#endif #endif
// //
guard !state.isInitialized else { return .none } guard !state.isInitialized else { return .none }
state.isInitialized = true state.isInitialized = true
@@ -168,7 +167,7 @@ struct FeedFeature {
} }
} }
// reducer // reducer
ifLet(\State.createFeedState, action: /Action.createFeed) { self.ifLet(\.createFeedState, action: \.createFeed) {
CreateFeedFeature() CreateFeedFeature()
} }
} }

View File

@@ -16,6 +16,9 @@ struct HomeFeature {
// Feed // Feed
var feedState = FeedFeature.State() var feedState = FeedFeature.State()
//
var isLoggedOut = false
} }
enum Action { enum Action {
@@ -33,6 +36,9 @@ struct HomeFeature {
// Feed actions // Feed actions
case feed(FeedFeature.Action) case feed(FeedFeature.Action)
//
case logoutCompleted
} }
var body: some ReducerOf<Self> { var body: some ReducerOf<Self> {
@@ -48,6 +54,9 @@ struct HomeFeature {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case .onAppear: case .onAppear:
#if DEBUG
return .none
#endif
state.isInitialized = true state.isInitialized = true
return .concatenate( return .concatenate(
.send(.loadUserInfo), .send(.loadUserInfo),
@@ -80,12 +89,16 @@ struct HomeFeature {
return .send(.logout) return .send(.logout)
case .logout: case .logout:
// //
return .run { _ in return .run { send in
await UserInfoManager.clearAllAuthenticationData() await UserInfoManager.clearAllAuthenticationData()
NotificationCenter.default.post(name: .homeLogout, object: nil) await send(.logoutCompleted)
} }
case .logoutCompleted:
state.isLoggedOut = true
return .none
case .settingDismissed: case .settingDismissed:
state.isSettingPresented = false state.isSettingPresented = false
return .none return .none
@@ -101,7 +114,7 @@ struct HomeFeature {
} }
} }
// MARK: - Notification Extension // 使
extension Notification.Name { // extension Notification.Name {
static let homeLogout = Notification.Name("homeLogout") // static let homeLogout = Notification.Name("homeLogout")
} // }

View File

@@ -11,6 +11,8 @@ struct LoginFeature {
var error: String? var error: String?
var idLoginState = IDLoginFeature.State() var idLoginState = IDLoginFeature.State()
var emailLoginState = EMailLoginFeature.State() // var emailLoginState = EMailLoginFeature.State() //
// HomeFeature
var homeState = HomeFeature.State()
// Account Model Ticket // Account Model Ticket
var accountModel: AccountModel? var accountModel: AccountModel?
@@ -18,6 +20,11 @@ struct LoginFeature {
var ticketError: String? var ticketError: String?
var loginStep: LoginStep = .initial var loginStep: LoginStep = .initial
// true
var isAnyLoginCompleted: Bool {
idLoginState.loginStep == .completed || emailLoginState.loginStep == .completed
}
enum LoginStep: Equatable { enum LoginStep: Equatable {
case initial // case initial //
case authenticating // OAuth case authenticating // OAuth
@@ -42,7 +49,8 @@ struct LoginFeature {
case loginResponse(TaskResult<IDLoginResponse>) case loginResponse(TaskResult<IDLoginResponse>)
case idLogin(IDLoginFeature.Action) case idLogin(IDLoginFeature.Action)
case emailLogin(EMailLoginFeature.Action) // action case emailLogin(EMailLoginFeature.Action) // action
// HomeFeature action
case home(HomeFeature.Action)
// Ticket actions // Ticket actions
case requestTicket(accessToken: String) case requestTicket(accessToken: String)
case ticketResponse(TaskResult<TicketResponse>) case ticketResponse(TaskResult<TicketResponse>)
@@ -60,6 +68,10 @@ struct LoginFeature {
Scope(state: \.emailLoginState, action: \.emailLogin) { Scope(state: \.emailLoginState, action: \.emailLogin) {
EMailLoginFeature() EMailLoginFeature()
} }
// HomeFeature
Scope(state: \.homeState, action: \.home) {
HomeFeature()
}
Reduce { state, action in Reduce { state, action in
switch action { switch action {
@@ -165,7 +177,6 @@ struct LoginFeature {
// Effect AccountModel // Effect AccountModel
return .run { _ in return .run { _ in
await UserInfoManager.saveAccountModel(newAccountModel) await UserInfoManager.saveAccountModel(newAccountModel)
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
} }
} else { } else {
debugErrorSync("❌ AccountModel 不存在,无法保存 ticket") debugErrorSync("❌ AccountModel 不存在,无法保存 ticket")
@@ -211,7 +222,14 @@ struct LoginFeature {
case .emailLogin: case .emailLogin:
// EmailLoginfeature // EmailLoginfeature
return .none return .none
case .home(_):
return .none
} }
} }
} }
} }
// 使
// extension Notification.Name {
// static let ticketSuccess = Notification.Name("ticketSuccess")
// }

View File

@@ -58,19 +58,18 @@ struct SettingFeature {
state.isLoading = true state.isLoading = true
return .run { _ in return .run { _ in
await UserInfoManager.clearAllAuthenticationData() await UserInfoManager.clearAllAuthenticationData()
NotificationCenter.default.post(name: .homeLogout, object: nil)
} }
case .dismissTapped: case .dismissTapped:
return .run { _ in // NotificationCenter.default.post(name: .settingsDismiss, object: nil)
NotificationCenter.default.post(name: .settingsDismiss, object: nil) // action
} return .none
} }
} }
} }
} }
// MARK: - Notification Extension // 使
extension Notification.Name { // extension Notification.Name {
static let settingsDismiss = Notification.Name("settingsDismiss") // static let settingsDismiss = Notification.Name("settingsDismiss")
} // }

View File

@@ -24,7 +24,7 @@ struct SplashFeature {
case .onAppear: case .onAppear:
state.isLoading = true state.isLoading = true
state.shouldShowMainApp = false state.shouldShowMainApp = false
state.authenticationStatus = .notFound state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound
state.isCheckingAuthentication = false state.isCheckingAuthentication = false
// 1 (iOS 15.5+ ) // 1 (iOS 15.5+ )
@@ -51,7 +51,6 @@ struct SplashFeature {
case let .authenticationChecked(status): case let .authenticationChecked(status):
#if DEBUG #if DEBUG
debugInfoSync("🔑 需要手动登录") debugInfoSync("🔑 需要手动登录")
NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
return .none return .none
#endif #endif
state.isCheckingAuthentication = false state.isCheckingAuthentication = false
@@ -60,10 +59,8 @@ struct SplashFeature {
// //
if status.canAutoLogin { if status.canAutoLogin {
debugInfoSync("🎉 自动登录成功,进入主页") debugInfoSync("🎉 自动登录成功,进入主页")
NotificationCenter.default.post(name: .autoLoginSuccess, object: nil)
} else { } else {
debugInfoSync("🔑 需要手动登录") debugInfoSync("🔑 需要手动登录")
NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
} }
return .none return .none

View File

@@ -2,83 +2,35 @@ import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct AppRootView: View { struct AppRootView: View {
@State private var shouldShowMainApp = false @State private var isLoggedIn = false
@State private var shouldShowHomePage = false
let splashStore = Store(
initialState: SplashFeature.State()
) {
SplashFeature()
}
let loginStore = Store(
initialState: LoginFeature.State()
) {
LoginFeature()
}
let homeStore = Store(
initialState: HomeFeature.State()
) {
HomeFeature()
}
var body: some View { var body: some View {
ZStack { if isLoggedIn {
Group { HomeView(
if shouldShowHomePage { store: Store(
// initialState: HomeFeature.State()
HomeView(store: homeStore) ) {
.transition(.opacity.animation(.easeInOut(duration: 0.5))) HomeFeature()
} else if shouldShowMainApp { },
// onLogout: {
LoginView(store: loginStore) isLoggedIn = false
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
} else {
//
SplashView(store: splashStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
} }
} )
.onReceive(NotificationCenter.default.publisher(for: .autoLoginSuccess)) { _ in } else {
// LoginView(
withAnimation(.easeInOut(duration: 0.5)) { store: Store(
shouldShowHomePage = true initialState: LoginFeature.State()
) {
LoginFeature()
},
onLoginSuccess: {
isLoggedIn = true
} }
} )
.onReceive(NotificationCenter.default.publisher(for: .autoLoginFailed)) { _ in
//
withAnimation(.easeInOut(duration: 0.5)) {
shouldShowMainApp = true
}
}
.onReceive(NotificationCenter.default.publisher(for: .ticketSuccess)) { _ in
//
withAnimation(.easeInOut(duration: 0.5)) {
shouldShowHomePage = true
}
}
.onReceive(NotificationCenter.default.publisher(for: .homeLogout)) { _ in
//
withAnimation(.easeInOut(duration: 0.5)) {
shouldShowHomePage = false
shouldShowMainApp = true
}
}
// API Loading -
APILoadingEffectView()
} }
} }
} }
extension Notification.Name {
static let splashFinished = Notification.Name("splashFinished")
static let ticketSuccess = Notification.Name("ticketSuccess")
static let autoLoginSuccess = Notification.Name("autoLoginSuccess")
static let autoLoginFailed = Notification.Name("autoLoginFailed")
}
#Preview { #Preview {
AppRootView() AppRootView()
} }

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
import Combine
struct EMailLoginView: View { struct EMailLoginView: View {
let store: StoreOf<EMailLoginFeature> let store: StoreOf<EMailLoginFeature>
@@ -9,7 +10,7 @@ struct EMailLoginView: View {
@State private var email: String = "" @State private var email: String = ""
@State private var verificationCode: String = "" @State private var verificationCode: String = ""
@State private var codeCountdown: Int = 0 @State private var codeCountdown: Int = 0
@State private var timer: Timer? @State private var timerCancellable: AnyCancellable?
// //
@FocusState private var focusedField: Field? @FocusState private var focusedField: Field?
@@ -41,8 +42,10 @@ struct EMailLoginView: View {
} }
var body: some View { var body: some View {
GeometryReader { geometry in // WithViewStore(store, observe: { $0 }) { _ in
ZStack { GeometryReader { geometry in
WithPerceptionTracking {
ZStack {
// //
Image("bg") Image("bg")
.resizable() .resizable()
@@ -200,36 +203,48 @@ struct EMailLoginView: View {
Spacer() Spacer()
} }
}
} }
} }
// }
.onAppear { .onAppear {
// let _ = WithPerceptionTracking {
store.send(.resetState) //
store.send(.resetState)
email = ""
verificationCode = "" email = ""
codeCountdown = 0 verificationCode = ""
stopCountdown() codeCountdown = 0
stopCountdown()
#if DEBUG
email = "exzero@126.com" #if DEBUG
store.send(.emailChanged(email)) email = "exzero@126.com"
#endif store.send(.emailChanged(email))
#endif
}
} }
.onDisappear { .onDisappear {
stopCountdown() let _ = WithPerceptionTracking {
stopCountdown()
}
} }
.onChange(of: email) { newEmail in .onChange(of: email) { newEmail in
store.send(.emailChanged(newEmail)) let _ = WithPerceptionTracking {
store.send(.emailChanged(newEmail))
}
} }
.onChange(of: verificationCode) { newCode in .onChange(of: verificationCode) { newCode in
store.send(.verificationCodeChanged(newCode)) let _ = WithPerceptionTracking {
store.send(.verificationCodeChanged(newCode))
}
} }
.onChange(of: store.isCodeLoading) { isCodeLoading in .onChange(of: store.isCodeLoading) { isCodeLoading in
// API let _ = WithPerceptionTracking {
if !isCodeLoading && store.errorMessage == nil { // API
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if !isCodeLoading && store.errorMessage == nil {
focusedField = .verificationCode DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
focusedField = .verificationCode
}
} }
} }
} }
@@ -242,30 +257,31 @@ struct EMailLoginView: View {
// //
codeCountdown = 60 codeCountdown = 60
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in // 使 SwiftUI Timer.publish
DispatchQueue.main.async { timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { _ in
if codeCountdown > 0 { if codeCountdown > 0 {
codeCountdown -= 1 codeCountdown -= 1
} else { } else {
stopCountdown() stopCountdown()
} }
} }
}
} }
private func stopCountdown() { private func stopCountdown() {
timer?.invalidate() timerCancellable?.cancel()
timer = nil timerCancellable = nil
} }
} }
#Preview { //#Preview {
EMailLoginView( // EMailLoginView(
store: Store( // store: Store(
initialState: EMailLoginFeature.State() // initialState: EMailLoginFeature.State()
) { // ) {
EMailLoginFeature() // EMailLoginFeature()
}, // },
onBack: {} // onBack: {}
) // )
} //}

View File

@@ -96,7 +96,7 @@ struct FeedView: View {
store.send(.loadLatestMoments) store.send(.loadLatestMoments)
} }
.onAppear { .onAppear {
store.send(.onAppear) // store.send(.onAppear)
} }
.sheet(isPresented: .init( .sheet(isPresented: .init(
get: { store.isShowingCreateFeed }, get: { store.isShowingCreateFeed },

View File

@@ -3,6 +3,7 @@ import ComposableArchitecture
struct HomeView: View { struct HomeView: View {
let store: StoreOf<HomeFeature> let store: StoreOf<HomeFeature>
let onLogout: () -> Void //
@ObservedObject private var localizationManager = LocalizationManager.shared @ObservedObject private var localizationManager = LocalizationManager.shared
@State private var selectedTab: Tab = .feed @State private var selectedTab: Tab = .feed
@@ -27,7 +28,7 @@ struct HomeView: View {
) )
.transition(.opacity) .transition(.opacity)
case .me: case .me:
MeView() MeView(onLogout: onLogout)
.transition(.opacity) .transition(.opacity)
} }
} }
@@ -60,6 +61,6 @@ struct HomeView: View {
initialState: HomeFeature.State() initialState: HomeFeature.State()
) { ) {
HomeFeature() HomeFeature()
} }, onLogout: {}
) )
} }

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
import Perception
struct IDLoginView: View { struct IDLoginView: View {
let store: StoreOf<IDLoginFeature> let store: StoreOf<IDLoginFeature>
@@ -9,6 +10,8 @@ struct IDLoginView: View {
@State private var userID: String = "" @State private var userID: String = ""
@State private var password: String = "" @State private var password: String = ""
@State private var isPasswordVisible: Bool = false @State private var isPasswordVisible: Bool = false
// - LoginView
@State private var showRecoverPassword: Bool = false @State private var showRecoverPassword: Bool = false
// //
@@ -18,6 +21,7 @@ struct IDLoginView: View {
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
WithPerceptionTracking {
ZStack { ZStack {
// - 使"bg" // - 使"bg"
Image("bg") Image("bg")
@@ -178,48 +182,49 @@ struct IDLoginView: View {
Spacer() Spacer()
} }
}
// NavigationLink - }
NavigationLink( }
destination: RecoverPasswordView( .navigationBarHidden(true)
store: Store( // 使 LoginView navigationDestination
initialState: RecoverPasswordFeature.State() .navigationDestination(isPresented: $showRecoverPassword) {
) { WithPerceptionTracking {
RecoverPasswordFeature() RecoverPasswordView(
}, store: Store(
onBack: { initialState: RecoverPasswordFeature.State()
showRecoverPassword = false ) {
} RecoverPasswordFeature()
) },
.navigationBarHidden(true), onBack: {
isActive: $showRecoverPassword showRecoverPassword = false
) { }
EmptyView() )
} .navigationBarHidden(true)
.hidden()
} }
} }
.onAppear { .onAppear {
// TCA let _ = WithPerceptionTracking {
userID = store.userID // TCA
password = store.password userID = store.userID
isPasswordVisible = store.isPasswordVisible password = store.password
isPasswordVisible = store.isPasswordVisible
#if DEBUG
// #if DEBUG
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据") //
#endif debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
#endif
}
} }
} }
} }
#Preview { //#Preview {
IDLoginView( // IDLoginView(
store: Store( // store: Store(
initialState: IDLoginFeature.State() // initialState: IDLoginFeature.State()
) { // ) {
IDLoginFeature() // IDLoginFeature()
}, // },
onBack: {} // onBack: {}
) // )
} //}

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
import Perception
// PreferenceKey // PreferenceKey
struct ImageHeightPreferenceKey: PreferenceKey { struct ImageHeightPreferenceKey: PreferenceKey {
@@ -11,8 +12,9 @@ struct ImageHeightPreferenceKey: PreferenceKey {
struct LoginView: View { struct LoginView: View {
let store: StoreOf<LoginFeature> let store: StoreOf<LoginFeature>
let onLoginSuccess: () -> Void //
@State private var topImageHeight: CGFloat = 120 // @State private var topImageHeight: CGFloat = 120 //
@ObservedObject private var localizationManager = LocalizationManager.shared // @ObservedObject private var localizationManager = LocalizationManager.shared
@State private var showLanguageSettings = false @State private var showLanguageSettings = false
@State private var isAgreedToTerms = true @State private var isAgreedToTerms = true
@State private var showUserAgreement = false @State private var showUserAgreement = false
@@ -21,158 +23,169 @@ struct LoginView: View {
@State private var showEmailLogin = false // @State private var showEmailLogin = false //
var body: some View { var body: some View {
NavigationStack { WithViewStore(self.store, observe: { $0.isAnyLoginCompleted }) { viewStore in
GeometryReader { geometry in NavigationStack {
ZStack { GeometryReader { geometry in
// 使 splash WithPerceptionTracking {
Image("bg") ZStack {
.resizable() // 使 splash
.aspectRatio(contentMode: .fill) Image("bg")
.ignoresSafeArea(.all) .resizable()
VStack(spacing: 0) { .aspectRatio(contentMode: .fill)
// "top" .ignoresSafeArea(.all)
ZStack { VStack(spacing: 0) {
Image("top") // "top"
.resizable() ZStack {
.aspectRatio(contentMode: .fit) Image("top")
.frame(maxWidth: .infinity) .resizable()
.padding(.top, -100) .aspectRatio(contentMode: .fit)
.background( .frame(maxWidth: .infinity)
GeometryReader { topImageGeometry in .padding(.top, -100)
Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height) .background(
} GeometryReader { topImageGeometry in
) Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height)
// E-PARTI "top"20 }
HStack { )
Text(NSLocalizedString("login.app_title", comment: "")) // E-PARTI "top"20
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white)
.padding(.leading, 20)
Spacer()
}
.padding(.top, max(0, topImageHeight - 100)) // top - 140
// - Debug
#if DEBUG
VStack {
HStack { HStack {
Text(NSLocalizedString("login.app_title", comment: ""))
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white)
.padding(.leading, 20)
Spacer() Spacer()
Button(action: { }
showLanguageSettings = true .padding(.top, max(0, topImageHeight - 100)) // top - 140
}) {
Image(systemName: "globe") // - Debug
.frame(width: 40, height: 40) #if DEBUG
.font(.system(size: 20)) VStack {
.foregroundColor(.white) HStack {
.background(Color.black.opacity(0.3)) Spacer()
.clipShape(Circle()) Button(action: {
showLanguageSettings = true
}) {
Image(systemName: "globe")
.frame(width: 40, height: 40)
.font(.system(size: 20))
.foregroundColor(.white)
.background(Color.black.opacity(0.3))
.clipShape(Circle())
}
.padding(.trailing, 16)
} }
.padding(.trailing, 16) Spacer()
} }
Spacer() #endif
VStack(spacing: 24) {
// ID Login
LoginButton(
iconName: "person.circle.fill",
iconColor: .green,
title: NSLocalizedString("login.id_login", comment: "")
) {
showIDLogin = true // SwiftUI
}
// Email Login
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: NSLocalizedString("login.email_login", comment: "")
) {
showEmailLogin = true //
}
}.padding(.top, max(0, topImageHeight+140))
}
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
topImageHeight = imageHeight
} }
#endif
VStack(spacing: 24) {
// ID Login
LoginButton(
iconName: "person.circle.fill",
iconColor: .green,
title: NSLocalizedString("login.id_login", comment: "")
) {
showIDLogin = true // SwiftUI
}
// Email Login
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: NSLocalizedString("login.email_login", comment: "")
) {
showEmailLogin = true //
}
}.padding(.top, max(0, topImageHeight+140))
}
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
topImageHeight = imageHeight
}
// 使"top"40pt // 使"top"40pt
Spacer() Spacer()
.frame(height: 120) .frame(height: 120)
//
UserAgreementView(
isAgreed: $isAgreedToTerms,
onUserServiceTapped: {
showUserAgreement = true
},
onPrivacyPolicyTapped: {
showPrivacyPolicy = true
}
)
.padding(.horizontal, 28)
.padding(.bottom, 140)
}
// // NavigationLink navigationDestination
UserAgreementView(
isAgreed: $isAgreedToTerms,
onUserServiceTapped: {
showUserAgreement = true
},
onPrivacyPolicyTapped: {
showPrivacyPolicy = true
}
)
.padding(.horizontal, 28)
.padding(.bottom, 140)
} }
}
// NavigationLink - 使SwiftUI }
NavigationLink( .navigationBarHidden(true)
destination: IDLoginView( // iOS 16 navigationDestination
.navigationDestination(isPresented: $showIDLogin) {
WithPerceptionTracking {
IDLoginView(
store: store.scope( store: store.scope(
state: \.idLoginState, state: \.idLoginState,
action: \.idLogin action: \.idLogin
), ),
onBack: { onBack: {
showIDLogin = false // SwiftUI showIDLogin = false
} }
) )
.navigationBarHidden(true), .navigationBarHidden(true)
isActive: $showIDLogin // 使SwiftUI
) {
EmptyView()
} }
.hidden() }
.navigationDestination(isPresented: $showEmailLogin) {
// NavigationLink WithPerceptionTracking {
NavigationLink( EMailLoginView(
destination: EMailLoginView(
store: store.scope( store: store.scope(
state: \.emailLoginState, state: \.emailLoginState,
action: \.emailLogin action: \.emailLogin
), ),
onBack: { onBack: {
showEmailLogin = false // SwiftUI showEmailLogin = false
} }
) )
.navigationBarHidden(true), .navigationBarHidden(true)
isActive: $showEmailLogin // 使SwiftUI }
) { }
EmptyView() // HomeView navigationDestination
}
.sheet(isPresented: $showLanguageSettings) {
WithPerceptionTracking {
LanguageSettingsView(isPresented: $showLanguageSettings)
}
}
.webView(
isPresented: $showUserAgreement,
url: APIConfiguration.webURL(for: .userAgreement)
)
.webView(
isPresented: $showPrivacyPolicy,
url: APIConfiguration.webURL(for: .privacyPolicy)
)
//
.onChange(of: viewStore.state) { completed in
WithPerceptionTracking {
if completed {
onLoginSuccess()
} }
.hidden()
} }
} }
.navigationBarHidden(true)
} }
.sheet(isPresented: $showLanguageSettings) {
LanguageSettingsView(isPresented: $showLanguageSettings)
}
.webView(
isPresented: $showUserAgreement,
url: APIConfiguration.webURL(for: .userAgreement)
)
.webView(
isPresented: $showPrivacyPolicy,
url: APIConfiguration.webURL(for: .privacyPolicy)
)
} }
} }
#Preview { //#Preview {
LoginView( // LoginView(
store: Store( // store: Store(
initialState: LoginFeature.State() // initialState: LoginFeature.State()
) { // ) {
LoginFeature() // LoginFeature()
} // },
) // onLoginSuccess: {}
} // )
//}

View File

@@ -2,6 +2,8 @@ import SwiftUI
struct MeView: View { struct MeView: View {
@State private var showLogoutConfirmation = false @State private var showLogoutConfirmation = false
let onLogout: () -> Void //
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
ScrollView { ScrollView {
@@ -90,8 +92,8 @@ struct MeView: View {
debugInfoSync("🔓 开始执行退出登录...") debugInfoSync("🔓 开始执行退出登录...")
// keychain // keychain
await UserInfoManager.clearAllAuthenticationData() await UserInfoManager.clearAllAuthenticationData()
// window root login view //
NotificationCenter.default.post(name: .homeLogout, object: nil) onLogout()
debugInfoSync("✅ 退出登录完成") debugInfoSync("✅ 退出登录完成")
} }
} }
@@ -131,5 +133,5 @@ struct MenuItemView: View {
} }
#Preview { #Preview {
MeView() MeView(onLogout: {})
} }

View File

@@ -14,16 +14,41 @@ struct RecoverPasswordView: View {
// //
@State private var countdown: Int = 0 @State private var countdown: Int = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @State private var timerCancellable: AnyCancellable?
//
private var isEmailValid: Bool {
!email.isEmpty
}
private var isVerificationCodeValid: Bool {
!verificationCode.isEmpty
}
private var isNewPasswordValid: Bool {
!newPassword.isEmpty
}
private var isStoreNotLoading: Bool {
!store.isResetLoading
}
private var isCodeNotLoading: Bool {
!store.isCodeLoading
}
private var isCountdownFinished: Bool {
countdown == 0
}
// //
private var isConfirmButtonEnabled: Bool { private var isConfirmButtonEnabled: Bool {
return !store.isResetLoading && !email.isEmpty && !verificationCode.isEmpty && !newPassword.isEmpty isStoreNotLoading && isEmailValid && isVerificationCodeValid && isNewPasswordValid
} }
// //
private var isGetCodeButtonEnabled: Bool { private var isGetCodeButtonEnabled: Bool {
return !store.isCodeLoading && !email.isEmpty && countdown == 0 isCodeNotLoading && isEmailValid && isCountdownFinished
} }
// //
@@ -75,115 +100,13 @@ struct RecoverPasswordView: View {
// //
VStack(spacing: 24) { VStack(spacing: 24) {
// //
ZStack { emailInputField
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
TextField("", text: $email)
.placeholder(when: email.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.autocapitalization(.none)
}
// //
ZStack { verificationCodeInputField
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
//
Button(action: {
//
startCountdown()
// API
store.send(.getVerificationCodeTapped)
}) {
ZStack {
if store.isCodeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.7)
} else {
Text(getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
}
}
.frame(width: 60, height: 36)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(Color.white.opacity(isGetCodeButtonEnabled ? 0.2 : 0.1))
)
}
.disabled(!isGetCodeButtonEnabled || store.isCodeLoading)
}
.padding(.horizontal, 24)
}
// //
ZStack { newPasswordInputField
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
if isNewPasswordVisible {
TextField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
} else {
SecureField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
}
Button(action: {
isNewPasswordVisible.toggle()
}) {
Image(systemName: isNewPasswordVisible ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
.font(.system(size: 18))
}
}
.padding(.horizontal, 24)
}
} }
.padding(.horizontal, 32) .padding(.horizontal, 32)
@@ -191,37 +114,7 @@ struct RecoverPasswordView: View {
.frame(height: 80) .frame(height: 80)
// //
Button(action: { confirmButton
store.send(.resetPasswordTapped)
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isResetLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isResetLoading ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(!isConfirmButtonEnabled)
.opacity(isConfirmButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
// //
if let errorMessage = store.errorMessage { if let errorMessage = store.errorMessage {
@@ -237,21 +130,10 @@ struct RecoverPasswordView: View {
} }
} }
.onAppear { .onAppear {
// resetState()
store.send(.resetState)
email = ""
verificationCode = ""
newPassword = ""
isNewPasswordVisible = false
countdown = 0
#if DEBUG
email = "exzero@126.com"
store.send(.emailChanged(email))
#endif
} }
.onDisappear { .onDisappear {
countdown = 0 stopCountdown()
} }
.onChange(of: email) { newEmail in .onChange(of: email) { newEmail in
store.send(.emailChanged(newEmail)) store.send(.emailChanged(newEmail))
@@ -262,43 +144,207 @@ struct RecoverPasswordView: View {
.onChange(of: newPassword) { newPassword in .onChange(of: newPassword) { newPassword in
store.send(.newPasswordChanged(newPassword)) store.send(.newPasswordChanged(newPassword))
} }
.onChange(of: store.isCodeLoading) { isCodeLoading in
// API
if !isCodeLoading && store.errorMessage == nil {
//
}
}
.onChange(of: store.isResetSuccess) { isResetSuccess in .onChange(of: store.isResetSuccess) { isResetSuccess in
//
if isResetSuccess { if isResetSuccess {
onBack() onBack()
} }
} }
.onReceive(timer) { _ in }
if countdown > 0 {
countdown -= 1 // MARK: - UI Components
}
private var emailInputField: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
TextField("", text: $email)
.placeholder(when: email.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.autocapitalization(.none)
} }
} }
private var verificationCodeInputField: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
//
Button(action: {
startCountdown()
store.send(.getVerificationCodeTapped)
}) {
ZStack {
if store.isCodeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.7)
} else {
Text(getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
}
}
.frame(width: 60, height: 36)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(Color.white.opacity(isGetCodeButtonEnabled ? 0.2 : 0.1))
)
}
.disabled(!isGetCodeButtonEnabled || store.isCodeLoading)
}
.padding(.horizontal, 24)
}
}
private var newPasswordInputField: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
if isNewPasswordVisible {
TextField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
} else {
SecureField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
}
Button(action: {
isNewPasswordVisible.toggle()
}) {
Image(systemName: isNewPasswordVisible ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
.font(.system(size: 18))
}
}
.padding(.horizontal, 24)
}
}
private var confirmButton: some View {
Button(action: {
store.send(.resetPasswordTapped)
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isResetLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isResetLoading ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(!isConfirmButtonEnabled)
.opacity(isConfirmButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
}
// MARK: - Private Methods // MARK: - Private Methods
private func resetState() {
store.send(.resetState)
email = ""
verificationCode = ""
newPassword = ""
isNewPasswordVisible = false
countdown = 0
#if DEBUG
email = "exzero@126.com"
store.send(.emailChanged(email))
#endif
}
private func startCountdown() { private func startCountdown() {
stopCountdown()
countdown = 60 countdown = 60
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { _ in
if countdown > 0 {
countdown -= 1
} else {
stopCountdown()
}
}
} }
private func stopCountdown() { private func stopCountdown() {
timerCancellable?.cancel()
timerCancellable = nil
countdown = 0 countdown = 0
} }
} }
#Preview { //#Preview {
RecoverPasswordView( // RecoverPasswordView(
store: Store( // store: Store(
initialState: RecoverPasswordFeature.State() // initialState: RecoverPasswordFeature.State()
) { // ) {
RecoverPasswordFeature() // RecoverPasswordFeature()
}, // },
onBack: {} // onBack: {}
) // )
} //}