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
Doc
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_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = Z7UCRF23F3;
ENABLE_PREVIEWS = YES;
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
if let userId = await UserInfoManager.getCurrentUserId() {
headers["pub_uid"] = userId
#if DEBUG
debugInfoSync("🔐 添加认证 header: pub_uid = \(userId)")
#endif
}
if let userTicket = await UserInfoManager.getCurrentUserTicket() {
headers["pub_ticket"] = userTicket
#if DEBUG
debugInfoSync("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...")
#endif
}
} else {
#if DEBUG
debugInfoSync("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)")
#endif
}
return headers
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,83 +2,35 @@ import SwiftUI
import ComposableArchitecture
struct AppRootView: View {
@State private var shouldShowMainApp = 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()
}
@State private var isLoggedIn = false
var body: some View {
ZStack {
Group {
if shouldShowHomePage {
//
HomeView(store: homeStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
} else if shouldShowMainApp {
//
LoginView(store: loginStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
} else {
//
SplashView(store: splashStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
if isLoggedIn {
HomeView(
store: Store(
initialState: HomeFeature.State()
) {
HomeFeature()
},
onLogout: {
isLoggedIn = false
}
}
.onReceive(NotificationCenter.default.publisher(for: .autoLoginSuccess)) { _ in
//
withAnimation(.easeInOut(duration: 0.5)) {
shouldShowHomePage = true
)
} else {
LoginView(
store: Store(
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 {
AppRootView()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import SwiftUI
import ComposableArchitecture
import Perception
// PreferenceKey
struct ImageHeightPreferenceKey: PreferenceKey {
@@ -11,8 +12,9 @@ struct ImageHeightPreferenceKey: PreferenceKey {
struct LoginView: View {
let store: StoreOf<LoginFeature>
let onLoginSuccess: () -> Void //
@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 isAgreedToTerms = true
@State private var showUserAgreement = false
@@ -21,158 +23,169 @@ struct LoginView: View {
@State private var showEmailLogin = false //
var body: some View {
NavigationStack {
GeometryReader { geometry in
ZStack {
// 使 splash
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
// "top"
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)
}
)
// E-PARTI "top"20
HStack {
Text(NSLocalizedString("login.app_title", comment: ""))
.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 {
WithViewStore(self.store, observe: { $0.isAnyLoginCompleted }) { viewStore in
NavigationStack {
GeometryReader { geometry in
WithPerceptionTracking {
ZStack {
// 使 splash
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
// "top"
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)
}
)
// E-PARTI "top"20
HStack {
Text(NSLocalizedString("login.app_title", comment: ""))
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white)
.padding(.leading, 20)
Spacer()
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(.top, max(0, topImageHeight - 100)) // top - 140
// - Debug
#if DEBUG
VStack {
HStack {
Spacer()
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
Spacer()
.frame(height: 120)
// 使"top"40pt
Spacer()
.frame(height: 120)
//
UserAgreementView(
isAgreed: $isAgreedToTerms,
onUserServiceTapped: {
showUserAgreement = true
},
onPrivacyPolicyTapped: {
showPrivacyPolicy = true
}
)
.padding(.horizontal, 28)
.padding(.bottom, 140)
}
//
UserAgreementView(
isAgreed: $isAgreedToTerms,
onUserServiceTapped: {
showUserAgreement = true
},
onPrivacyPolicyTapped: {
showPrivacyPolicy = true
}
)
.padding(.horizontal, 28)
.padding(.bottom, 140)
// NavigationLink navigationDestination
}
// NavigationLink - 使SwiftUI
NavigationLink(
destination: IDLoginView(
}
}
.navigationBarHidden(true)
// iOS 16 navigationDestination
.navigationDestination(isPresented: $showIDLogin) {
WithPerceptionTracking {
IDLoginView(
store: store.scope(
state: \.idLoginState,
action: \.idLogin
),
onBack: {
showIDLogin = false // SwiftUI
showIDLogin = false
}
)
.navigationBarHidden(true),
isActive: $showIDLogin // 使SwiftUI
) {
EmptyView()
.navigationBarHidden(true)
}
.hidden()
// NavigationLink
NavigationLink(
destination: EMailLoginView(
}
.navigationDestination(isPresented: $showEmailLogin) {
WithPerceptionTracking {
EMailLoginView(
store: store.scope(
state: \.emailLoginState,
action: \.emailLogin
),
onBack: {
showEmailLogin = false // SwiftUI
showEmailLogin = false
}
)
.navigationBarHidden(true),
isActive: $showEmailLogin // 使SwiftUI
) {
EmptyView()
.navigationBarHidden(true)
}
}
// 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 {
LoginView(
store: Store(
initialState: LoginFeature.State()
) {
LoginFeature()
}
)
}
//#Preview {
// LoginView(
// store: Store(
// initialState: LoginFeature.State()
// ) {
// LoginFeature()
// },
// onLoginSuccess: {}
// )
//}

View File

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

View File

@@ -14,16 +14,41 @@ struct RecoverPasswordView: View {
//
@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 {
return !store.isResetLoading && !email.isEmpty && !verificationCode.isEmpty && !newPassword.isEmpty
isStoreNotLoading && isEmailValid && isVerificationCodeValid && isNewPasswordValid
}
//
private var isGetCodeButtonEnabled: Bool {
return !store.isCodeLoading && !email.isEmpty && countdown == 0
isCodeNotLoading && isEmailValid && isCountdownFinished
}
//
@@ -75,115 +100,13 @@ struct RecoverPasswordView: View {
//
VStack(spacing: 24) {
//
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)
}
emailInputField
//
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()
// 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)
}
verificationCodeInputField
//
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)
}
newPasswordInputField
}
.padding(.horizontal, 32)
@@ -191,37 +114,7 @@ struct RecoverPasswordView: View {
.frame(height: 80)
//
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)
confirmButton
//
if let errorMessage = store.errorMessage {
@@ -237,21 +130,10 @@ struct RecoverPasswordView: View {
}
}
.onAppear {
//
store.send(.resetState)
email = ""
verificationCode = ""
newPassword = ""
isNewPasswordVisible = false
countdown = 0
#if DEBUG
email = "exzero@126.com"
store.send(.emailChanged(email))
#endif
resetState()
}
.onDisappear {
countdown = 0
stopCountdown()
}
.onChange(of: email) { newEmail in
store.send(.emailChanged(newEmail))
@@ -262,43 +144,207 @@ struct RecoverPasswordView: View {
.onChange(of: newPassword) { newPassword in
store.send(.newPasswordChanged(newPassword))
}
.onChange(of: store.isCodeLoading) { isCodeLoading in
// API
if !isCodeLoading && store.errorMessage == nil {
//
}
}
.onChange(of: store.isResetSuccess) { isResetSuccess in
//
if isResetSuccess {
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
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() {
stopCountdown()
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() {
timerCancellable?.cancel()
timerCancellable = nil
countdown = 0
}
}
#Preview {
RecoverPasswordView(
store: Store(
initialState: RecoverPasswordFeature.State()
) {
RecoverPasswordFeature()
},
onBack: {}
)
}
//#Preview {
// RecoverPasswordView(
// store: Store(
// initialState: RecoverPasswordFeature.State()
// ) {
// RecoverPasswordFeature()
// },
// onBack: {}
// )
//}