feat: 更新依赖和项目配置,优化代码结构

- 在Package.swift中注释掉旧的swift-composable-architecture依赖,并添加swift-case-paths依赖。
- 在Podfile中将iOS平台版本更新至16.0,并移除QCloudCOSXML/Transfer依赖,改为使用QCloudCOSXML。
- 更新Podfile.lock以反映依赖变更,确保项目依赖的准确性。
- 新增架构分析需求文档,明确项目架构评估和改进建议。
- 在多个文件中实现async/await语法,提升异步操作的可读性和性能。
- 更新日志输出方法,确保在调试模式下提供一致的调试信息。
- 优化多个视图组件,提升用户体验和代码可维护性。
This commit is contained in:
edwinQQQ
2025-07-17 18:47:09 +08:00
parent 4bbb4f8434
commit 128bf36c88
46 changed files with 1250 additions and 1203 deletions

View File

@@ -0,0 +1,51 @@
# 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

@@ -15,7 +15,8 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.8.0"),
// .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.8.0"),
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", branch: "main")
],
targets: [
.target(
@@ -29,4 +30,4 @@ let package = Package(
dependencies: ["yana"]
),
]
)
)

View File

@@ -1,5 +1,5 @@
# Uncomment the next line to define a global platform for your project
platform :ios, '13.0'
platform :ios, '16.0'
target 'yana' do
# Comment the next line if you don't want to use dynamic frameworks
@@ -19,7 +19,7 @@ target 'yana' do
pod 'Alamofire'
# 腾讯云 COS 精简版 SDK
pod 'QCloudCOSXML/Transfer'
pod 'QCloudCOSXML'
end
post_install do |installer|

View File

@@ -1,24 +1,32 @@
PODS:
- Alamofire (5.10.2)
- QCloudCore/WithoutMTA (6.5.1)
- QCloudCOSXML/Transfer (6.5.1):
- QCloudCore/WithoutMTA (= 6.5.1)
- QCloudCore (6.5.1):
- QCloudCore/Default (= 6.5.1)
- QCloudCore/Default (6.5.1):
- QCloudTrack/Beacon (= 6.5.1)
- QCloudCOSXML (6.5.1):
- QCloudCOSXML/Default (= 6.5.1)
- QCloudCOSXML/Default (6.5.1):
- QCloudCore (= 6.5.1)
- QCloudTrack/Beacon (6.5.1)
DEPENDENCIES:
- Alamofire
- QCloudCOSXML/Transfer
- QCloudCOSXML
SPEC REPOS:
trunk:
- Alamofire
- QCloudCore
- QCloudCOSXML
- QCloudTrack
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
PODFILE CHECKSUM: cdb7b0983805213c0c4148aeaed5b1e5ea5345ab
PODFILE CHECKSUM: cd339c4c75faaa076936a34cac2be597c64f138a
COCOAPODS: 1.16.2

View File

@@ -10,6 +10,8 @@
4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */; };
4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */; };
4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 4C4C912F2DE864F000384527 /* ComposableArchitecture */; };
4CE9EFEA2E28FC3B0078D046 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE9EFE92E28FC3B0078D046 /* CasePaths */; };
4CE9EFEC2E28FC3B0078D046 /* CasePathsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE9EFEB2E28FC3B0078D046 /* CasePathsCore */; };
DF26704F4C1F2ABA0405A0C3 /* Pods_yana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9DBA38268C435BE06F39AE2 /* Pods_yana.framework */; };
/* End PBXBuildFile section */
@@ -65,7 +67,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4CE9EFEA2E28FC3B0078D046 /* CasePaths in Frameworks */,
4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */,
4CE9EFEC2E28FC3B0078D046 /* CasePathsCore in Frameworks */,
4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */,
4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */,
DF26704F4C1F2ABA0405A0C3 /* Pods_yana.framework in Frameworks */,
@@ -144,6 +148,7 @@
4C3E651C2DB61F7A00E5A455 /* Frameworks */,
4C3E651D2DB61F7A00E5A455 /* Resources */,
0C5E1A8360250314246001A5 /* [CP] Embed Pods Frameworks */,
0BECA6AFA61BE910372F6299 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -186,7 +191,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1630;
LastUpgradeCheck = 1630;
LastUpgradeCheck = 1640;
TargetAttributes = {
4C3E651E2DB61F7A00E5A455 = {
CreatedOnToolsVersion = 16.3;
@@ -209,6 +214,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */,
4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 4C3E65202DB61F7A00E5A455 /* Products */;
@@ -239,6 +245,27 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0BECA6AFA61BE910372F6299 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
showEnvVarsInLog = 0;
};
0C5E1A8360250314246001A5 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -315,6 +342,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -379,6 +407,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -467,7 +496,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -483,7 +512,8 @@
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h";
SWIFT_VERSION = 5.9;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
@@ -524,7 +554,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -540,7 +570,8 @@
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h";
SWIFT_VERSION = 5.9;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
@@ -632,6 +663,14 @@
minimumVersion = 1.20.2;
};
};
4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pointfreeco/swift-case-paths";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -640,6 +679,16 @@
package = 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */;
productName = ComposableArchitecture;
};
4CE9EFE92E28FC3B0078D046 /* CasePaths */ = {
isa = XCSwiftPackageProductDependency;
package = 4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */;
productName = CasePaths;
};
4CE9EFEB2E28FC3B0078D046 /* CasePathsCore */ = {
isa = XCSwiftPackageProductDependency;
package = 4CE9EFE82E28FC3B0078D046 /* XCRemoteSwiftPackageReference "swift-case-paths" */;
productName = CasePathsCore;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */;

View File

@@ -1,5 +1,5 @@
{
"originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8",
"originHash" : "411c4947a4ddec377a0aac37852b26ccdf5b2dd58cb99bedfb2d4c108efd60fd",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25",
"version" : "1.7.0"
"branch" : "main",
"revision" : "9810c8d6c2914de251e072312f01d3bf80071852"
}
},
{

View File

@@ -1,5 +1,5 @@
{
"originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8",
"originHash" : "411c4947a4ddec377a0aac37852b26ccdf5b2dd58cb99bedfb2d4c108efd60fd",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25",
"version" : "1.7.0"
"branch" : "main",
"revision" : "9810c8d6c2914de251e072312f01d3bf80071852"
}
},
{
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-navigation",
"state" : {
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
"version" : "2.3.0"
"revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
"version" : "2.3.1"
}
},
{

View File

@@ -3,4 +3,176 @@
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

@@ -85,40 +85,36 @@ struct APIConfiguration {
/// -
///
///
static var defaultHeaders: [String: String] {
static func defaultHeaders() async -> [String: String] {
var headers = [
"Content-Type": "application/json",
"Accept": "application/json",
"Accept-Encoding": "gzip, br",
"Accept-Language": Locale.current.languageCode ?? "en",
"Accept-Language": Locale.current.language.languageCode?.identifier ?? "en",
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 16.4; Scale/2.00)"
]
// headers
let authStatus = UserInfoManager.checkAuthenticationStatus()
let authStatus = await UserInfoManager.checkAuthenticationStatus()
if authStatus.canAutoLogin {
// headers AccountModel
if let userId = UserInfoManager.getCurrentUserId() {
if let userId = await UserInfoManager.getCurrentUserId() {
headers["pub_uid"] = userId
#if DEBUG
debugInfo("🔐 添加认证 header: pub_uid = \(userId)")
debugInfoSync("🔐 添加认证 header: pub_uid = \(userId)")
#endif
}
if let userTicket = UserInfoManager.getCurrentUserTicket() {
if let userTicket = await UserInfoManager.getCurrentUserTicket() {
headers["pub_ticket"] = userTicket
#if DEBUG
debugInfo("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...")
debugInfoSync("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...")
#endif
}
} else {
#if DEBUG
debugInfo("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)")
debugInfoSync("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)")
#endif
}
return headers
}
}

View File

@@ -1,6 +1,7 @@
import Foundation
// MARK: - API Logger
@MainActor
class APILogger {
enum LogLevel {
case none
@@ -21,7 +22,12 @@ class APILogger {
}()
// MARK: - Request Logging
static func logRequest<T: APIRequestProtocol>(_ request: T, url: URL, body: Data?, finalHeaders: [String: String]? = nil) {
@MainActor static func logRequest<T: APIRequestProtocol>(
_ request: T,
url: URL,
body: Data?,
finalHeaders: [String: String]? = nil
) {
#if DEBUG
guard logLevel != .none else { return }
#else

View File

@@ -111,9 +111,10 @@ struct BaseRequest: Codable {
case pubSign = "pub_sign"
}
@MainActor
init() {
//
let preferredLanguage = Locale.current.languageCode ?? "en"
let preferredLanguage = Locale.current.language.languageCode?.identifier ?? "en"
self.acceptLanguage = preferredLanguage
self.lang = preferredLanguage
@@ -237,6 +238,7 @@ struct CarrierInfoManager {
// MARK: - User Info Manager (for Headers)
struct UserInfoManager {
@MainActor
private static let keychain = KeychainManager.shared
// MARK: - Storage Keys
@@ -246,72 +248,66 @@ struct UserInfoManager {
}
// MARK: -
private static var accountModelCache: AccountModel?
private static var userInfoCache: UserInfo?
// UserInfoCacheActor
private static let cacheQueue = DispatchQueue(label: "com.yana.userinfo.cache", attributes: .concurrent)
// MARK: - User ID Management ( AccountModel)
static func getCurrentUserId() -> String? {
return getAccountModel()?.uid
static func getCurrentUserId() async -> String? {
return await getAccountModel()?.uid
}
// MARK: - Access Token Management ( AccountModel)
static func getAccessToken() -> String? {
return getAccountModel()?.accessToken
static func getAccessToken() async -> String? {
return await getAccountModel()?.accessToken
}
// MARK: - Ticket Management ( AccountModel )
private static var currentTicket: String?
// UserInfoCacheActor
static func getCurrentUserTicket() -> String? {
static func getCurrentUserTicket() async -> String? {
// AccountModel ticket
if let accountTicket = getAccountModel()?.ticket, !accountTicket.isEmpty {
if let accountTicket = await getAccountModel()?.ticket, !accountTicket.isEmpty {
return accountTicket
}
//
return currentTicket
// actor
return await cacheActor.getCurrentTicket()
}
static func saveTicket(_ ticket: String) {
currentTicket = ticket
debugInfo("💾 保存 Ticket 到内存")
static func saveTicket(_ ticket: String) async {
await cacheActor.setCurrentTicket(ticket)
debugInfoSync("💾 保存 Ticket 到内存")
}
static func clearTicket() {
currentTicket = nil
debugInfo("🗑️ 清除 Ticket")
static func clearTicket() async {
await cacheActor.clearCurrentTicket()
debugInfoSync("🗑️ 清除 Ticket")
}
// MARK: - User Info Management
static func saveUserInfo(_ userInfo: UserInfo) {
cacheQueue.async(flags: .barrier) {
do {
try keychain.store(userInfo, forKey: StorageKeys.userInfo)
userInfoCache = userInfo
debugInfo("💾 保存用户信息成功")
} catch {
debugError("❌ 保存用户信息失败: \(error)")
}
static func saveUserInfo(_ userInfo: UserInfo) async {
do {
try await keychain.store(userInfo, forKey: StorageKeys.userInfo)
await cacheActor.setUserInfo(userInfo)
debugInfoSync("💾 保存用户信息成功")
} catch {
debugErrorSync("❌ 保存用户信息失败: \(error)")
}
}
static func getUserInfo() -> UserInfo? {
return cacheQueue.sync {
//
if let cached = userInfoCache {
return cached
}
// Keychain
do {
let userInfo = try keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
userInfoCache = userInfo
return userInfo
} catch {
debugError("❌ 读取用户信息失败: \(error)")
return nil
}
static func getUserInfo() async -> UserInfo? {
//
if let cached = await cacheActor.getUserInfo() {
return cached
}
// Keychain
do {
let userInfo = try await keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
await cacheActor.setUserInfo(userInfo)
return userInfo
} catch {
debugErrorSync("❌ 读取用户信息失败: \(error)")
return nil
}
}
@@ -322,7 +318,7 @@ struct UserInfoManager {
ticket: String,
uid: Int?,
userInfo: UserInfo?
) {
) async {
// AccountModel
let accountModel = AccountModel(
uid: uid != nil ? "\(uid!)" : nil,
@@ -336,38 +332,40 @@ struct UserInfoManager {
ticket: ticket
)
saveAccountModel(accountModel)
saveTicket(ticket)
await saveAccountModel(accountModel)
await saveTicket(ticket)
if let userInfo = userInfo {
saveUserInfo(userInfo)
await saveUserInfo(userInfo)
}
debugInfo("✅ 完整认证信息保存成功")
debugInfoSync("✅ 完整认证信息保存成功")
}
///
static func hasValidAuthentication() -> Bool {
return getAccessToken() != nil && getCurrentUserTicket() != nil
static func hasValidAuthentication() async -> Bool {
let token = await getAccessToken()
let ticket = await getCurrentUserTicket()
return token != nil && ticket != nil
}
///
static func clearAllAuthenticationData() {
clearAccountModel()
clearUserInfo()
clearTicket()
static func clearAllAuthenticationData() async {
await clearAccountModel()
await clearUserInfo()
await clearTicket()
debugInfo("🗑️ 清除所有认证信息")
debugInfoSync("🗑️ 清除所有认证信息")
}
/// Ticket
static func restoreTicketIfNeeded() async -> Bool {
guard let accessToken = getAccessToken(),
getCurrentUserTicket() == nil else {
guard let _ = await getAccessToken(),
await getCurrentUserTicket() == nil else {
return false
}
debugInfo("🔄 尝试使用 Access Token 恢复 Ticket...")
debugInfoSync("🔄 尝试使用 Access Token 恢复 Ticket...")
// APIService false
// TicketHelper.createTicketRequest
@@ -377,50 +375,48 @@ struct UserInfoManager {
// MARK: - Account Model Management
/// AccountModel
/// - Parameter accountModel:
static func saveAccountModel(_ accountModel: AccountModel) {
cacheQueue.async(flags: .barrier) {
do {
try keychain.store(accountModel, forKey: StorageKeys.accountModel)
accountModelCache = accountModel
// ticket
if let ticket = accountModel.ticket {
saveTicket(ticket)
}
debugInfo("💾 AccountModel 保存成功")
} catch {
debugError("❌ AccountModel 保存失败: \(error)")
static func saveAccountModel(_ accountModel: AccountModel) async {
do {
try await keychain.store(accountModel, forKey: StorageKeys.accountModel)
await cacheActor.setAccountModel(accountModel)
// ticket
if let ticket = accountModel.ticket {
await saveTicket(ticket)
}
debugInfoSync("💾 AccountModel 保存成功")
} catch {
debugErrorSync("❌ AccountModel 保存失败: \(error)")
}
}
/// AccountModel
/// - Returns: nil
static func getAccountModel() -> AccountModel? {
return cacheQueue.sync {
//
if let cached = accountModelCache {
return cached
}
// Keychain
do {
let accountModel = try keychain.retrieve(AccountModel.self, forKey: StorageKeys.accountModel)
accountModelCache = accountModel
return accountModel
} catch {
debugError("❌ 读取 AccountModel 失败: \(error)")
return nil
}
static func getAccountModel() async -> AccountModel? {
//
if let cached = await cacheActor.getAccountModel() {
return cached
}
// Keychain
do {
let accountModel = try await keychain.retrieve(
AccountModel.self,
forKey: StorageKeys.accountModel
)
await cacheActor.setAccountModel(accountModel)
return accountModel
} catch {
debugErrorSync("❌ 读取 AccountModel 失败: \(error)")
return nil
}
}
/// AccountModel ticket
/// - Parameter ticket:
static func updateAccountModelTicket(_ ticket: String) {
guard var accountModel = getAccountModel() else {
debugError("❌ 无法更新 ticketAccountModel 不存在")
static func updateAccountModelTicket(_ ticket: String) async {
guard var accountModel = await getAccountModel() else {
debugErrorSync("❌ 无法更新 ticketAccountModel 不存在")
return
}
@@ -436,97 +432,78 @@ struct UserInfoManager {
ticket: ticket
)
saveAccountModel(accountModel)
saveTicket(ticket) // ticket
await saveAccountModel(accountModel)
await saveTicket(ticket) // ticket
}
/// AccountModel
/// - Returns:
static func hasValidAccountModel() -> Bool {
guard let accountModel = getAccountModel() else {
static func hasValidAccountModel() async -> Bool {
guard let accountModel = await getAccountModel() else {
return false
}
return accountModel.hasValidAuthentication
}
/// AccountModel
static func clearAccountModel() {
cacheQueue.async(flags: .barrier) {
do {
try keychain.delete(forKey: StorageKeys.accountModel)
accountModelCache = nil
debugInfo("🗑️ AccountModel 已清除")
} catch {
debugError("❌ 清除 AccountModel 失败: \(error)")
}
static func clearAccountModel() async {
do {
try await keychain.delete(forKey: StorageKeys.accountModel)
await cacheActor.clearAccountModel()
debugInfoSync("🗑️ AccountModel 已清除")
} catch {
debugErrorSync("❌ 清除 AccountModel 失败: \(error)")
}
}
///
static func clearUserInfo() {
cacheQueue.async(flags: .barrier) {
do {
try keychain.delete(forKey: StorageKeys.userInfo)
userInfoCache = nil
debugInfo("🗑️ UserInfo 已清除")
} catch {
debugError("❌ 清除 UserInfo 失败: \(error)")
}
static func clearUserInfo() async {
do {
try await keychain.delete(forKey: StorageKeys.userInfo)
await cacheActor.clearUserInfo()
debugInfoSync("🗑️ UserInfo 已清除")
} catch {
debugErrorSync("❌ 清除 UserInfo 失败: \(error)")
}
}
///
static func clearAllCache() {
cacheQueue.async(flags: .barrier) {
accountModelCache = nil
userInfoCache = nil
debugInfo("🗑️ 清除所有内存缓存")
}
static func clearAllCache() async {
await cacheActor.clearAccountModel()
await cacheActor.clearUserInfo()
debugInfoSync("🗑️ 清除所有内存缓存")
}
/// 访
static func preloadCache() {
cacheQueue.async {
// AccountModel
_ = getAccountModel()
// UserInfo
_ = getUserInfo()
debugInfo("🚀 缓存预加载完成")
}
static func preloadCache() async {
await cacheActor.setAccountModel(await getAccountModel())
await cacheActor.setUserInfo(await getUserInfo())
debugInfoSync("🚀 缓存预加载完成")
}
// MARK: - Authentication Validation
///
/// - Returns:
static func checkAuthenticationStatus() -> AuthenticationStatus {
return cacheQueue.sync {
guard let accountModel = getAccountModel() else {
debugInfo("🔍 认证检查:未找到 AccountModel")
return .notFound
}
// uid
guard let uid = accountModel.uid, !uid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
debugInfo("🔍 认证检查uid 无效")
return .invalid
}
// ticket
guard let ticket = accountModel.ticket, !ticket.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
debugInfo("🔍 认证检查ticket 无效")
return .invalid
}
// access token
guard let accessToken = accountModel.accessToken, !accessToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
debugInfo("🔍 认证检查access token 无效")
return .invalid
}
debugInfo("🔍 认证检查:认证有效 - uid: \(uid), ticket: \(ticket.prefix(10))...")
return .valid
static func checkAuthenticationStatus() async -> AuthenticationStatus {
guard let accountModel = await getAccountModel() else {
debugInfoSync("🔍 认证检查:未找到 AccountModel")
return .notFound
}
guard let uid = accountModel.uid, !uid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
debugInfoSync("🔍 认证检查uid 无效")
return .invalid
}
guard let ticket = accountModel.ticket, !ticket.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
debugInfoSync("🔍 认证检查ticket 无效")
return .invalid
}
guard let accessToken = accountModel.accessToken, !accessToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
debugInfoSync("🔍 认证检查access token 无效")
return .invalid
}
debugInfoSync("🔍 认证检查:认证有效 - uid: \(uid), ticket: \(ticket.prefix(10))...")
return .valid
}
///
@@ -556,19 +533,19 @@ struct UserInfoManager {
/// header
/// header
static func testAuthenticationHeaders() {
static func testAuthenticationHeaders() async {
#if DEBUG
debugInfo("\n🧪 开始测试认证 header 功能")
debugInfoSync("\n🧪 开始测试认证 header 功能")
// 1
debugInfo("📝 测试1未登录状态")
clearAllAuthenticationData()
let headers1 = APIConfiguration.defaultHeaders
debugInfoSync("📝 测试1未登录状态")
await clearAllAuthenticationData()
let headers1 = await APIConfiguration.defaultHeaders()
let hasAuthHeaders1 = headers1.keys.contains("pub_uid") || headers1.keys.contains("pub_ticket")
debugInfo(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)")
debugInfoSync(" 认证 headers 存在: \(hasAuthHeaders1) (应该为 false)")
// 2
debugInfo("📝 测试2模拟登录状态")
debugInfoSync("📝 测试2模拟登录状态")
let testAccount = AccountModel(
uid: "12345",
jti: "test-jti",
@@ -580,22 +557,48 @@ struct UserInfoManager {
scope: "read write",
ticket: "test-ticket-12345678901234567890"
)
saveAccountModel(testAccount)
await saveAccountModel(testAccount)
let headers2 = APIConfiguration.defaultHeaders
let headers2 = await APIConfiguration.defaultHeaders()
let hasUid = headers2["pub_uid"] == "12345"
let hasTicket = headers2["pub_ticket"] == "test-ticket-12345678901234567890"
debugInfo(" pub_uid 正确: \(hasUid) (应该为 true)")
debugInfo(" pub_ticket 正确: \(hasTicket) (应该为 true)")
debugInfoSync(" pub_uid 正确: \(hasUid) (应该为 true)")
debugInfoSync(" pub_ticket 正确: \(hasTicket) (应该为 true)")
// 3
debugInfo("📝 测试3清理测试数据")
clearAllAuthenticationData()
debugInfo("✅ 认证 header 测试完成\n")
debugInfoSync("📝 测试3清理测试数据")
await clearAllAuthenticationData()
debugInfoSync("✅ 认证 header 测试完成\n")
#endif
}
}
// MARK: - User Info Cache Actor
actor UserInfoCacheActor {
private var accountModelCache: AccountModel?
private var userInfoCache: UserInfo?
private var currentTicket: String?
// AccountModel
func getAccountModel() -> AccountModel? { accountModelCache }
func setAccountModel(_ model: AccountModel?) { accountModelCache = model }
func clearAccountModel() { accountModelCache = nil }
// UserInfo
func getUserInfo() -> UserInfo? { userInfoCache }
func setUserInfo(_ info: UserInfo?) { userInfoCache = info }
func clearUserInfo() { userInfoCache = nil }
// Ticket
func getCurrentTicket() -> String? { currentTicket }
func setCurrentTicket(_ ticket: String?) { currentTicket = ticket }
func clearCurrentTicket() { currentTicket = nil }
}
extension UserInfoManager {
static let cacheActor = UserInfoCacheActor()
}
// MARK: - API Request Protocol
/// API
@@ -604,7 +607,7 @@ struct UserInfoManager {
/// API
///
///
/// - Response:
/// - Response: Sendable
/// - endpoint: API
/// - method: HTTP
/// -
@@ -618,8 +621,8 @@ struct UserInfoManager {
/// // ...
/// }
/// ```
protocol APIRequestProtocol {
associatedtype Response: Codable
protocol APIRequestProtocol: Sendable {
associatedtype Response: Codable & Sendable
var endpoint: String { get }
var method: HTTPMethod { get }

View File

@@ -14,7 +14,7 @@ import ComposableArchitecture
/// let request = ConfigRequest()
/// let response = try await apiService.request(request)
/// ```
protocol APIServiceProtocol {
protocol APIServiceProtocol: Sendable {
///
/// - Parameter request: APIRequestProtocol
/// - Returns:
@@ -39,19 +39,22 @@ protocol APIServiceProtocol {
/// -
/// - /
/// -
struct LiveAPIService: APIServiceProtocol {
struct LiveAPIService: APIServiceProtocol, Sendable {
private let session: URLSession
private let baseURL: String
// actor
private static let cachedBaseURL: String = APIConfiguration.baseURL
private static let cachedTimeout: TimeInterval = APIConfiguration.timeout
/// API
/// - Parameter baseURL: API URL使
init(baseURL: String = APIConfiguration.baseURL) {
/// - Parameter baseURL: API URL使
init(baseURL: String = LiveAPIService.cachedBaseURL) {
self.baseURL = baseURL
// URLSession
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = APIConfiguration.timeout
config.timeoutIntervalForResource = APIConfiguration.timeout * 2
config.timeoutIntervalForRequest = LiveAPIService.cachedTimeout
config.timeoutIntervalForResource = LiveAPIService.cachedTimeout * 2
config.waitsForConnectivity = true
config.allowsCellularAccess = true
@@ -78,14 +81,14 @@ struct LiveAPIService: APIServiceProtocol {
let startTime = Date()
// Loading
let loadingId = APILoadingManager.shared.startLoading(
let loadingId = await APILoadingManager.shared.startLoading(
shouldShowLoading: request.shouldShowLoading,
shouldShowError: request.shouldShowError
)
// URL
guard let url = buildURL(for: request) else {
APILoadingManager.shared.setError(loadingId, errorMessage: APIError.invalidURL.localizedDescription)
guard let url = await buildURL(for: request) else {
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.invalidURL.localizedDescription)
throw APIError.invalidURL
}
@@ -95,8 +98,8 @@ struct LiveAPIService: APIServiceProtocol {
urlRequest.timeoutInterval = request.timeout
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
//
var headers = APIConfiguration.defaultHeaders
// await
var headers = await APIConfiguration.defaultHeaders()
if let customHeaders = request.headers {
headers.merge(customHeaders) { _, new in new }
}
@@ -119,7 +122,7 @@ struct LiveAPIService: APIServiceProtocol {
//
if request.includeBaseParameters {
//
var baseParams = BaseRequest()
var baseParams = await BaseRequest()
// bodyParams +
baseParams.generateSignature(with: bodyParams)
@@ -127,8 +130,7 @@ struct LiveAPIService: APIServiceProtocol {
//
let baseDict = try baseParams.toDictionary()
finalBody.merge(baseDict) { _, new in new } //
debugInfo("🔐 签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
debugInfoSync("🔐 签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
}
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
@@ -136,17 +138,18 @@ struct LiveAPIService: APIServiceProtocol {
// urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
if let httpBody = urlRequest.httpBody,
let bodyString = String(data: httpBody, encoding: .utf8) {
debugInfo("HTTP Body: \(bodyString)")
debugInfoSync("HTTP Body: \(bodyString)")
}
} catch {
let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription)
await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription)
throw encodingError
}
}
// headers
APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
await APILogger
.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
do {
//
@@ -156,34 +159,36 @@ struct LiveAPIService: APIServiceProtocol {
//
guard let httpResponse = response as? HTTPURLResponse else {
let networkError = APIError.networkError("无效的响应类型")
APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription)
await APILoadingManager.shared.setError(loadingId, errorMessage: networkError.localizedDescription)
throw networkError
}
//
if data.count > APIConfiguration.maxDataSize {
APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription)
await APILogger
.logError(APIError.resourceTooLarge, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription)
throw APIError.resourceTooLarge
}
//
APILogger.logResponse(data: data, response: httpResponse, duration: duration)
await APILogger
.logResponse(data: data, response: httpResponse, duration: duration)
//
APILogger.logPerformanceWarning(duration: duration)
await APILogger.logPerformanceWarning(duration: duration)
// HTTP
guard 200...299 ~= httpResponse.statusCode else {
let errorMessage = extractErrorMessage(from: data)
let httpError = APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage)
APILoadingManager.shared.setError(loadingId, errorMessage: httpError.localizedDescription)
await APILoadingManager.shared.setError(loadingId, errorMessage: httpError.localizedDescription)
throw httpError
}
//
guard !data.isEmpty else {
APILoadingManager.shared.setError(loadingId, errorMessage: APIError.noData.localizedDescription)
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.noData.localizedDescription)
throw APIError.noData
}
@@ -191,28 +196,28 @@ struct LiveAPIService: APIServiceProtocol {
do {
let decoder = JSONDecoder()
let decodedResponse = try decoder.decode(T.Response.self, from: data)
APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
await APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
// loading
APILoadingManager.shared.finishLoading(loadingId)
await APILoadingManager.shared.finishLoading(loadingId)
return decodedResponse
} catch {
let decodingError = APIError.decodingError("响应解析失败: \(error.localizedDescription)")
APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription)
await APILoadingManager.shared.setError(loadingId, errorMessage: decodingError.localizedDescription)
throw decodingError
}
} catch let error as APIError {
let duration = Date().timeIntervalSince(startTime)
APILogger.logError(error, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription)
await APILogger.logError(error, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription)
throw error
} catch {
let duration = Date().timeIntervalSince(startTime)
let apiError = mapSystemError(error)
APILogger.logError(apiError, url: url, duration: duration)
APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
await APILogger.logError(apiError, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
throw apiError
}
}
@@ -228,7 +233,7 @@ struct LiveAPIService: APIServiceProtocol {
///
/// - Parameter request: API
/// - Returns: URL nil
private func buildURL<T: APIRequestProtocol>(for request: T) -> URL? {
@MainActor private func buildURL<T: APIRequestProtocol>(for request: T) -> URL? {
guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else {
return nil
}
@@ -252,9 +257,9 @@ struct LiveAPIService: APIServiceProtocol {
queryItems.append(URLQueryItem(name: key, value: "\(value)"))
}
debugInfo("🔐 GET请求签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
debugInfoSync("🔐 GET请求签名生成完成 - 基于所有参数统一生成: \(baseParams.pubSign)")
} catch {
debugWarn("警告:无法添加基础参数到查询字符串")
debugWarnSync("警告:无法添加基础参数到查询字符串")
}
}
@@ -322,46 +327,31 @@ struct LiveAPIService: APIServiceProtocol {
// MARK: - Mock API Service (for testing)
/// API
///
/// API
/// -
/// -
/// - UI
///
/// 使
/// ```swift
/// var mockService = MockAPIService()
/// mockService.setMockResponse(for: "/client/config", response: mockConfigResponse)
/// let response = try await mockService.request(ConfigRequest())
/// ```
struct MockAPIService: APIServiceProtocol {
/// Mock API Service
actor MockAPIServiceActor: APIServiceProtocol, Sendable {
private var mockResponses: [String: Any] = [:]
mutating func setMockResponse<T>(for endpoint: String, response: T) {
func setMockResponse<T>(for endpoint: String, response: T) {
mockResponses[endpoint] = response
}
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response {
//
try await Task.sleep(nanoseconds: 500_000_000) // 0.5
if let mockResponse = mockResponses[request.endpoint] as? T.Response {
return mockResponse
}
throw APIError.noData
}
}
// MARK: - TCA Dependency Integration
private enum APIServiceKey: DependencyKey {
static let liveValue: APIServiceProtocol = LiveAPIService()
static let testValue: APIServiceProtocol = MockAPIService()
static let liveValue: any APIServiceProtocol & Sendable = LiveAPIService()
static let testValue: any APIServiceProtocol & Sendable = MockAPIServiceActor()
}
extension DependencyValues {
var apiService: APIServiceProtocol {
var apiService: (any APIServiceProtocol & Sendable) {
get { self[APIServiceKey.self] }
set { self[APIServiceKey.self] = newValue }
}

View File

@@ -78,7 +78,7 @@ struct IDLoginAPIRequest: APIRequestProtocol {
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
/// ID
@@ -98,14 +98,6 @@ struct IDLoginAPIRequest: APIRequestProtocol {
"client_id": clientId,
"grant_type": grantType
];
// self.bodyParameters = [
// "phone": phone,
// "password": password,
// "client_secret": clientSecret,
// "version": version,
// "client_id": clientId,
// "grant_type": grantType
// ];
}
}
@@ -186,21 +178,21 @@ struct LoginHelper {
/// - userID: ID
/// - password:
/// - Returns: APInil
static func createIDLoginRequest(userID: String, password: String) -> IDLoginAPIRequest? {
static func createIDLoginRequest(userID: String, password: String) async -> IDLoginAPIRequest? {
// 使DESID
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedID = DESEncrypt.encryptUseDES(userID, key: encryptionKey),
let encryptedPassword = DESEncrypt.encryptUseDES(password, key: encryptionKey) else {
debugError("❌ DES加密失败")
debugErrorSync("❌ DES加密失败")
return nil
}
debugInfo("🔐 DES加密成功")
debugInfo(" 原始ID: \(userID)")
debugInfo(" 加密后ID: \(encryptedID)")
debugInfo(" 原始密码: \(password)")
debugInfo(" 加密后密码: \(encryptedPassword)")
await debugInfoSync("🔐 DES加密成功")
await debugInfoSync(" 原始ID: \(userID)")
await debugInfoSync(" 加密后ID: \(encryptedID)")
await debugInfoSync(" 原始密码: \(password)")
await debugInfoSync(" 加密后密码: \(encryptedPassword)")
return IDLoginAPIRequest(
phone: userID,
@@ -219,7 +211,7 @@ struct TicketAPIRequest: APIRequestProtocol {
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
let customHeaders: [String: String]?
@@ -292,13 +284,13 @@ struct TicketHelper {
/// - accessToken: OAuth 访
/// - uid:
static func debugTicketRequest(accessToken: String, uid: Int?) {
debugInfo("🎫 Ticket 请求调试信息")
debugInfo(" AccessToken: \(accessToken)")
debugInfo(" UID: \(uid?.description ?? "nil")")
debugInfo(" Endpoint: /oauth/ticket")
debugInfo(" Method: POST")
debugInfo(" Headers: pub_uid = \(uid?.description ?? "nil")")
debugInfo(" Parameters: access_token=\(accessToken), issue_type=multi")
debugInfoSync("🎫 Ticket 请求调试信息")
debugInfoSync(" AccessToken: \(accessToken)")
debugInfoSync(" UID: \(uid?.description ?? "nil")")
debugInfoSync(" Endpoint: /oauth/ticket")
debugInfoSync(" Method: POST")
debugInfoSync(" Headers: pub_uid = \(uid?.description ?? "nil")")
debugInfoSync(" Parameters: access_token=\(accessToken), issue_type=multi")
}
}
@@ -315,7 +307,7 @@ struct EmailGetCodeRequest: APIRequestProtocol {
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
///
@@ -356,7 +348,7 @@ struct EmailLoginRequest: APIRequestProtocol {
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
///
@@ -389,13 +381,13 @@ extension LoginHelper {
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
debugError("❌ 邮箱DES加密失败")
debugErrorSync("❌ 邮箱DES加密失败")
return nil
}
debugInfo("🔐 邮箱DES加密成功")
debugInfo(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)")
debugInfoSync("🔐 邮箱DES加密成功")
debugInfoSync(" 原始邮箱: \(email)")
debugInfoSync(" 加密邮箱: \(encryptedEmail)")
return EmailGetCodeRequest(emailAddress: email, type: 1)
}
@@ -405,18 +397,18 @@ extension LoginHelper {
/// - email:
/// - code:
/// - Returns: APInil
static func createEmailLoginRequest(email: String, code: String) -> EmailLoginRequest? {
static func createEmailLoginRequest(email: String, code: String) async -> EmailLoginRequest? {
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
debugError("❌ 邮箱DES加密失败")
await debugErrorSync("❌ 邮箱DES加密失败")
return nil
}
debugInfo("🔐 邮箱验证码登录DES加密成功")
debugInfo(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)")
debugInfo(" 验证码: \(code)")
await debugInfoSync("🔐 邮箱验证码登录DES加密成功")
await debugInfoSync(" 原始邮箱: \(email)")
await debugInfoSync(" 加密邮箱: \(encryptedEmail)")
await debugInfoSync(" 验证码: \(code)")
return EmailLoginRequest(email: encryptedEmail, code: code)
}

View File

@@ -2,13 +2,13 @@ import UIKit
//import NIMSDK
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) async -> Bool {
// UserDefaults Keychain
DataMigrationManager.performStartupMigration()
//
UserInfoManager.preloadCache()
await UserInfoManager.preloadCache()
//
// NetworkManager.shared.networkStatusChanged = { status in

View File

@@ -4,7 +4,7 @@ enum Environment {
}
struct AppConfig {
static var current: Environment = {
static let current: Environment = {
#if DEBUG
return .development
#else
@@ -43,9 +43,9 @@ struct AppConfig {
}
//
static func switchEnvironment(to env: Environment) {
current = env
}
// static func switchEnvironment(to env: Environment) {
// current = env
// }
//
static var enableNetworkDebug: Bool {

View File

@@ -2,17 +2,18 @@ import Foundation
import UIKit //
@_exported import Alamofire //
@MainActor
final class ClientConfig {
static let shared = ClientConfig()
private init() {}
func initializeClient() {
debugInfo("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init")
debugInfoSync("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init")
callClientInitAPI() //
}
func callClientInitAPI() {
debugInfo("🆕 使用GET方法调用初始化接口")
debugInfoSync("🆕 使用GET方法调用初始化接口")
// let queryParams = [
// "debug": "1",

View File

@@ -28,6 +28,144 @@ enum UILogLevel: String, CaseIterable {
case detailed = "详细日志"
}
struct LoginTabView: View {
let store: StoreOf<LoginFeature>
let initStore: StoreOf<InitFeature>
@Binding var selectedLogLevel: APILogger.LogLevel
var body: some View {
VStack {
//
VStack(alignment: .leading, spacing: 8) {
Text("日志级别:")
.font(.headline)
.foregroundColor(.primary)
Picker("日志级别", selection: $selectedLogLevel) {
Text("无日志").tag(APILogger.LogLevel.none)
Text("基础日志").tag(APILogger.LogLevel.basic)
Text("详细日志").tag(APILogger.LogLevel.detailed)
}
.pickerStyle(SegmentedPickerStyle())
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
Spacer()
VStack(spacing: 20) {
Text("eparty")
.font(.largeTitle)
.fontWeight(.bold)
VStack(spacing: 15) {
TextField("账号", text: Binding(
get: { store.account },
set: { store.send(.updateAccount($0)) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
.autocorrectionDisabled(true)
SecureField("密码", text: Binding(
get: { store.password },
set: { store.send(.updatePassword($0)) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(.horizontal)
if let error = store.error {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
VStack(spacing: 10) {
Button(action: {
store.send(.login)
}) {
HStack {
if store.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(store.isLoading ? "登录中..." : "登录")
}
.frame(maxWidth: .infinity)
.padding()
.background(store.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(store.isLoading || store.account.isEmpty || store.password.isEmpty)
Button(action: {
initStore.send(.initialize)
}) {
HStack {
if initStore.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(initStore.isLoading ? "测试中..." : "测试初始化")
}
.frame(maxWidth: .infinity)
.padding()
.background(initStore.isLoading ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(initStore.isLoading)
if let response = initStore.response {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("API 测试结果:")
.font(.headline)
.foregroundColor(.primary)
}
ScrollView {
VStack(alignment: .leading, spacing: 4) {
Text("状态: \(response.status)")
if let message = response.message {
Text("消息: \(message)")
}
if let data = response.data {
Text("版本: \(data.version ?? "未知")")
Text("时间戳: \(data.timestamp ?? 0)")
if let config = data.config {
Text("配置:")
ForEach(Array(config.keys), id: \.self) { key in
Text(" \(key): \(config[key] ?? "")")
}
}
}
}
.font(.system(.caption, design: .monospaced))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.frame(maxHeight: 200)
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(10)
}
if let error = initStore.error {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
.padding()
}
}
.padding(.horizontal)
}
Spacer()
}
.padding()
}
}
struct ContentView: View {
let store: StoreOf<LoginFeature>
let initStore: StoreOf<InitFeature>
@@ -38,155 +176,11 @@ struct ContentView: View {
var body: some View {
WithPerceptionTracking {
TabView(selection: $selectedTab) {
//
VStack {
//
VStack(alignment: .leading, spacing: 8) {
Text("日志级别:")
.font(.headline)
.foregroundColor(.primary)
Picker("日志级别", selection: $selectedLogLevel) {
Text("无日志").tag(APILogger.LogLevel.none)
Text("基础日志").tag(APILogger.LogLevel.basic)
Text("详细日志").tag(APILogger.LogLevel.detailed)
}
.pickerStyle(SegmentedPickerStyle())
LoginTabView(store: store, initStore: initStore, selectedLogLevel: $selectedLogLevel)
.tabItem {
Label("登录", systemImage: "person.circle")
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
Spacer()
VStack(spacing: 20) {
Text("eparty")
.font(.largeTitle)
.fontWeight(.bold)
VStack(spacing: 15) {
TextField("账号", text: Binding(
get: { store.account },
set: { store.send(.updateAccount($0)) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
.autocorrectionDisabled(true)
SecureField("密码", text: Binding(
get: { store.password },
set: { store.send(.updatePassword($0)) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(.horizontal)
if let error = store.error {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
VStack(spacing: 10) {
Button(action: {
store.send(.login)
}) {
HStack {
if store.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(store.isLoading ? "登录中..." : "登录")
}
.frame(maxWidth: .infinity)
.padding()
.background(store.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(store.isLoading || store.account.isEmpty || store.password.isEmpty)
Button(action: {
initStore.send(.initialize)
}) {
HStack {
if initStore.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(initStore.isLoading ? "测试中..." : "测试初始化")
}
.frame(maxWidth: .infinity)
.padding()
.background(initStore.isLoading ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(initStore.isLoading)
// API
if let response = initStore.response {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("API 测试结果:")
.font(.headline)
.foregroundColor(.primary)
}
ScrollView {
VStack(alignment: .leading, spacing: 4) {
Text("状态: \(response.status)")
if let message = response.message {
Text("消息: \(message)")
}
if let data = response.data {
Text("版本: \(data.version ?? "未知")")
Text("时间戳: \(data.timestamp ?? 0)")
if let config = data.config {
Text("配置:")
ForEach(Array(config.keys), id: \.self) { key in
Text(" \(key): \(config[key] ?? "")")
}
}
}
}
.font(.system(.caption, design: .monospaced))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.frame(maxHeight: 200)
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(10)
}
if let error = initStore.error {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
.padding()
}
}
.padding(.horizontal)
}
Spacer()
}
.padding()
.tabItem {
Label("登录", systemImage: "person.circle")
}
.tag(0)
// API
.tag(0)
ConfigView(store: configStore)
.tabItem {
Label("API 测试", systemImage: "network")

View File

@@ -1,11 +1,7 @@
import Foundation
import ComposableArchitecture
import SwiftUI
// PhotosUI (iOS 16.0+)
#if canImport(PhotosUI)
import PhotosUI
#endif
@Reducer
struct CreateFeedFeature {
@@ -13,52 +9,33 @@ struct CreateFeedFeature {
struct State: Equatable {
var content: String = ""
var processedImages: [UIImage] = []
var isLoading: Bool = false
var errorMessage: String? = nil
var characterCount: Int = 0
// iOS 16+ PhotosPicker
#if canImport(PhotosUI) && swift(>=5.7)
var selectedImages: [PhotosPickerItem] = []
#endif
// iOS 15 UIImagePickerController
var showingImagePicker: Bool = false
var canAddMoreImages: Bool {
processedImages.count < 9
}
var canPublish: Bool {
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
}
var isLoading: Bool = false
}
enum Action {
case contentChanged(String)
case publishButtonTapped
case publishResponse(Result<PublishDynamicResponse, Error>)
case clearError
case dismissView
// iOS 16+ PhotosPicker Actions
#if canImport(PhotosUI) && swift(>=5.7)
case photosPickerItemsChanged([PhotosPickerItem])
case processPhotosPickerItems([PhotosPickerItem])
#endif
// iOS 15 UIImagePickerController Actions
case showImagePicker
case hideImagePicker
case imageSelected(UIImage)
case removeImage(Int)
case updateProcessedImages([UIImage])
}
@Dependency(\.apiService) var apiService
@Dependency(\.dismiss) var dismiss
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
@@ -66,72 +43,47 @@ struct CreateFeedFeature {
state.content = newContent
state.characterCount = newContent.count
return .none
#if canImport(PhotosUI) && swift(>=5.7)
case .photosPickerItemsChanged(let items):
state.selectedImages = items
return .run { send in
await send(.processPhotosPickerItems(items))
}
case .processPhotosPickerItems(let items):
return .run { [currentImages = state.processedImages] send in
let currentImages = state.processedImages
return .run { send in
var newImages = currentImages
for item in items {
if let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
if newImages.count < 9 {
newImages.append(image)
}
guard let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) else { continue }
if newImages.count < 9 {
newImages.append(image)
}
}
await MainActor.run {
state.processedImages = newImages
send(.updateProcessedImages(newImages))
}
}
#endif
case .showImagePicker:
state.showingImagePicker = true
case .updateProcessedImages(let images):
state.processedImages = images
return .none
case .hideImagePicker:
state.showingImagePicker = false
return .none
case .imageSelected(let image):
if state.processedImages.count < 9 {
state.processedImages.append(image)
}
state.showingImagePicker = false
return .none
case .removeImage(let index):
guard index < state.processedImages.count else { return .none }
state.processedImages.remove(at: index)
#if canImport(PhotosUI) && swift(>=5.7)
if index < state.selectedImages.count {
state.selectedImages.remove(at: index)
}
#endif
return .none
case .publishButtonTapped:
guard state.canPublish else {
state.errorMessage = "请输入内容"
return .none
}
state.isLoading = true
state.errorMessage = nil
let request = PublishDynamicRequest(
content: state.content.trimmingCharacters(in: .whitespacesAndNewlines),
images: state.processedImages
)
return .run { send in
do {
let response = try await apiService.request(request)
@@ -140,27 +92,21 @@ struct CreateFeedFeature {
await send(.publishResponse(.failure(error)))
}
}
case .publishResponse(.success(let response)):
state.isLoading = false
if response.code == 200 {
//
return .send(.dismissView)
} else {
state.errorMessage = response.message.isEmpty ? "发布失败" : response.message
return .none
}
case .publishResponse(.failure(let error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
return .none
case .clearError:
state.errorMessage = nil
return .none
case .dismissView:
return .run { _ in
await dismiss()
@@ -170,47 +116,57 @@ struct CreateFeedFeature {
}
}
extension CreateFeedFeature.Action: Equatable {
static func == (lhs: CreateFeedFeature.Action, rhs: CreateFeedFeature.Action) -> Bool {
switch (lhs, rhs) {
case let (.contentChanged(a), .contentChanged(b)):
return a == b
case (.publishButtonTapped, .publishButtonTapped):
return true
case (.clearError, .clearError):
return true
case (.dismissView, .dismissView):
return true
case let (.removeImage(a), .removeImage(b)):
return a == b
default:
return false
}
}
}
// MARK: -
///
struct PublishDynamicRequest: APIRequestProtocol {
typealias Response = PublishDynamicResponse
let endpoint: String = "/dynamic/square/publish" //
let endpoint: String = "/dynamic/square/publish"
let method: HTTPMethod = .POST
let includeBaseParameters: Bool = true
let queryParameters: [String: String]? = nil
let timeout: TimeInterval = 30.0
let content: String
let images: [UIImage]
let type: Int // 0: , 2:
init(content: String, images: [UIImage] = []) {
self.content = content
self.images = images
self.type = images.isEmpty ? 0 : 2
}
var bodyParameters: [String: Any]? {
var params: [String: Any] = [
"content": content,
"type": type
]
// base64
if !images.isEmpty {
let imageData = images.compactMap { image in
image.jpegData(compressionQuality: 0.8)?.base64EncodedString()
}
params["images"] = imageData
}
return params
}
}
///
struct PublishDynamicResponse: Codable {
let code: Int
let message: String

View File

@@ -48,12 +48,12 @@ struct EMailLoginFeature {
case .getVerificationCodeTapped:
guard !state.email.isEmpty else {
state.errorMessage = "email_login.email_required".localized
state.errorMessage = NSLocalizedString("email_login.email_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = "email_login.invalid_email".localized
state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "")
return .none
}
@@ -98,12 +98,12 @@ struct EMailLoginFeature {
case .loginButtonTapped(let email, let verificationCode):
guard !email.isEmpty && !verificationCode.isEmpty else {
state.errorMessage = "email_login.fields_required".localized
state.errorMessage = NSLocalizedString("email_login.fields_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(email) else {
state.errorMessage = "email_login.invalid_email".localized
state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "")
return .none
}
@@ -112,7 +112,7 @@ struct EMailLoginFeature {
return .run { send in
do {
guard let request = LoginHelper.createEmailLoginRequest(email: email, code: verificationCode) else {
guard let request = await LoginHelper.createEmailLoginRequest(email: email, code: verificationCode) else {
await send(.loginResponse(.failure(APIError.encryptionFailed)))
return
}
@@ -149,14 +149,11 @@ struct EMailLoginFeature {
case .loginResponse(.success(let accountModel)):
state.isLoading = false
// AccountModel
UserInfoManager.saveAccountModel(accountModel)
//
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
return .none
// Effect AccountModel
return .run { _ in
await UserInfoManager.saveAccountModel(accountModel)
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
}
case .loginResponse(.failure(let error)):
state.isLoading = false

View File

@@ -16,9 +16,10 @@ struct FeedFeature {
// CreateFeedView
var isShowingCreateFeed = false
var createFeedState: CreateFeedFeature.State? = nil
}
enum Action: Equatable {
enum Action {
case onAppear
case loadLatestMoments
case loadMoreMoments
@@ -30,6 +31,7 @@ struct FeedFeature {
case showCreateFeed
case dismissCreateFeed
case createFeedCompleted
indirect case createFeed(CreateFeedFeature.Action)
}
@Dependency(\.apiService) var apiService
@@ -38,6 +40,10 @@ struct FeedFeature {
Reduce { state, action in
switch action {
case .onAppear:
#if DEBUG
return .none
#endif
//
guard !state.isInitialized else { return .none }
state.isInitialized = true
@@ -83,49 +89,49 @@ struct FeedFeature {
state.isLoading = false
//
debugInfo("📱 FeedFeature: API 响应成功")
debugInfo("📱 FeedFeature: response.code = \(response.code)")
debugInfo("📱 FeedFeature: response.message = \(response.message)")
debugInfo("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")")
debugInfoSync("📱 FeedFeature: API 响应成功")
debugInfoSync("📱 FeedFeature: response.code = \(response.code)")
debugInfoSync("📱 FeedFeature: response.message = \(response.message)")
debugInfoSync("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")")
//
guard response.code == 200, let data = response.data else {
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
state.error = errorMsg
debugError("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)")
debugErrorSync("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)")
return .none
}
//
debugInfo("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)")
debugInfo("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)")
debugInfoSync("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)")
debugInfoSync("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)")
//
let isRefresh = state.nextDynamicId == 0
debugInfo("📱 FeedFeature: isRefresh = \(isRefresh)")
debugInfoSync("📱 FeedFeature: isRefresh = \(isRefresh)")
if isRefresh {
//
state.moments = data.dynamicList
debugInfo(" FeedFeature: 刷新数据moments.count = \(state.moments.count)")
debugInfoSync(" FeedFeature: 刷新数据moments.count = \(state.moments.count)")
} else {
//
let oldCount = state.moments.count
state.moments.append(contentsOf: data.dynamicList)
debugInfo(" FeedFeature: 加载更多moments.count: \(oldCount) -> \(state.moments.count)")
debugInfoSync(" FeedFeature: 加载更多moments.count: \(oldCount) -> \(state.moments.count)")
}
//
state.nextDynamicId = data.nextDynamicId
state.hasMoreData = !data.dynamicList.isEmpty
debugInfo("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)")
debugInfoSync("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)")
return .none
case let .momentsResponse(.failure(error)):
state.isLoading = false
state.error = error.localizedDescription
debugError("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)")
debugErrorSync("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)")
return .none
case .clearError:
@@ -142,17 +148,28 @@ struct FeedFeature {
case .showCreateFeed:
state.isShowingCreateFeed = true
// createFeedState
state.createFeedState = CreateFeedFeature.State()
return .none
case .dismissCreateFeed:
state.isShowingCreateFeed = false
state.createFeedState = nil
return .none
case .createFeedCompleted:
state.isShowingCreateFeed = false
state.createFeedState = nil
//
return .send(.loadLatestMoments)
case .createFeed:
// Action reducer
return .none
}
}
// reducer
ifLet(\State.createFeedState, action: /Action.createFeed) {
CreateFeedFeature()
}
}
}

View File

@@ -18,7 +18,7 @@ struct HomeFeature {
var feedState = FeedFeature.State()
}
enum Action: Equatable {
enum Action {
case onAppear
case loadUserInfo
case userInfoLoaded(UserInfo?)
@@ -56,8 +56,10 @@ struct HomeFeature {
case .loadUserInfo:
//
let userInfo = UserInfoManager.getUserInfo()
return .send(.userInfoLoaded(userInfo))
return .run { send in
let userInfo = await UserInfoManager.getUserInfo()
await send(.userInfoLoaded(userInfo))
}
case let .userInfoLoaded(userInfo):
state.userInfo = userInfo
@@ -65,8 +67,10 @@ struct HomeFeature {
case .loadAccountModel:
//
let accountModel = UserInfoManager.getAccountModel()
return .send(.accountModelLoaded(accountModel))
return .run { send in
let accountModel = await UserInfoManager.getAccountModel()
await send(.accountModelLoaded(accountModel))
}
case let .accountModelLoaded(accountModel):
state.accountModel = accountModel
@@ -76,12 +80,11 @@ struct HomeFeature {
return .send(.logout)
case .logout:
//
UserInfoManager.clearAllAuthenticationData()
//
NotificationCenter.default.post(name: .homeLogout, object: nil)
return .none
//
return .run { _ in
await UserInfoManager.clearAllAuthenticationData()
NotificationCenter.default.post(name: .homeLogout, object: nil)
}
case .settingDismissed:
state.isSettingPresented = false

View File

@@ -56,7 +56,6 @@ struct IDLoginFeature {
case .togglePasswordVisibility:
state.isPasswordVisible.toggle()
return .none
case let .loginButtonTapped(userID, password):
state.userID = userID
state.password = password
@@ -64,17 +63,13 @@ struct IDLoginFeature {
state.errorMessage = nil
state.ticketError = nil
state.loginStep = .authenticating
// IDAPI
// API Effect
return .run { send in
do {
// 使LoginHelper
guard let loginRequest = LoginHelper.createIDLoginRequest(userID: userID, password: password) else {
guard let loginRequest = await LoginHelper.createIDLoginRequest(userID: userID, password: password) else {
await send(.loginResponse(.failure(APIError.decodingError("加密失败"))))
return
}
//
let response = try await apiService.request(loginRequest)
await send(.loginResponse(.success(response)))
} catch {
@@ -85,35 +80,21 @@ struct IDLoginFeature {
}
}
}
case .forgotPasswordTapped:
// TODO:
return .none
case .backButtonTapped:
//
return .none
case let .loginResponse(.success(response)):
state.isLoading = false
if response.isSuccess {
// OAuth
state.errorMessage = nil
// AccountModel
if let loginData = response.data,
let accountModel = AccountModel.from(loginData: loginData) {
state.accountModel = accountModel
//
// Effect userInfo
if let userInfo = loginData.userInfo {
UserInfoManager.saveUserInfo(userInfo)
return .run { _ in await UserInfoManager.saveUserInfo(userInfo) }
}
debugInfo("✅ ID 登录 OAuth 认证成功")
debugInfo("🔑 Access Token: \(accountModel.accessToken ?? "nil")")
debugInfo("🆔 用户 UID: \(accountModel.uid ?? "nil")")
// ticket
return .send(.requestTicket(accessToken: accountModel.accessToken!))
} else {
@@ -125,90 +106,77 @@ struct IDLoginFeature {
state.loginStep = .failed
}
return .none
case let .loginResponse(.failure(error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
state.loginStep = .failed
return .none
case let .requestTicket(accessToken):
state.isTicketLoading = true
state.ticketError = nil
state.loginStep = .gettingTicket
return .run { [accountModel = state.accountModel] send in
//
let uid: Int? = {
if let am = state.accountModel, let uidStr = am.uid { return Int(uidStr) } else { return nil }
}()
return .run { send in
do {
// AccountModel uid Int
let uid = accountModel?.uid != nil ? Int(accountModel!.uid!) : nil
let ticketRequest = TicketHelper.createTicketRequest(accessToken: accessToken, uid: uid)
let response = try await apiService.request(ticketRequest)
await send(.ticketResponse(.success(response)))
} catch {
debugError("❌ ID登录 Ticket 获取失败: \(error)")
debugErrorSync("❌ ID登录 Ticket 获取失败: \(error)")
await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription))))
}
}
case let .ticketResponse(.success(response)):
state.isTicketLoading = false
if response.isSuccess {
state.ticketError = nil
state.loginStep = .completed
debugInfo("✅ ID 登录完整流程成功")
debugInfo("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
// AccountModel ticket
if let ticket = response.ticket {
if var accountModel = state.accountModel {
accountModel.ticket = ticket
state.accountModel = accountModel
// AccountModel
UserInfoManager.saveAccountModel(accountModel)
// Ticket
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
} else {
debugError("❌ AccountModel 不存在,无法保存 ticket")
state.ticketError = "内部错误:账户信息丢失"
state.loginStep = .failed
debugInfoSync("✅ ID 登录完整流程成功")
debugInfoSync("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
// --- Effect state/accountModel ---
if let ticket = response.ticket, let oldAccountModel = state.accountModel {
// withTicket struct newAccountModel
let newAccountModel = oldAccountModel.withTicket(ticket)
state.accountModel = newAccountModel
// newAccountModel state
return .run { _ in
// state/accountModel Swift
await UserInfoManager.saveAccountModel(newAccountModel)
}
} else {
} else if response.ticket == nil {
state.ticketError = "Ticket 为空"
state.loginStep = .failed
} else {
debugErrorSync("❌ AccountModel 不存在,无法保存 ticket")
state.ticketError = "内部错误:账户信息丢失"
state.loginStep = .failed
}
} else {
state.ticketError = response.errorMessage
state.loginStep = .failed
}
return .none
case let .ticketResponse(.failure(error)):
state.isTicketLoading = false
state.ticketError = error.localizedDescription
state.loginStep = .failed
debugError("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)")
debugErrorSync("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)")
return .none
case .clearTicketError:
state.ticketError = nil
return .none
case .resetLogin:
state.isLoading = false
state.isTicketLoading = false
state.errorMessage = nil
state.ticketError = nil
state.accountModel = nil // AccountModel
state.accountModel = nil
state.loginStep = .initial
//
UserInfoManager.clearAllAuthenticationData()
return .none
// Effect
return .run { _ in await UserInfoManager.clearAllAuthenticationData() }
}
}
}

View File

@@ -81,7 +81,7 @@ struct LoginFeature {
return .run { [account = state.account, password = state.password] send in
do {
// 使LoginHelper
guard let loginRequest = LoginHelper.createIDLoginRequest(userID: account, password: password) else {
guard let loginRequest = await LoginHelper.createIDLoginRequest(userID: account, password: password) else {
await send(.loginResponse(.failure(APIError.decodingError("加密失败"))))
return
}
@@ -108,10 +108,9 @@ struct LoginFeature {
if let loginData = response.data,
let accountModel = AccountModel.from(loginData: loginData) {
state.accountModel = accountModel
debugInfo("✅ OAuth 认证成功")
debugInfo("🔑 Access Token: \(accountModel.accessToken ?? "nil")")
debugInfo("🆔 用户 UID: \(accountModel.uid ?? "nil")")
debugInfoSync("✅ OAuth 认证成功")
debugInfoSync("🔑 Access Token: \(accountModel.accessToken ?? "nil")")
debugInfoSync("🆔 用户 UID: \(accountModel.uid ?? "nil")")
// ticket
return .send(.requestTicket(accessToken: accountModel.accessToken!))
@@ -144,7 +143,7 @@ struct LoginFeature {
let response = try await apiService.request(ticketRequest)
await send(.ticketResponse(.success(response)))
} catch {
debugError("❌ Ticket 获取失败: \(error)")
debugErrorSync("❌ Ticket 获取失败: \(error)")
await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription))))
}
}
@@ -155,22 +154,21 @@ struct LoginFeature {
state.ticketError = nil
state.loginStep = .completed
debugInfo("✅ 完整登录流程成功")
debugInfo("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
debugInfoSync("✅ 完整登录流程成功")
debugInfoSync("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
// AccountModel ticket
if let ticket = response.ticket {
if var accountModel = state.accountModel {
accountModel.ticket = ticket
state.accountModel = accountModel
// AccountModel
UserInfoManager.saveAccountModel(accountModel)
// Ticket
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
if let oldAccountModel = state.accountModel {
let newAccountModel = oldAccountModel.withTicket(ticket)
state.accountModel = newAccountModel
// Effect AccountModel
return .run { _ in
await UserInfoManager.saveAccountModel(newAccountModel)
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
}
} else {
debugError("❌ AccountModel 不存在,无法保存 ticket")
debugErrorSync("❌ AccountModel 不存在,无法保存 ticket")
state.ticketError = "内部错误:账户信息丢失"
state.loginStep = .failed
}
@@ -189,7 +187,7 @@ struct LoginFeature {
state.isTicketLoading = false
state.ticketError = error.localizedDescription
state.loginStep = .failed
debugError("❌ Ticket 获取失败: \(error.localizedDescription)")
debugErrorSync("❌ Ticket 获取失败: \(error.localizedDescription)")
return .none
case .clearTicketError:
@@ -203,11 +201,8 @@ struct LoginFeature {
state.ticketError = nil
state.accountModel = nil // AccountModel
state.loginStep = .initial
//
UserInfoManager.clearAllAuthenticationData()
return .none
// Effect
return .run { _ in await UserInfoManager.clearAllAuthenticationData() }
case .idLogin:
// IDLoginfeature

View File

@@ -57,12 +57,12 @@ struct RecoverPasswordFeature {
case .getVerificationCodeTapped:
guard !state.email.isEmpty else {
state.errorMessage = "recover_password.email_required".localized
state.errorMessage = NSLocalizedString("recover_password.email_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = "recover_password.invalid_email".localized
state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "")
return .none
}
@@ -101,23 +101,23 @@ struct RecoverPasswordFeature {
if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription
} else {
state.errorMessage = "recover_password.code_send_failed".localized
state.errorMessage = NSLocalizedString("recover_password.code_send_failed", comment: "")
}
return .none
case .resetPasswordTapped:
guard !state.email.isEmpty && !state.verificationCode.isEmpty && !state.newPassword.isEmpty else {
state.errorMessage = "recover_password.fields_required".localized
state.errorMessage = NSLocalizedString("recover_password.fields_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = "recover_password.invalid_email".localized
state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "")
return .none
}
guard ValidationHelper.isValidPassword(state.newPassword) else {
state.errorMessage = "recover_password.invalid_password".localized
state.errorMessage = NSLocalizedString("recover_password.invalid_password", comment: "")
return .none
}
@@ -160,7 +160,7 @@ struct RecoverPasswordFeature {
if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription
} else {
state.errorMessage = "recover_password.reset_failed".localized
state.errorMessage = NSLocalizedString("recover_password.reset_failed", comment: "")
}
return .none
@@ -199,7 +199,7 @@ struct ResetPasswordResponse: Codable, Equatable {
///
var errorMessage: String {
return message ?? "recover_password.reset_failed".localized
return message ?? NSLocalizedString("recover_password.reset_failed", comment: "")
}
}
@@ -211,7 +211,7 @@ struct ResetPasswordRequest: APIRequestProtocol {
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
///
@@ -238,13 +238,13 @@ struct RecoverPasswordHelper {
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
debugError("❌ 邮箱DES加密失败")
debugErrorSync("❌ 邮箱DES加密失败")
return nil
}
debugInfo("🔐 密码恢复邮箱DES加密成功")
debugInfo(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)")
debugInfoSync("🔐 密码恢复邮箱DES加密成功")
debugInfoSync(" 原始邮箱: \(email)")
debugInfoSync(" 加密邮箱: \(encryptedEmail)")
// 使type=3
return EmailGetCodeRequest(emailAddress: email, type: 3)
@@ -261,16 +261,16 @@ struct RecoverPasswordHelper {
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey),
let encryptedPassword = DESEncrypt.encryptUseDES(newPassword, key: encryptionKey) else {
debugError("❌ 密码重置DES加密失败")
debugErrorSync("❌ 密码重置DES加密失败")
return nil
}
debugInfo("🔐 密码重置DES加密成功")
debugInfo(" 原始邮箱: \(email)")
debugInfo(" 加密邮箱: \(encryptedEmail)")
debugInfo(" 验证码: \(code)")
debugInfo(" 原始新密码: \(newPassword)")
debugInfo(" 加密新密码: \(encryptedPassword)")
debugInfoSync("🔐 密码重置DES加密成功")
debugInfoSync(" 原始邮箱: \(email)")
debugInfoSync(" 加密邮箱: \(encryptedEmail)")
debugInfoSync(" 验证码: \(code)")
debugInfoSync(" 原始新密码: \(newPassword)")
debugInfoSync(" 加密新密码: \(encryptedPassword)")
return ResetPasswordRequest(
email: email,

View File

@@ -32,16 +32,20 @@ struct SettingFeature {
)
case .loadUserInfo:
let userInfo = UserInfoManager.getUserInfo()
return .send(.userInfoLoaded(userInfo))
return .run { send in
let userInfo = await UserInfoManager.getUserInfo()
await send(.userInfoLoaded(userInfo))
}
case let .userInfoLoaded(userInfo):
state.userInfo = userInfo
return .none
case .loadAccountModel:
let accountModel = UserInfoManager.getAccountModel()
return .send(.accountModelLoaded(accountModel))
return .run { send in
let accountModel = await UserInfoManager.getAccountModel()
await send(.accountModelLoaded(accountModel))
}
case let .accountModelLoaded(accountModel):
state.accountModel = accountModel
@@ -52,18 +56,15 @@ struct SettingFeature {
case .logout:
state.isLoading = true
//
UserInfoManager.clearAllAuthenticationData()
//
NotificationCenter.default.post(name: .homeLogout, object: nil)
return .none
return .run { _ in
await UserInfoManager.clearAllAuthenticationData()
NotificationCenter.default.post(name: .homeLogout, object: nil)
}
case .dismissTapped:
//
NotificationCenter.default.post(name: .settingsDismiss, object: nil)
return .none
return .run { _ in
NotificationCenter.default.post(name: .settingsDismiss, object: nil)
}
}
}
}

View File

@@ -32,7 +32,6 @@ struct SplashFeature {
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 = 1,000,000,000
await send(.splashFinished)
}
case .splashFinished:
state.isLoading = false
state.shouldShowMainApp = true
@@ -45,20 +44,25 @@ struct SplashFeature {
//
return .run { send in
let authStatus = UserInfoManager.checkAuthenticationStatus()
let authStatus = await UserInfoManager.checkAuthenticationStatus()
await send(.authenticationChecked(authStatus))
}
case let .authenticationChecked(status):
#if DEBUG
debugInfoSync("🔑 需要手动登录")
NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
return .none
#endif
state.isCheckingAuthentication = false
state.authenticationStatus = status
//
if status.canAutoLogin {
debugInfo("🎉 自动登录成功,进入主页")
debugInfoSync("🎉 自动登录成功,进入主页")
NotificationCenter.default.post(name: .autoLoginSuccess, object: nil)
} else {
debugInfo("🔑 需要手动登录")
debugInfoSync("🔑 需要手动登录")
NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
}
@@ -66,4 +70,4 @@ struct SplashFeature {
}
}
}
}
}

View File

@@ -9,6 +9,7 @@ public enum LogLevel: Int {
case error
}
@MainActor
public class LogManager {
///
public static let shared = LogManager()
@@ -45,43 +46,99 @@ public class LogManager {
}
// MARK: -
@MainActor
public func logVerbose(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.verbose, message(), onlyRelease: onlyRelease)
}
@MainActor
public func logDebug(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.debug, message(), onlyRelease: onlyRelease)
}
@MainActor
public func logInfo(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.info, message(), onlyRelease: onlyRelease)
}
@MainActor
public func logWarn(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.warn, message(), onlyRelease: onlyRelease)
}
@MainActor
public func logError(_ message: @autoclosure () -> String, onlyRelease: Bool = false) {
LogManager.shared.log(.error, message(), onlyRelease: onlyRelease)
}
// MARK: - DEBUG使
public func debugVerbose(_ message: @autoclosure () -> String) {
LogManager.shared.debugLog(.verbose, message())
public func debugVerbose(_ message: @autoclosure () -> String) async {
let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.verbose, msg)
}
}
public func debugLog(_ message: @autoclosure () -> String) {
LogManager.shared.debugLog(.debug, message())
public func debugLog(_ message: @autoclosure () -> String) async {
let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.debug, msg)
}
}
public func debugInfo(_ message: @autoclosure () -> String) {
LogManager.shared.debugLog(.info, message())
public func debugInfo(_ message: @autoclosure () -> String) async {
let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.info, msg)
}
}
public func debugWarn(_ message: @autoclosure () -> String) {
LogManager.shared.debugLog(.warn, message())
public func debugWarn(_ message: @autoclosure () -> String) async {
let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.warn, msg)
}
}
public func debugError(_ message: @autoclosure () -> String) {
LogManager.shared.debugLog(.error, message())
}
public func debugError(_ message: @autoclosure () -> String) async {
let msg = message()
await MainActor.run {
LogManager.shared.debugLog(.error, msg)
}
}
// fire-and-forget Sync
public func debugVerboseSync(_ message: @autoclosure () -> String) {
let msg = message()
Task {
await debugVerbose(msg)
}
}
public func debugLogSync(_ message: @autoclosure () -> String) {
let msg = message()
Task {
await debugLog(msg)
}
}
public func debugInfoSync(_ message: @autoclosure () -> String) {
let msg = message()
Task {
await debugInfo(msg)
}
}
public func debugWarnSync(_ message: @autoclosure () -> String) {
let msg = message()
Task {
await debugWarn(msg)
}
}
public func debugErrorSync(_ message: @autoclosure () -> String) {
let msg = message()
Task {
await debugError(msg)
}
}

View File

@@ -18,25 +18,25 @@ struct APILoadingEffectView: View {
if let firstItem = getFirstDisplayItem() {
SingleLoadingView(item: firstItem)
.onAppear {
debugInfo("🔍 Loading item appeared: \(firstItem.id)")
debugInfoSync("🔍 Loading item appeared: \(firstItem.id)")
}
.onDisappear {
debugInfo("🔍 Loading item disappeared: \(firstItem.id)")
debugInfoSync("🔍 Loading item disappeared: \(firstItem.id)")
}
}
}
.allowsHitTesting(false) //
.ignoresSafeArea(.all) //
.onReceive(loadingManager.$loadingItems) { items in
debugInfo("🔍 Loading items updated: \(items.count) items")
debugInfoSync("🔍 Loading items updated: \(items.count) items")
}
}
///
private func getFirstDisplayItem() -> APILoadingItem? {
guard Thread.isMainThread else {
debugWarn("⚠️ getFirstDisplayItem called from background thread")
return nil
debugWarnSync("⚠️ getFirstDisplayItem called from background thread")
return nil
}
return loadingManager.loadingItems.first { $0.shouldDisplay }
@@ -151,7 +151,7 @@ struct APILoadingEffectView_Previews: PreviewProvider {
.font(.title)
Button("测试按钮") {
debugInfo("按钮被点击了!")
debugInfoSync("按钮被点击了!")
}
.padding()
.background(Color.blue)
@@ -169,12 +169,12 @@ struct APILoadingEffectView_Previews: PreviewProvider {
let manager = APILoadingManager.shared
// loading
let id1 = await manager.startLoading()
let id1 = manager.startLoading()
// 2
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
Task {
await manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
}
}
}
@@ -197,13 +197,13 @@ private struct PreviewStateModifier: ViewModifier {
let manager = APILoadingManager.shared
if showLoading {
let _ = await manager.startLoading()
let _ = manager.startLoading()
}
if showError {
let id = await manager.startLoading()
let id = manager.startLoading()
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1
await manager.setError(id, errorMessage: errorMessage)
manager.setError(id, errorMessage: errorMessage)
}
}
}
@@ -224,4 +224,4 @@ extension View {
))
}
}
#endif
#endif

View File

@@ -11,24 +11,18 @@ import Combine
/// - loading
/// -
/// - 线
@MainActor
class APILoadingManager: ObservableObject {
// MARK: - Properties
///
static let shared = APILoadingManager()
///
@Published private(set) var loadingItems: [APILoadingItem] = []
///
private var errorCleanupTasks: [UUID: DispatchWorkItem] = [:]
///
private init() {}
// MARK: - Public Methods
/// loading
/// - Parameters:
/// - shouldShowLoading: loading
@@ -36,127 +30,74 @@ class APILoadingManager: ObservableObject {
/// - Returns: ID
func startLoading(shouldShowLoading: Bool = true, shouldShowError: Bool = true) -> UUID {
let loadingId = UUID()
let loadingItem = APILoadingItem(
id: loadingId,
state: .loading,
shouldShowError: shouldShowError,
shouldShowLoading: shouldShowLoading
)
// 🚨 线 @Published
DispatchQueue.main.async { [weak self] in
self?.loadingItems.append(loadingItem)
}
loadingItems.append(loadingItem)
return loadingId
}
/// loading
/// - Parameter id: ID
func finishLoading(_ id: UUID) {
DispatchQueue.main.async { [weak self] in
self?.removeLoading(id)
}
removeLoading(id)
}
/// loading
/// - Parameters:
/// - id: ID
/// - errorMessage:
func setError(_ id: UUID, errorMessage: String) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
//
if let index = self.loadingItems.firstIndex(where: { $0.id == id }) {
let currentItem = self.loadingItems[index]
//
if currentItem.shouldShowError {
let errorItem = APILoadingItem(
id: id,
state: .error(message: errorMessage),
shouldShowError: true,
shouldShowLoading: currentItem.shouldShowLoading
)
self.loadingItems[index] = errorItem
//
self.setupErrorCleanup(for: id)
} else {
//
self.loadingItems.removeAll { $0.id == id }
}
}
guard let index = loadingItems.firstIndex(where: { $0.id == id }) else { return }
let currentItem = loadingItems[index]
if currentItem.shouldShowError {
let errorItem = APILoadingItem(
id: id,
state: .error(message: errorMessage),
shouldShowError: true,
shouldShowLoading: currentItem.shouldShowLoading
)
loadingItems[index] = errorItem
setupErrorCleanup(for: id)
} else {
loadingItems.removeAll { $0.id == id }
}
}
///
/// - Parameter id: ID
private func removeLoading(_ id: UUID) {
cancelErrorCleanup(for: id)
// 🚨 线 @Published
if Thread.isMainThread {
loadingItems.removeAll { $0.id == id }
} else {
DispatchQueue.main.async { [weak self] in
self?.loadingItems.removeAll { $0.id == id }
}
}
loadingItems.removeAll { $0.id == id }
}
///
func clearAll() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
//
self.errorCleanupTasks.values.forEach { $0.cancel() }
self.errorCleanupTasks.removeAll()
//
self.loadingItems.removeAll()
}
errorCleanupTasks.values.forEach { $0.cancel() }
errorCleanupTasks.removeAll()
loadingItems.removeAll()
}
// MARK: - Computed Properties
/// loading
var hasActiveLoading: Bool {
if Thread.isMainThread {
return loadingItems.contains { $0.state == .loading && $0.shouldDisplay }
} else {
return false
}
loadingItems.contains { $0.state == .loading && $0.shouldDisplay }
}
///
var hasActiveError: Bool {
if Thread.isMainThread {
return loadingItems.contains { $0.isError && $0.shouldDisplay }
} else {
return false
}
loadingItems.contains { $0.isError && $0.shouldDisplay }
}
// MARK: - Private Methods
///
/// - Parameter id: ID
private func setupErrorCleanup(for id: UUID) {
let workItem = DispatchWorkItem { [weak self] in
self?.removeLoading(id)
}
errorCleanupTasks[id] = workItem
DispatchQueue.main.asyncAfter(
deadline: .now() + APILoadingConfiguration.errorDisplayDuration,
execute: workItem
)
}
///
/// - Parameter id: ID
private func cancelErrorCleanup(for id: UUID) {
@@ -168,14 +109,14 @@ class APILoadingManager: ObservableObject {
// MARK: - Convenience Extensions
extension APILoadingManager {
/// 便 loading
/// - Parameters:
/// - shouldShowLoading: loading
/// - shouldShowError:
/// - operation:
/// - Returns:
func withLoading<T>(
@MainActor
func withLoading<T: Sendable>(
shouldShowLoading: Bool = true,
shouldShowError: Bool = true,
operation: @escaping () async throws -> T
@@ -184,7 +125,6 @@ extension APILoadingManager {
shouldShowLoading: shouldShowLoading,
shouldShowError: shouldShowError
)
do {
let result = try await operation()
finishLoading(loadingId)

View File

@@ -6,7 +6,7 @@ struct StringHashTest {
///
static func runTests() {
debugInfo("🧪 开始测试字符串哈希方法...")
debugInfoSync("🧪 开始测试字符串哈希方法...")
let testStrings = [
"hello world",
@@ -16,27 +16,27 @@ struct StringHashTest {
]
for testString in testStrings {
debugInfo("\n📝 测试字符串: \"\(testString)\"")
debugInfoSync("\n📝 测试字符串: \"\(testString)\"")
// MD5
let md5Result = testString.md5()
debugInfo(" MD5: \(md5Result)")
debugInfoSync(" MD5: \(md5Result)")
// SHA256 (iOS 13+)
if #available(iOS 13.0, *) {
let sha256Result = testString.sha256()
debugInfo(" SHA256: \(sha256Result)")
debugInfoSync(" SHA256: \(sha256Result)")
} else {
debugInfo(" SHA256: 不支持 (需要 iOS 13+)")
debugInfoSync(" SHA256: 不支持 (需要 iOS 13+)")
}
}
debugInfo("\n✅ 哈希方法测试完成")
debugInfoSync("\n✅ 哈希方法测试完成")
}
///
static func verifyKnownHashes() {
debugInfo("\n🔍 验证已知哈希值...")
debugInfoSync("\n🔍 验证已知哈希值...")
// "hello world" MD5 "5d41402abc4b2a76b9719d911017c592"
let testString = "hello world"
@@ -44,11 +44,11 @@ struct StringHashTest {
let actualMD5 = testString.md5()
if actualMD5 == expectedMD5 {
debugInfo("✅ MD5 验证通过: \(actualMD5)")
debugInfoSync("✅ MD5 验证通过: \(actualMD5)")
} else {
debugError("❌ MD5 验证失败:")
debugError(" 期望: \(expectedMD5)")
debugError(" 实际: \(actualMD5)")
debugErrorSync("❌ MD5 验证失败:")
debugErrorSync(" 期望: \(expectedMD5)")
debugErrorSync(" 实际: \(actualMD5)")
}
// SHA256
@@ -57,11 +57,11 @@ struct StringHashTest {
let actualSHA256 = testString.sha256()
if actualSHA256 == expectedSHA256 {
debugInfo("✅ SHA256 验证通过: \(actualSHA256)")
debugInfoSync("✅ SHA256 验证通过: \(actualSHA256)")
} else {
debugError("❌ SHA256 验证失败:")
debugError(" 期望: \(expectedSHA256)")
debugError(" 实际: \(actualSHA256)")
debugErrorSync("❌ SHA256 验证失败:")
debugErrorSync(" 期望: \(expectedSHA256)")
debugErrorSync(" 实际: \(actualSHA256)")
}
}
}
@@ -75,9 +75,9 @@ struct StringHashTest {
StringHashTest.verifyKnownHashes()
//
debugInfo("Test MD5:", "hello".md5())
debugInfoSync("Test MD5:", "hello".md5())
if #available(iOS 13.0, *) {
debugInfo("Test SHA256:", "hello".sha256())
debugInfoSync("Test SHA256:", "hello".sha256())
}
*/
*/

View File

@@ -67,11 +67,11 @@ struct FontManager {
///
static func printAllAvailableFonts() {
debugInfo("=== 所有可用字体 ===")
debugInfoSync("=== 所有可用字体 ===")
for font in getAllAvailableFonts() {
debugInfo(font)
debugInfoSync(font)
}
debugInfo("==================")
debugInfoSync("==================")
}
}

View File

@@ -8,6 +8,7 @@ import SwiftUI
/// -
/// -
/// - UserDefaults
@MainActor
class LocalizationManager: ObservableObject {
// MARK: -
@@ -42,9 +43,9 @@ class LocalizationManager: ObservableObject {
didSet {
do {
try KeychainManager.shared.storeString(currentLanguage.rawValue, forKey: "AppLanguage")
} catch {
debugError("❌ 保存语言设置失败: \(error)")
}
} catch {
debugErrorSync("❌ 保存语言设置失败: \(error)")
}
//
objectWillChange.send()
}
@@ -56,7 +57,7 @@ class LocalizationManager: ObservableObject {
do {
savedLanguage = try KeychainManager.shared.retrieveString(forKey: "AppLanguage")
} catch {
debugError("❌ 读取语言设置失败: \(error)")
debugErrorSync("❌ 读取语言设置失败: \(error)")
savedLanguage = nil
}
@@ -113,34 +114,26 @@ class LocalizationManager: ObservableObject {
}
// MARK: - SwiftUI Extensions
extension View {
///
/// - Parameter key: key
/// - Returns:
func localized(_ key: String) -> some View {
self.modifier(LocalizedTextModifier(key: key))
}
}
///
struct LocalizedTextModifier: ViewModifier {
let key: String
@ObservedObject private var localizationManager = LocalizationManager.shared
func body(content: Content) -> some View {
content
}
}
// extension View {
// ///
// /// - Parameter key: key
// /// - Returns:
// @MainActor
// func localized(_ key: String) -> some View {
// self.modifier(LocalizedTextModifier(key: key))
// }
// }
// MARK: - 便
extension String {
///
var localized: String {
return LocalizationManager.shared.localizedString(self)
}
///
func localized(arguments: CVarArg...) -> String {
return LocalizationManager.shared.localizedString(self, arguments: arguments)
}
}
// extension String {
// ///
// @MainActor
// var localized: String {
// return LocalizationManager.shared.localizedString(self)
// }
// ///
// @MainActor
// func localized(arguments: CVarArg...) -> String {
// return LocalizationManager.shared.localizedString(self, arguments: arguments)
// }
// }

View File

@@ -5,8 +5,8 @@ struct DESEncryptOCTest {
/// OC DES
static func testOCDESEncryption() {
debugInfo("🧪 开始测试 OC 版本的 DES 加密...")
debugInfo(String(repeating: "=", count: 50))
debugInfoSync("🧪 开始测试 OC 版本的 DES 加密...")
debugInfoSync(String(repeating: "=", count: 50))
let key = "1ea53d260ecf11e7b56e00163e046a26"
let testCases = [
@@ -19,25 +19,25 @@ struct DESEncryptOCTest {
for testCase in testCases {
if let encrypted = DESEncrypt.encryptUseDES(testCase, key: key) {
debugInfo("✅ 加密成功:")
debugInfo(" 原文: \"\(testCase)\"")
debugInfo(" 密文: \(encrypted)")
debugInfoSync("✅ 加密成功:")
debugInfoSync(" 原文: \"\(testCase)\"")
debugInfoSync(" 密文: \(encrypted)")
//
if let decrypted = DESEncrypt.decryptUseDES(encrypted, key: key) {
let isMatch = decrypted == testCase
debugInfo(" 解密: \"\(decrypted)\" \(isMatch ? "" : "")")
debugInfoSync(" 解密: \"\(decrypted)\" \(isMatch ? "" : "")")
} else {
debugError(" 解密: 失败 ❌")
debugErrorSync(" 解密: 失败 ❌")
}
} else {
debugError("❌ 加密失败: \"\(testCase)\"")
debugErrorSync("❌ 加密失败: \"\(testCase)\"")
}
debugInfo("")
debugInfoSync("")
}
debugInfo(String(repeating: "=", count: 50))
debugInfo("🏁 OC版本DES加密测试完成")
debugInfoSync(String(repeating: "=", count: 50))
debugInfoSync("🏁 OC版本DES加密测试完成")
}
}
@@ -48,4 +48,4 @@ extension DESEncryptOCTest {
DESEncryptOCTest.testOCDESEncryption()
}
}
#endif
#endif

View File

@@ -10,6 +10,7 @@ import Foundation
/// 2. Keychain
/// 3.
/// 4.
@MainActor
final class DataMigrationManager {
// MARK: -
@@ -54,23 +55,23 @@ final class DataMigrationManager {
///
/// - Returns:
func performMigration() -> MigrationResult {
debugInfo("🔄 开始检查数据迁移...")
debugInfoSync("🔄 开始检查数据迁移...")
//
if isMigrationCompleted() {
debugInfo("✅ 数据已经迁移过,跳过迁移")
debugInfoSync("✅ 数据已经迁移过,跳过迁移")
return .alreadyMigrated
}
//
let legacyData = collectLegacyData()
if legacyData.isEmpty {
debugInfo(" 没有发现需要迁移的数据")
debugInfoSync(" 没有发现需要迁移的数据")
markMigrationCompleted()
return .noDataToMigrate
}
debugInfo("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))")
debugInfoSync("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))")
do {
//
@@ -85,11 +86,11 @@ final class DataMigrationManager {
//
markMigrationCompleted()
debugInfo("✅ 数据迁移完成")
debugInfoSync("✅ 数据迁移完成")
return .completed
} catch {
debugError("❌ 数据迁移失败: \(error)")
debugErrorSync("❌ 数据迁移失败: \(error)")
return .failed(error)
}
}
@@ -157,9 +158,9 @@ final class DataMigrationManager {
do {
let accountModel = try JSONDecoder().decode(AccountModel.self, from: accountModelData)
try keychain.store(accountModel, forKey: "account_model")
debugInfo("✅ AccountModel 迁移成功")
debugInfoSync("✅ AccountModel 迁移成功")
} catch {
debugError("❌ AccountModel 迁移失败: \(error)")
debugErrorSync("❌ AccountModel 迁移失败: \(error)")
// AccountModel
try migrateAccountModelFromIndependentFields(legacyData)
}
@@ -173,9 +174,9 @@ final class DataMigrationManager {
do {
let userInfo = try JSONDecoder().decode(UserInfo.self, from: userInfoData)
try keychain.store(userInfo, forKey: "user_info")
debugInfo("✅ UserInfo 迁移成功")
debugInfoSync("✅ UserInfo 迁移成功")
} catch {
debugError("❌ UserInfo 迁移失败: \(error)")
debugErrorSync("❌ UserInfo 迁移失败: \(error)")
throw error
}
}
@@ -183,7 +184,7 @@ final class DataMigrationManager {
//
if let appLanguage = legacyData[LegacyStorageKeys.appLanguage] as? String {
try keychain.storeString(appLanguage, forKey: "AppLanguage")
debugInfo("✅ 语言设置迁移成功")
debugInfoSync("✅ 语言设置迁移成功")
}
}
@@ -191,7 +192,7 @@ final class DataMigrationManager {
private func migrateAccountModelFromIndependentFields(_ legacyData: [String: Any]) throws {
guard let userId = legacyData[LegacyStorageKeys.userId] as? String,
let accessToken = legacyData[LegacyStorageKeys.accessToken] as? String else {
debugInfo(" 没有足够的独立字段来重建 AccountModel")
debugInfoSync(" 没有足够的独立字段来重建 AccountModel")
return
}
@@ -208,7 +209,7 @@ final class DataMigrationManager {
)
try KeychainManager.shared.store(accountModel, forKey: "account_model")
debugInfo("✅ 从独立字段重建 AccountModel 成功")
debugInfoSync("✅ 从独立字段重建 AccountModel 成功")
}
///
@@ -240,7 +241,7 @@ final class DataMigrationManager {
}
}
debugInfo("✅ 迁移数据验证成功")
debugInfoSync("✅ 迁移数据验证成功")
}
///
@@ -249,11 +250,11 @@ final class DataMigrationManager {
for key in keys {
userDefaults.removeObject(forKey: key)
debugInfo("🗑️ 清理旧数据: \(key)")
debugInfoSync("🗑️ 清理旧数据: \(key)")
}
userDefaults.synchronize()
debugInfo("✅ 旧数据清理完成")
debugInfoSync("✅ 旧数据清理完成")
}
}
@@ -287,13 +288,13 @@ extension DataMigrationManager {
switch migrationResult {
case .completed:
debugInfo("🎉 应用启动时数据迁移完成")
debugInfoSync("🎉 应用启动时数据迁移完成")
case .alreadyMigrated:
break //
case .noDataToMigrate:
break //
case .failed(let error):
debugError("⚠️ 应用启动时数据迁移失败: \(error)")
debugErrorSync("⚠️ 应用启动时数据迁移失败: \(error)")
//
}
}
@@ -307,9 +308,9 @@ extension DataMigrationManager {
///
func debugPrintLegacyData() {
let legacyData = collectLegacyData()
debugInfo("🔍 旧版本数据:")
debugInfoSync("🔍 旧版本数据:")
for (key, value) in legacyData {
debugInfo(" - \(key): \(type(of: value))")
debugInfoSync(" - \(key): \(type(of: value))")
}
}
@@ -322,7 +323,7 @@ extension DataMigrationManager {
userDefaults.set("zh-Hans", forKey: LegacyStorageKeys.appLanguage)
userDefaults.synchronize()
debugInfo("🧪 已创建测试用的旧版本数据")
debugInfoSync("🧪 已创建测试用的旧版本数据")
}
///
@@ -331,7 +332,7 @@ extension DataMigrationManager {
do {
try KeychainManager.shared.clearAll()
} catch {
debugError("❌ 清除 Keychain 数据失败: \(error)")
debugErrorSync("❌ 清除 Keychain 数据失败: \(error)")
}
// UserDefaults
@@ -350,7 +351,7 @@ extension DataMigrationManager {
}
userDefaults.synchronize()
debugInfo("🧪 已清除所有迁移相关数据")
debugInfoSync("🧪 已清除所有迁移相关数据")
}
}
#endif
#endif

View File

@@ -12,10 +12,11 @@ import Security
/// -
/// - 线
/// - 访
@MainActor
final class KeychainManager {
// MARK: -
static let shared = KeychainManager()
@MainActor static let shared = KeychainManager()
private init() {}
// MARK: -
@@ -108,7 +109,7 @@ final class KeychainManager {
throw KeychainError.keychainOperationFailed(status)
}
debugInfo("🔐 Keychain 存储成功: \(key)")
debugInfoSync("🔐 Keychain 存储成功: \(key)")
}
/// Keychain Codable
@@ -137,7 +138,7 @@ final class KeychainManager {
// 4.
do {
let object = try JSONDecoder().decode(type, from: data)
debugInfo("🔐 Keychain 读取成功: \(key)")
debugInfoSync("🔐 Keychain 读取成功: \(key)")
return object
} catch {
throw KeychainError.decodingFailed(error)
@@ -176,7 +177,7 @@ final class KeychainManager {
switch status {
case errSecSuccess:
debugInfo("🔐 Keychain 更新成功: \(key)")
debugInfoSync("🔐 Keychain 更新成功: \(key)")
case errSecItemNotFound:
//
@@ -196,7 +197,7 @@ final class KeychainManager {
switch status {
case errSecSuccess:
debugInfo("🔐 Keychain 删除成功: \(key)")
debugInfoSync("🔐 Keychain 删除成功: \(key)")
case errSecItemNotFound:
//
@@ -231,7 +232,7 @@ final class KeychainManager {
switch status {
case errSecSuccess, errSecItemNotFound:
debugInfo("🔐 Keychain 清除完成")
debugInfoSync("🔐 Keychain 清除完成")
default:
throw KeychainError.keychainOperationFailed(status)
@@ -353,10 +354,10 @@ extension KeychainManager {
///
func debugPrintAllKeys() {
let keys = debugListAllKeys()
debugInfo("🔐 Keychain 中存储的键:")
debugInfoSync("🔐 Keychain 中存储的键:")
for key in keys {
debugInfo(" - \(key)")
debugInfoSync(" - \(key)")
}
}
}
#endif
#endif

View File

@@ -38,20 +38,20 @@ struct UserAgreementView: View {
// MARK: - Private Methods
private func createAttributedText() -> AttributedString {
var attributedString = AttributedString("login.agreement_policy".localized)
var attributedString = AttributedString(NSLocalizedString("login.agreement_policy", comment: ""))
//
attributedString.foregroundColor = Color(hex: 0x666666)
// ""
if let userServiceRange = attributedString.range(of: "login.agreement".localized) {
if let userServiceRange = attributedString.range(of: NSLocalizedString("login.agreement", comment: "")) {
attributedString[userServiceRange].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[userServiceRange].underlineStyle = .single
attributedString[userServiceRange].link = URL(string: "user-service-agreement")
}
// ""
if let privacyPolicyRange = attributedString.range(of: "login.policy".localized) {
if let privacyPolicyRange = attributedString.range(of: NSLocalizedString("login.policy", comment: "")) {
attributedString[privacyPolicyRange].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[privacyPolicyRange].underlineStyle = .single
attributedString[privacyPolicyRange].link = URL(string: "privacy-policy")
@@ -61,28 +61,28 @@ struct UserAgreementView: View {
}
}
#Preview {
VStack(spacing: 20) {
UserAgreementView(
isAgreed: .constant(true),
onUserServiceTapped: {
debugInfo("User Service Agreement tapped")
},
onPrivacyPolicyTapped: {
debugInfo("Privacy Policy tapped")
}
)
UserAgreementView(
isAgreed: .constant(true),
onUserServiceTapped: {
debugInfo("User Service Agreement tapped")
},
onPrivacyPolicyTapped: {
debugInfo("Privacy Policy tapped")
}
)
}
.padding()
.background(Color.gray.opacity(0.1))
}
//#Preview {
// VStack(spacing: 20) {
// UserAgreementView(
// isAgreed: .constant(true),
// onUserServiceTapped: {
// debugInfoSync("User Service Agreement tapped")
// },
// onPrivacyPolicyTapped: {
// debugInfoSync("Privacy Policy tapped")
// }
// )
//
// UserAgreementView(
// isAgreed: .constant(true),
// onUserServiceTapped: {
// debugInfoSync("User Service Agreement tapped")
// },
// onPrivacyPolicyTapped: {
// debugInfoSync("Privacy Policy tapped")
// }
// )
// }
// .padding()
// .background(Color.gray.opacity(0.1))
//}

View File

@@ -1,17 +1,13 @@
import SwiftUI
import ComposableArchitecture
// PhotosUI (iOS 16.0+)
#if canImport(PhotosUI)
import PhotosUI
#endif
struct CreateFeedView: View {
let store: StoreOf<CreateFeedFeature>
var body: some View {
WithPerceptionTracking {
NavigationView {
NavigationStack {
GeometryReader { geometry in
ZStack {
//
@@ -59,7 +55,7 @@ struct CreateFeedView: View {
Text("\(store.characterCount)/500")
.font(.system(size: 12))
.foregroundColor(
store.isCharacterLimitExceeded ? .red : .white.opacity(0.6)
store.characterCount > 500 ? .red : .white.opacity(0.6)
)
}
}
@@ -69,32 +65,17 @@ struct CreateFeedView: View {
//
VStack(alignment: .leading, spacing: 12) {
if !store.processedImages.isEmpty || store.canAddMoreImages {
if #available(iOS 16.0, *) {
#if canImport(PhotosUI)
ModernImageSelectionGrid(
images: store.processedImages,
selectedItems: store.selectedImages,
canAddMore: store.canAddMoreImages,
onItemsChanged: { items in
store.send(.photosPickerItemsChanged(items))
},
onRemoveImage: { index in
store.send(.removeImage(index))
}
)
#endif
} else {
LegacyImageSelectionGrid(
images: store.processedImages,
canAddMore: store.canAddMoreImages,
onAddImage: {
store.send(.showImagePicker)
},
onRemoveImage: { index in
store.send(.removeImage(index))
}
)
}
ModernImageSelectionGrid(
images: store.processedImages,
selectedItems: store.selectedImages,
canAddMore: store.canAddMoreImages,
onItemsChanged: { items in
store.send(.photosPickerItemsChanged(items))
},
onRemoveImage: { index in
store.send(.removeImage(index))
}
)
}
}
.padding(.horizontal, 20)
@@ -112,7 +93,7 @@ struct CreateFeedView: View {
}
//
if let error = store.error {
if let error = store.errorMessage {
Text(error)
.font(.system(size: 14))
.foregroundColor(.red)
@@ -133,7 +114,7 @@ struct CreateFeedView: View {
store.send(.publishButtonTapped)
}) {
HStack {
if store.isPublishing {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
@@ -159,8 +140,8 @@ struct CreateFeedView: View {
)
)
.cornerRadius(25)
.disabled(store.isPublishing || (!store.isContentValid && !store.isLoading))
.opacity(store.isPublishing || (!store.isContentValid && !store.isLoading) ? 0.6 : 1.0)
.disabled(store.isLoading || !store.canPublish)
.opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
}
.padding(.horizontal, 20)
.padding(.bottom, geometry.safeAreaInsets.bottom + 20)
@@ -182,27 +163,17 @@ struct CreateFeedView: View {
Button("发布") {
store.send(.publishButtonTapped)
}
.foregroundColor(store.isContentValid ? .white : .white.opacity(0.5))
.disabled(!store.isContentValid || store.isPublishing)
.foregroundColor(store.canPublish ? .white : .white.opacity(0.5))
.disabled(!store.canPublish || store.isLoading)
}
}
}
.preferredColorScheme(.dark)
.sheet(isPresented: .init(
get: { store.showingImagePicker },
set: { _ in store.send(.hideImagePicker) }
)) {
ImagePickerView { image in
store.send(.imageSelected(image))
}
}
}
}
}
// MARK: - iOS 16+
#if canImport(PhotosUI)
@available(iOS 16.0, *)
struct ModernImageSelectionGrid: View {
let images: [UIImage]
let selectedItems: [PhotosPickerItem]
@@ -261,98 +232,6 @@ struct ModernImageSelectionGrid: View {
}
}
}
#endif
// MARK: - iOS 15
struct LegacyImageSelectionGrid: View {
let images: [UIImage]
let canAddMore: Bool
let onAddImage: () -> Void
let onRemoveImage: (Int) -> Void
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
var body: some View {
LazyVGrid(columns: columns, spacing: 8) {
//
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 100)
.clipped()
.cornerRadius(8)
//
Button(action: {
onRemoveImage(index)
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 20))
.foregroundColor(.white)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
.padding(4)
}
}
//
if canAddMore {
Button(action: onAddImage) {
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.1))
.frame(height: 100)
.overlay(
Image(systemName: "plus")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
)
}
}
}
}
}
// MARK: - UIImagePicker
struct ImagePickerView: UIViewControllerRepresentable {
let onImageSelected: (UIImage) -> Void
@Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
picker.allowsEditing = false
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePickerView
init(_ parent: ImagePickerView) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
parent.onImageSelected(image)
}
parent.presentationMode.wrappedValue.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.presentationMode.wrappedValue.dismiss()
}
}
}
// MARK: -
#Preview {

View File

@@ -31,7 +31,7 @@ struct EMailLoginView: View {
} else if codeCountdown > 0 {
return "\(codeCountdown)S"
} else {
return "email_login.get_code".localized
return NSLocalizedString("email_login.get_code", comment: "")
}
}
@@ -70,7 +70,7 @@ struct EMailLoginView: View {
.frame(height: 60)
//
Text("email_login.title".localized)
Text(NSLocalizedString("email_login.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
@@ -89,7 +89,7 @@ struct EMailLoginView: View {
TextField("", text: $email)
.placeholder(when: email.isEmpty) {
Text("placeholder.enter_email".localized)
Text(NSLocalizedString("placeholder.enter_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -114,7 +114,7 @@ struct EMailLoginView: View {
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text("placeholder.enter_verification_code".localized)
Text(NSLocalizedString("placeholder.enter_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -178,7 +178,7 @@ struct EMailLoginView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? "email_login.logging_in".localized : "email_login.login_button".localized)
Text(store.isLoading ? NSLocalizedString("email_login.logging_in", comment: "") : NSLocalizedString("email_login.login_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}

View File

@@ -1,6 +1,64 @@
import SwiftUI
import ComposableArchitecture
struct FeedTopBarView: View {
let store: StoreOf<FeedFeature>
var body: some View {
HStack {
Spacer()
Text("Enjoy your Life Time")
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Spacer()
Button(action: {
store.send(.showCreateFeed)
}) {
Image("add icon")
.frame(width: 36, height: 36)
}
}
.padding(.horizontal, 20)
}
}
struct FeedMomentsListView: View {
let store: StoreOf<FeedFeature>
var body: some View {
WithPerceptionTracking {
LazyVStack(spacing: 16) {
if store.moments.isEmpty {
VStack(spacing: 12) {
Image(systemName: "heart.text.square")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
Text("暂无动态内容")
.font(.system(size: 16))
.foregroundColor(.white.opacity(0.8))
if let error = store.error {
Text("错误: \(error)")
.font(.system(size: 12))
.foregroundColor(.red.opacity(0.8))
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
}
}
.padding(.top, 40)
} else {
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
OptimizedDynamicCardView(
moment: moment,
allMoments: store.moments,
currentIndex: index
)
}
}
}
}
.padding(.horizontal, 16)
.padding(.top, 30)
}
}
struct FeedView: View {
let store: StoreOf<FeedFeature>
@@ -9,80 +67,18 @@ struct FeedView: View {
GeometryReader { geometry in
ScrollView {
VStack(spacing: 20) {
// -
HStack {
Spacer()
//
Text("Enjoy your Life Time")
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Spacer()
//
Button(action: {
store.send(.showCreateFeed)
}) {
Image("add icon")
.frame(width: 36, height: 36)
}
}
.padding(.horizontal, 20)
//
FeedTopBarView(store: store)
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundColor(.red)
.padding(.top, 40)
//
Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.")
.font(.system(size: 16))
.multilineTextAlignment(.center)
.foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30)
.padding(.top, 20)
// - 使store
WithPerceptionTracking {
LazyVStack(spacing: 16) {
if store.moments.isEmpty {
//
VStack(spacing: 12) {
Image(systemName: "heart.text.square")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
Text("暂无动态内容")
.font(.system(size: 16))
.foregroundColor(.white.opacity(0.8))
if let error = store.error {
Text("错误: \(error)")
.font(.system(size: 12))
.foregroundColor(.red.opacity(0.8))
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
}
}
.padding(.top, 40)
} else {
//
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
OptimizedDynamicCardView(
moment: moment,
allMoments: store.moments,
currentIndex: index
)
}
}
}
}
.padding(.horizontal, 16)
.padding(.top, 30)
// - 使store
FeedMomentsListView(store: store)
if store.isLoading {
HStack {
ProgressView()
@@ -93,8 +89,6 @@ struct FeedView: View {
}
.padding(.top, 20)
}
//
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
}
}
@@ -108,14 +102,8 @@ struct FeedView: View {
get: { store.isShowingCreateFeed },
set: { _ in store.send(.dismissCreateFeed) }
)) {
CreateFeedView(
store: Store(initialState: CreateFeedFeature.State()) {
CreateFeedFeature()
}
)
.onDisappear {
// CreateFeedView
//
if let createFeedStore = store.scope(state: \.createFeedState, action: \.createFeed) {
CreateFeedView(store: createFeedStore)
}
}
}

View File

@@ -46,7 +46,7 @@ struct IDLoginView: View {
.frame(height: 60)
//
Text("id_login.title".localized)
Text(NSLocalizedString("id_login.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
@@ -65,7 +65,7 @@ struct IDLoginView: View {
TextField("", text: $userID) // 使SwiftUI
.placeholder(when: userID.isEmpty) {
Text("placeholder.enter_id".localized)
Text(NSLocalizedString("placeholder.enter_id", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -88,7 +88,7 @@ struct IDLoginView: View {
if isPasswordVisible {
TextField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized)
Text(NSLocalizedString("placeholder.enter_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -96,7 +96,7 @@ struct IDLoginView: View {
} else {
SecureField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized)
Text(NSLocalizedString("placeholder.enter_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -122,7 +122,7 @@ struct IDLoginView: View {
Button(action: {
showRecoverPassword = true
}) {
Text("id_login.forgot_password".localized)
Text(NSLocalizedString("id_login.forgot_password", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
@@ -156,7 +156,7 @@ struct IDLoginView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? "id_login.logging_in".localized : "id_login.login_button".localized)
Text(store.isLoading ? NSLocalizedString("id_login.logging_in", comment: "") : NSLocalizedString("id_login.login_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
@@ -207,7 +207,7 @@ struct IDLoginView: View {
#if DEBUG
//
debugInfo("🐛 Debug模式: 已移除硬编码测试凭据")
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
#endif
}
}

View File

@@ -10,7 +10,7 @@ struct LanguageSettingsView: View {
}
var body: some View {
NavigationView {
NavigationStack {
List {
Section {
ForEach(LocalizationManager.SupportedLanguage.allCases, id: \.rawValue) { language in
@@ -47,13 +47,6 @@ struct LanguageSettingsView: View {
.navigationTitle("语言设置 / Language")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("返回 / Back") {
isPresented = false
}
}
}
}
}
}

View File

@@ -3,7 +3,7 @@ import ComposableArchitecture
// PreferenceKey
struct ImageHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
@@ -21,7 +21,7 @@ struct LoginView: View {
@State private var showEmailLogin = false //
var body: some View {
NavigationView {
NavigationStack {
GeometryReader { geometry in
ZStack {
// 使 splash
@@ -44,7 +44,7 @@ struct LoginView: View {
)
// E-PARTI "top"20
HStack {
Text("login.app_title".localized)
Text(NSLocalizedString("login.app_title", comment: ""))
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white)
.padding(.leading, 20)
@@ -78,7 +78,7 @@ struct LoginView: View {
LoginButton(
iconName: "person.circle.fill",
iconColor: .green,
title: "login.id_login".localized
title: NSLocalizedString("login.id_login", comment: "")
) {
showIDLogin = true // SwiftUI
}
@@ -86,7 +86,7 @@ struct LoginView: View {
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: "login.email_login".localized
title: NSLocalizedString("login.email_login", comment: "")
) {
showEmailLogin = true //
}
@@ -153,7 +153,6 @@ struct LoginView: View {
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
.sheet(isPresented: $showLanguageSettings) {
LanguageSettingsView(isPresented: $showLanguageSettings)
}

View File

@@ -78,7 +78,7 @@ struct MeView: View {
.alert("确认退出", isPresented: $showLogoutConfirmation) {
Button("取消", role: .cancel) { }
Button("退出", role: .destructive) {
performLogout()
Task { await performLogout() }
}
} message: {
Text("确定要退出登录吗?")
@@ -86,16 +86,13 @@ struct MeView: View {
}
// MARK: - 退
private func performLogout() {
debugInfo("🔓 开始执行退出登录...")
private func performLogout() async {
debugInfoSync("🔓 开始执行退出登录...")
// keychain
UserInfoManager.clearAllAuthenticationData()
await UserInfoManager.clearAllAuthenticationData()
// window root login view
NotificationCenter.default.post(name: .homeLogout, object: nil)
debugInfo("✅ 退出登录完成")
debugInfoSync("✅ 退出登录完成")
}
}

View File

@@ -1,5 +1,6 @@
import SwiftUI
import ComposableArchitecture
import Combine
struct RecoverPasswordView: View {
let store: StoreOf<RecoverPasswordFeature>
@@ -13,7 +14,7 @@ struct RecoverPasswordView: View {
//
@State private var countdown: Int = 0
@State private var countdownTimer: Timer?
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
//
private var isConfirmButtonEnabled: Bool {
@@ -32,7 +33,7 @@ struct RecoverPasswordView: View {
} else if countdown > 0 {
return "\(countdown)s"
} else {
return "recover_password.get_code".localized
return NSLocalizedString("recover_password.get_code", comment: "")
}
}
@@ -66,7 +67,7 @@ struct RecoverPasswordView: View {
.frame(height: 60)
//
Text("recover_password.title".localized)
Text(NSLocalizedString("recover_password.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
@@ -85,7 +86,7 @@ struct RecoverPasswordView: View {
TextField("", text: $email)
.placeholder(when: email.isEmpty) {
Text("recover_password.placeholder_email".localized)
Text(NSLocalizedString("recover_password.placeholder_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -108,7 +109,7 @@ struct RecoverPasswordView: View {
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text("recover_password.placeholder_verification_code".localized)
Text(NSLocalizedString("recover_password.placeholder_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -158,7 +159,7 @@ struct RecoverPasswordView: View {
if isNewPasswordVisible {
TextField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text("recover_password.placeholder_new_password".localized)
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -166,7 +167,7 @@ struct RecoverPasswordView: View {
} else {
SecureField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text("recover_password.placeholder_new_password".localized)
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -211,7 +212,7 @@ struct RecoverPasswordView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isResetLoading ? "recover_password.resetting".localized : "recover_password.confirm_button".localized)
Text(store.isResetLoading ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
@@ -244,15 +245,13 @@ struct RecoverPasswordView: View {
newPassword = ""
isNewPasswordVisible = false
countdown = 0
stopCountdown()
#if DEBUG
email = "exzero@126.com"
store.send(.emailChanged(email))
#endif
}
.onDisappear {
stopCountdown()
countdown = 0
}
.onChange(of: email) { newEmail in
store.send(.emailChanged(newEmail))
@@ -275,24 +274,20 @@ struct RecoverPasswordView: View {
onBack()
}
}
.onReceive(timer) { _ in
if countdown > 0 {
countdown -= 1
}
}
}
// MARK: - Private Methods
private func startCountdown() {
countdown = 60
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if countdown > 0 {
countdown -= 1
} else {
stopCountdown()
}
}
}
private func stopCountdown() {
countdownTimer?.invalidate()
countdownTimer = nil
countdown = 0
}
}

View File

@@ -21,7 +21,7 @@ struct yanaApp: App {
}
#endif
debugInfo("🛠 原生URLSession测试开始")
debugInfoSync("🛠 原生URLSession测试开始")
}
var body: some Scene {

View File

@@ -163,10 +163,10 @@ final class yanaAPITests: XCTestCase {
XCTAssertEqual(accountModel?.uid, "3184", "真实API数据的UID应该正确")
XCTAssertTrue(accountModel?.hasValidAuthentication ?? false, "真实API数据应该有有效认证")
debugInfo("✅ 真实API数据测试通过")
debugInfo(" UID: \(accountModel?.uid ?? "nil")")
debugInfo(" Access Token存在: \(accountModel?.accessToken != nil)")
debugInfo(" Token类型: \(accountModel?.tokenType ?? "nil")")
debugInfoSync("✅ 真实API数据测试通过")
debugInfoSync(" UID: \(accountModel?.uid ?? "nil")")
debugInfoSync(" Access Token存在: \(accountModel?.accessToken != nil)")
debugInfoSync(" Token类型: \(accountModel?.tokenType ?? "nil")")
} catch {
XCTFail("解析真实API数据失败: \(error)")