feat: 更新API相关逻辑及视图结构

- 在Info.plist中新增API签名密钥配置。
- 将Splash视图替换为SplashV2,优化启动逻辑和用户体验。
- 更新API请求中的User-Agent逻辑,使用UserAgentProvider提供的动态值。
- 在APILogger中添加敏感信息脱敏处理,增强安全性。
- 新增CreateFeedPage视图,支持用户发布动态功能。
- 更新MainPage和Splash视图的导航逻辑,整合统一的AppRoute管理。
- 移除冗余的SplashFeature视图,提升代码整洁性和可维护性。
This commit is contained in:
edwinQQQ
2025-09-17 16:37:21 +08:00
parent c57bde4525
commit 8b4eb9cb7e
23 changed files with 640 additions and 646 deletions

View File

@@ -16,7 +16,7 @@
| 环境 | 地址 | 说明 | | 环境 | 地址 | 说明 |
|------|------|------| |------|------|------|
| 生产环境 | `https://api.epartylive.com` | 正式服务器 | | 生产环境 | `https://api.epartylive.com` | 正式服务器 |
| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 | | 测试环境 | `http://beta.api.pekolive.com` | 开发测试服务器 |
| 图片服务 | `https://image.hfighting.com` | 静态资源服务器 | | 图片服务 | `https://image.hfighting.com` | 静态资源服务器 |
**环境切换机制:** **环境切换机制:**

View File

@@ -102,7 +102,7 @@ struct APIConfiguration {
"Accept-Encoding": "gzip, br", "Accept-Encoding": "gzip, br",
"Accept-Language": Locale.current.language.languageCode?.identifier ?? "en", "Accept-Language": Locale.current.language.languageCode?.identifier ?? "en",
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0", "App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 17+; Scale/2.00)" "User-Agent": await UserAgentProvider.userAgent()
] ]
// headers // headers
let authStatus = await UserInfoManager.checkAuthenticationStatus() let authStatus = await UserInfoManager.checkAuthenticationStatus()

View File

@@ -1,7 +1,6 @@
import Foundation import Foundation
// MARK: - API Logger // MARK: - API Logger
@MainActor
class APILogger { class APILogger {
enum LogLevel { enum LogLevel {
case none case none
@@ -10,19 +9,82 @@ class APILogger {
} }
#if DEBUG #if DEBUG
static var logLevel: LogLevel = .detailed static let logLevel: LogLevel = .detailed
#else #else
static var logLevel: LogLevel = .none static let logLevel: LogLevel = .none
#endif #endif
private static let logQueue = DispatchQueue(label: "com.yana.api.logger", qos: .utility)
private static let dateFormatter: DateFormatter = { private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS" formatter.dateFormat = "HH:mm:ss.SSS"
return formatter return formatter
}() }()
// MARK: - Redaction
///
private static let sensitiveKeys: Set<String> = [
"authorization", "token", "access-token", "access_token", "refresh-token", "refresh_token",
"password", "passwd", "secret", "pub_ticket", "ticket", "set-cookie", "cookie"
]
///
private static func maskString(_ value: String, keepPrefix: Int = 3, keepSuffix: Int = 2) -> String {
guard !value.isEmpty else { return value }
if value.count <= keepPrefix + keepSuffix { return String(repeating: "*", count: value.count) }
let start = value.startIndex
let prefixEnd = value.index(start, offsetBy: keepPrefix)
let suffixStart = value.index(value.endIndex, offsetBy: -keepSuffix)
let prefix = value[start..<prefixEnd]
let suffix = value[suffixStart..<value.endIndex]
return String(prefix) + String(repeating: "*", count: max(0, value.distance(from: prefixEnd, to: suffixStart))) + String(suffix)
}
/// headers
private static func maskHeaders(_ headers: [String: String]) -> [String: String] {
var masked: [String: String] = [:]
for (key, value) in headers {
if sensitiveKeys.contains(key.lowercased()) {
masked[key] = maskString(value)
} else {
masked[key] = value
}
}
return masked
}
/// JSON
private static func redactJSONObject(_ obj: Any) -> Any {
if let dict = obj as? [String: Any] {
var newDict: [String: Any] = [:]
for (k, v) in dict {
if sensitiveKeys.contains(k.lowercased()) {
if let str = v as? String { newDict[k] = maskString(str) }
else { newDict[k] = "<redacted>" }
} else {
newDict[k] = redactJSONObject(v)
}
}
return newDict
} else if let arr = obj as? [Any] {
return arr.map { redactJSONObject($0) }
} else {
return obj
}
}
/// Data Pretty JSON
private static func maskedBodyString(from body: Data?) -> String {
guard let body = body, !body.isEmpty else { return "No body" }
if let json = try? JSONSerialization.jsonObject(with: body, options: []) {
let redacted = redactJSONObject(json)
if let pretty = try? JSONSerialization.data(withJSONObject: redacted, options: [.prettyPrinted]),
let prettyString = String(data: pretty, encoding: .utf8) {
return prettyString
}
}
return "<non-json body> (\(body.count) bytes)"
}
// MARK: - Request Logging // MARK: - Request Logging
@MainActor static func logRequest<T: APIRequestProtocol>( static func logRequest<T: APIRequestProtocol>(
_ request: T, _ request: T,
url: URL, url: URL,
body: Data?, body: Data?,
@@ -34,85 +96,70 @@ class APILogger {
return return
#endif #endif
let timestamp = dateFormatter.string(from: Date()) logQueue.async {
let timestamp = dateFormatter.string(from: Date())
print("\n🚀 [API Request] [\(timestamp)] ==================") debugInfoSync("\n🚀 [API Request] [\(timestamp)] ==================")
print("📍 Endpoint: \(request.endpoint)") debugInfoSync("📍 Endpoint: \(request.endpoint)")
print("🔗 Full URL: \(url.absoluteString)") debugInfoSync("🔗 Full URL: \(url.absoluteString)")
print("📝 Method: \(request.method.rawValue)") debugInfoSync("📝 Method: \(request.method.rawValue)")
print("⏰ Timeout: \(request.timeout)s") debugInfoSync("⏰ Timeout: \(request.timeout)s")
// headers headers headers // headers headers headers
if let headers = finalHeaders, !headers.isEmpty { if let headers = finalHeaders, !headers.isEmpty {
if logLevel == .detailed { if logLevel == .detailed {
print("📋 Final Headers (包括默认 + 自定义):") debugInfoSync("📋 Final Headers (包括默认 + 自定义):")
for (key, value) in headers.sorted(by: { $0.key < $1.key }) { let masked = maskHeaders(headers)
print(" \(key): \(value)") for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
} debugInfoSync(" \(key): \(value)")
} else if logLevel == .basic { }
print("📋 Headers: \(headers.count) 个 headers") } else if logLevel == .basic {
// headers debugInfoSync("📋 Headers: \(headers.count) 个 headers")
let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"] // headers
for key in importantHeaders { let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"]
if let value = headers[key] { let masked = maskHeaders(headers)
print(" \(key): \(value)") for key in importantHeaders {
if let value = masked[key] {
debugInfoSync(" \(key): \(value)")
}
} }
} }
} } else if let customHeaders = request.headers, !customHeaders.isEmpty {
} else if let customHeaders = request.headers, !customHeaders.isEmpty { debugInfoSync("📋 Custom Headers:")
print("📋 Custom Headers:") let masked = maskHeaders(customHeaders)
for (key, value) in customHeaders.sorted(by: { $0.key < $1.key }) { for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
print(" \(key): \(value)") debugInfoSync(" \(key): \(value)")
}
} else {
print("📋 Headers: 使用默认 headers")
}
if let queryParams = request.queryParameters, !queryParams.isEmpty {
print("🔍 Query Parameters:")
for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) {
print(" \(key): \(value)")
}
}
if logLevel == .detailed {
if let body = body {
print("📦 Request Body (\(body.count) bytes):")
if let jsonObject = try? JSONSerialization.jsonObject(with: body, options: []),
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
let prettyString = String(data: prettyData, encoding: .utf8) {
print(prettyString)
} else if let rawString = String(data: body, encoding: .utf8) {
print(rawString)
} else {
print("Binary data")
} }
} else { } else {
print("📦 Request Body: No body") debugInfoSync("📋 Headers: 使用默认 headers")
} }
// if let queryParams = request.queryParameters, !queryParams.isEmpty {
if request.includeBaseParameters { debugInfoSync("🔍 Query Parameters:")
print("📱 Base Parameters: 自动注入设备和应用信息") for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) {
let baseParams = BaseRequest() let masked = sensitiveKeys.contains(key.lowercased()) ? maskString(value) : value
print(" Device: \(baseParams.model), OS: \(baseParams.os) \(baseParams.osVersion)") debugInfoSync(" \(key): \(masked)")
print(" App: \(baseParams.app) v\(baseParams.appVersion)") }
print(" Language: \(baseParams.acceptLanguage)")
}
} else if logLevel == .basic {
if let body = body {
print("📦 Request Body: \(formatBytes(body.count))")
} else {
print("📦 Request Body: No body")
} }
// if logLevel == .detailed {
if request.includeBaseParameters { let pretty = maskedBodyString(from: body)
print("📱 Base Parameters: 已自动注入") debugInfoSync("📦 Request Body: \n\(pretty)")
// actor UIKit
if request.includeBaseParameters {
debugInfoSync("📱 Base Parameters: 已自动注入")
}
} else if logLevel == .basic {
let size = body?.count ?? 0
debugInfoSync("📦 Request Body: \(formatBytes(size))")
//
if request.includeBaseParameters {
debugInfoSync("📱 Base Parameters: 已自动注入")
}
} }
debugInfoSync("=====================================")
} }
print("=====================================")
} }
// MARK: - Response Logging // MARK: - Response Logging
@@ -123,36 +170,41 @@ class APILogger {
return return
#endif #endif
let timestamp = dateFormatter.string(from: Date()) logQueue.async {
let statusEmoji = response.statusCode < 400 ? "" : "" let timestamp = dateFormatter.string(from: Date())
let statusEmoji = response.statusCode < 400 ? "" : ""
print("\n\(statusEmoji) [API Response] [\(timestamp)] ===================") debugInfoSync("\n\(statusEmoji) [API Response] [\(timestamp)] ===================")
print("⏱️ Duration: \(String(format: "%.3f", duration))s") debugInfoSync("⏱️ Duration: \(String(format: "%.3f", duration))s")
print("📊 Status Code: \(response.statusCode)") debugInfoSync("📊 Status Code: \(response.statusCode)")
print("🔗 URL: \(response.url?.absoluteString ?? "Unknown")") debugInfoSync("🔗 URL: \(response.url?.absoluteString ?? "Unknown")")
print("📏 Data Size: \(formatBytes(data.count))") debugInfoSync("📏 Data Size: \(formatBytes(data.count))")
if logLevel == .detailed {
print("📋 Response Headers:")
for (key, value) in response.allHeaderFields.sorted(by: { "\($0.key)" < "\($1.key)" }) {
print(" \(key): \(value)")
}
print("📦 Response Data:") if logLevel == .detailed {
if data.isEmpty { debugInfoSync("📋 Response Headers:")
print(" Empty response") // headers [String:String]
} else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), var headers: [String: String] = [:]
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted), for (k, v) in response.allHeaderFields { headers["\(k)"] = "\(v)" }
let prettyString = String(data: prettyData, encoding: .utf8) { let masked = maskHeaders(headers)
print(prettyString) for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
} else if let rawString = String(data: data, encoding: .utf8) { debugInfoSync(" \(key): \(value)")
print(rawString) }
} else {
print(" Binary data (\(data.count) bytes)") debugInfoSync("📦 Response Data:")
if data.isEmpty {
debugInfoSync(" Empty response")
} else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let prettyData = try? JSONSerialization.data(withJSONObject: redactJSONObject(jsonObject), options: .prettyPrinted),
let prettyString = String(data: prettyData, encoding: .utf8) {
debugInfoSync(prettyString)
} else if let _ = String(data: data, encoding: .utf8) {
// JSON
debugInfoSync("<non-json text> (\(data.count) bytes)")
} else {
debugInfoSync(" Binary data (\(data.count) bytes)")
}
} }
debugInfoSync("=====================================")
} }
print("=====================================")
} }
// MARK: - Error Logging // MARK: - Error Logging
@@ -163,43 +215,43 @@ class APILogger {
return return
#endif #endif
let timestamp = dateFormatter.string(from: Date()) logQueue.async {
let timestamp = dateFormatter.string(from: Date())
print("\n❌ [API Error] [\(timestamp)] ======================") debugErrorSync("\n❌ [API Error] [\(timestamp)] ======================")
print("⏱️ Duration: \(String(format: "%.3f", duration))s") debugErrorSync("⏱️ Duration: \(String(format: "%.3f", duration))s")
if let url = url { if let url = url {
print("🔗 URL: \(url.absoluteString)") debugErrorSync("🔗 URL: \(url.absoluteString)")
}
if let apiError = error as? APIError {
print("🚨 API Error: \(apiError.localizedDescription)")
} else {
print("🚨 System Error: \(error.localizedDescription)")
}
if logLevel == .detailed {
if let urlError = error as? URLError {
print("🔍 URLError Code: \(urlError.code.rawValue)")
print("🔍 URLError Localized: \(urlError.localizedDescription)")
//
switch urlError.code {
case .timedOut:
print("💡 建议:检查网络连接或增加超时时间")
case .notConnectedToInternet:
print("💡 建议:检查网络连接")
case .cannotConnectToHost:
print("💡 建议:检查服务器地址和端口")
case .resourceUnavailable:
print("💡 建议:检查 API 端点是否正确")
default:
break
}
} }
print("🔍 Full Error: \(error)")
if let apiError = error as? APIError {
debugErrorSync("🚨 API Error: \(apiError.localizedDescription)")
} else {
debugErrorSync("🚨 System Error: \(error.localizedDescription)")
}
if logLevel == .detailed {
if let urlError = error as? URLError {
debugInfoSync("🔍 URLError Code: \(urlError.code.rawValue)")
debugInfoSync("🔍 URLError Localized: \(urlError.localizedDescription)")
//
switch urlError.code {
case .timedOut:
debugWarnSync("💡 建议:检查网络连接或增加超时时间")
case .notConnectedToInternet:
debugWarnSync("💡 建议:检查网络连接")
case .cannotConnectToHost:
debugWarnSync("💡 建议:检查服务器地址和端口")
case .resourceUnavailable:
debugWarnSync("💡 建议:检查 API 端点是否正确")
default:
break
}
}
debugInfoSync("🔍 Full Error: \(error)")
}
debugErrorSync("=====================================\n")
} }
print("=====================================\n")
} }
// MARK: - Decoded Response Logging // MARK: - Decoded Response Logging
@@ -210,9 +262,11 @@ class APILogger {
return return
#endif #endif
let timestamp = dateFormatter.string(from: Date()) logQueue.async {
print("🎯 [Decoded Response] [\(timestamp)] Type: \(type)") let timestamp = dateFormatter.string(from: Date())
print("=====================================\n") debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)")
debugInfoSync("=====================================\n")
}
} }
// MARK: - Helper Methods // MARK: - Helper Methods
@@ -231,10 +285,12 @@ class APILogger {
return return
#endif #endif
let timestamp = dateFormatter.string(from: Date()) logQueue.async {
print("\n⚠️ [Performance Warning] [\(timestamp)] ============") let timestamp = dateFormatter.string(from: Date())
print("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)") debugWarnSync("\n⚠️ [Performance Warning] [\(timestamp)] ============")
print("💡 建议:检查网络条件或优化 API 响应") debugWarnSync("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)")
print("================================================\n") debugWarnSync("💡 建议:检查网络条件或优化 API 响应")
debugWarnSync("================================================\n")
}
} }
} }

View File

@@ -1,5 +1,4 @@
import Foundation import Foundation
import ComposableArchitecture
// MARK: - HTTP Method // MARK: - HTTP Method
@@ -205,8 +204,9 @@ struct BaseRequest: Codable {
"\(key)=\(String(describing: filteredParams[key] ?? ""))" "\(key)=\(String(describing: filteredParams[key] ?? ""))"
}.joined(separator: "&") }.joined(separator: "&")
// 4. // 4.
let keyString = "key=rpbs6us1m8r2j9g6u06ff2bo18orwaya" let key = SigningKeyProvider.signingKey()
let keyString = "key=\(key)"
let finalString = paramString.isEmpty ? keyString : "\(paramString)&\(keyString)" let finalString = paramString.isEmpty ? keyString : "\(paramString)&\(keyString)"
// 5. MD5 // 5. MD5
@@ -217,9 +217,8 @@ struct BaseRequest: Codable {
// MARK: - Network Type Detector // MARK: - Network Type Detector
struct NetworkTypeDetector { struct NetworkTypeDetector {
static func getCurrentNetworkType() -> Int { static func getCurrentNetworkType() -> Int {
// WiFi = 2, = 1 // WiFi = 2, = 1, / = 0
// return NetworkMonitor.shared.currentType
return 2 //
} }
} }
@@ -238,7 +237,6 @@ struct CarrierInfoManager {
// MARK: - User Info Manager (for Headers) // MARK: - User Info Manager (for Headers)
struct UserInfoManager { struct UserInfoManager {
@MainActor
private static let keychain = KeychainManager.shared private static let keychain = KeychainManager.shared
// MARK: - Storage Keys // MARK: - Storage Keys
@@ -287,7 +285,7 @@ struct UserInfoManager {
// MARK: - User Info Management // MARK: - User Info Management
static func saveUserInfo(_ userInfo: UserInfo) async { static func saveUserInfo(_ userInfo: UserInfo) async {
do { do {
try await keychain.store(userInfo, forKey: StorageKeys.userInfo) try keychain.store(userInfo, forKey: StorageKeys.userInfo)
await cacheActor.setUserInfo(userInfo) await cacheActor.setUserInfo(userInfo)
debugInfoSync("💾 保存用户信息成功") debugInfoSync("💾 保存用户信息成功")
} catch { } catch {
@@ -302,7 +300,7 @@ struct UserInfoManager {
} }
// Keychain // Keychain
do { do {
let userInfo = try await keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo) let userInfo = try keychain.retrieve(UserInfo.self, forKey: StorageKeys.userInfo)
await cacheActor.setUserInfo(userInfo) await cacheActor.setUserInfo(userInfo)
return userInfo return userInfo
} catch { } catch {
@@ -377,7 +375,7 @@ struct UserInfoManager {
/// - Parameter accountModel: /// - Parameter accountModel:
static func saveAccountModel(_ accountModel: AccountModel) async { static func saveAccountModel(_ accountModel: AccountModel) async {
do { do {
try await keychain.store(accountModel, forKey: StorageKeys.accountModel) try keychain.store(accountModel, forKey: StorageKeys.accountModel)
await cacheActor.setAccountModel(accountModel) await cacheActor.setAccountModel(accountModel)
// ticket // ticket
@@ -400,7 +398,7 @@ struct UserInfoManager {
} }
// Keychain // Keychain
do { do {
let accountModel = try await keychain.retrieve( let accountModel = try keychain.retrieve(
AccountModel.self, AccountModel.self,
forKey: StorageKeys.accountModel forKey: StorageKeys.accountModel
) )
@@ -448,7 +446,7 @@ struct UserInfoManager {
/// AccountModel /// AccountModel
static func clearAccountModel() async { static func clearAccountModel() async {
do { do {
try await keychain.delete(forKey: StorageKeys.accountModel) try keychain.delete(forKey: StorageKeys.accountModel)
await cacheActor.clearAccountModel() await cacheActor.clearAccountModel()
debugInfoSync("🗑️ AccountModel 已清除") debugInfoSync("🗑️ AccountModel 已清除")
} catch { } catch {
@@ -459,7 +457,7 @@ struct UserInfoManager {
/// ///
static func clearUserInfo() async { static func clearUserInfo() async {
do { do {
try await keychain.delete(forKey: StorageKeys.userInfo) try keychain.delete(forKey: StorageKeys.userInfo)
await cacheActor.clearUserInfo() await cacheActor.clearUserInfo()
debugInfoSync("🗑️ UserInfo 已清除") debugInfoSync("🗑️ UserInfo 已清除")
} catch { } catch {

View File

@@ -1,5 +1,4 @@
import Foundation import Foundation
import ComposableArchitecture
// MARK: - API Service Protocol // MARK: - API Service Protocol
@@ -136,10 +135,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: []) requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
urlRequest.httpBody = requestBody urlRequest.httpBody = requestBody
// urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") // urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
if let httpBody = urlRequest.httpBody, // HTTP Body APILogger
let bodyString = String(data: httpBody, encoding: .utf8) {
debugInfoSync("HTTP Body: \(bodyString)")
}
} catch { } catch {
let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)") let encodingError = APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: encodingError.localizedDescription)
@@ -148,8 +144,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
} }
// headers // headers
await APILogger APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
do { do {
// //
@@ -165,18 +160,16 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
// //
if data.count > APIConfiguration.maxDataSize { if data.count > APIConfiguration.maxDataSize {
await APILogger APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration)
.logError(APIError.resourceTooLarge, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: APIError.resourceTooLarge.localizedDescription)
throw APIError.resourceTooLarge throw APIError.resourceTooLarge
} }
// //
await APILogger APILogger.logResponse(data: data, response: httpResponse, duration: duration)
.logResponse(data: data, response: httpResponse, duration: duration)
// //
await APILogger.logPerformanceWarning(duration: duration) APILogger.logPerformanceWarning(duration: duration)
// HTTP // HTTP
guard 200...299 ~= httpResponse.statusCode else { guard 200...299 ~= httpResponse.statusCode else {
@@ -196,7 +189,7 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
do { do {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let decodedResponse = try decoder.decode(T.Response.self, from: data) let decodedResponse = try decoder.decode(T.Response.self, from: data)
await APILogger.logDecodedResponse(decodedResponse, type: T.Response.self) APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
// loading // loading
await APILoadingManager.shared.finishLoading(loadingId) await APILoadingManager.shared.finishLoading(loadingId)
@@ -210,13 +203,13 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
} catch let error as APIError { } catch let error as APIError {
let duration = Date().timeIntervalSince(startTime) let duration = Date().timeIntervalSince(startTime)
await APILogger.logError(error, url: url, duration: duration) APILogger.logError(error, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: error.localizedDescription)
throw error throw error
} catch { } catch {
let duration = Date().timeIntervalSince(startTime) let duration = Date().timeIntervalSince(startTime)
let apiError = mapSystemError(error) let apiError = mapSystemError(error)
await APILogger.logError(apiError, url: url, duration: duration) APILogger.logError(apiError, url: url, duration: duration)
await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription) await APILoadingManager.shared.setError(loadingId, errorMessage: apiError.localizedDescription)
throw apiError throw apiError
} }
@@ -300,6 +293,14 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
return error return error
} else if let msg = json["msg"] as? String { } else if let msg = json["msg"] as? String {
return msg return msg
} else if let detail = json["detail"] as? String {
return detail
} else if let errorDescription = json["error_description"] as? String {
return errorDescription
} else if let errorDict = json["error"] as? [String: Any], let nestedMsg = errorDict["message"] as? String {
return nestedMsg
} else if let errors = json["errors"] as? [[String: Any]], let firstMsg = errors.first? ["message"] as? String {
return firstMsg
} }
return nil return nil
@@ -349,7 +350,9 @@ actor MockAPIServiceActor: APIServiceProtocol, Sendable {
} }
} }
// MARK: - TCA Dependency Integration // MARK: - TCA Dependency Integration (optional)
#if canImport(ComposableArchitecture)
import ComposableArchitecture
private enum APIServiceKey: DependencyKey { private enum APIServiceKey: DependencyKey {
static let liveValue: any APIServiceProtocol & Sendable = LiveAPIService() static let liveValue: any APIServiceProtocol & Sendable = LiveAPIService()
static let testValue: any APIServiceProtocol & Sendable = MockAPIServiceActor() static let testValue: any APIServiceProtocol & Sendable = MockAPIServiceActor()
@@ -361,6 +364,7 @@ extension DependencyValues {
set { self[APIServiceKey.self] = newValue } set { self[APIServiceKey.self] = newValue }
} }
} }
#endif
// MARK: - BaseRequest Dictionary Conversion // MARK: - BaseRequest Dictionary Conversion
extension BaseRequest { extension BaseRequest {

View File

@@ -1,5 +1,4 @@
import Foundation import Foundation
import ComposableArchitecture
// MARK: - // MARK: -

View File

@@ -5,17 +5,17 @@ enum AppEnvironment {
struct AppConfig { struct AppConfig {
static let current: AppEnvironment = { static let current: AppEnvironment = {
// #if DEBUG #if DEBUG
// return .development return .development
// #else #else
return .production return .production
// #endif #endif
}() }()
static var baseURL: String { static var baseURL: String {
switch current { switch current {
case .development: case .development:
return "http://beta.api.molistar.xyz" return "http://beta.api.molistar.xyz";//"http://beta.api.pekolive.com"
case .production: case .production:
return "https://api.epartylive.com" return "https://api.epartylive.com"
} }

View File

@@ -1,114 +0,0 @@
import Foundation
import ComposableArchitecture
@Reducer
struct SplashFeature {
@ObservableState
struct State: Equatable {
var isLoading = true
var shouldShowMainApp = false
var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound
var isCheckingAuthentication = false
//
var navigationDestination: NavigationDestination?
init() {
//
}
}
//
enum NavigationDestination: Equatable {
case login //
case main //
}
enum Action: Equatable {
case onAppear
case splashFinished
case checkAuthentication
case authenticationChecked(UserInfoManager.AuthenticationStatus)
// actions
case fetchUserInfo
case userInfoFetched(Bool)
// actions
case navigateToLogin
case navigateToMain
}
@Dependency(\.apiService) var apiService // API
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
state.isLoading = true
state.shouldShowMainApp = false
state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound
state.isCheckingAuthentication = false
state.navigationDestination = nil
// 1 (iOS 15.5+ )
return .run { send in
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 = 1,000,000,000
await send(.splashFinished)
}
case .splashFinished:
state.isLoading = false
// Splash
return .send(.checkAuthentication)
case .checkAuthentication:
state.isCheckingAuthentication = true
//
return .run { send in
let authStatus = await UserInfoManager.checkAuthenticationStatus()
await send(.authenticationChecked(authStatus))
}
case let .authenticationChecked(status):
state.isCheckingAuthentication = false
state.authenticationStatus = status
//
if status.canAutoLogin {
debugInfoSync("🎉 自动登录成功,开始获取用户信息")
//
return .send(.fetchUserInfo)
} else {
debugInfoSync("🔑 需要手动登录")
return .send(.navigateToLogin)
}
case .fetchUserInfo:
//
return .run { send in
let success = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: apiService)
await send(.userInfoFetched(success))
}
case let .userInfoFetched(success):
if success {
debugInfoSync("✅ 用户信息获取成功,进入主页")
} else {
debugInfoSync("⚠️ 用户信息获取失败,但仍进入主页")
}
return .send(.navigateToMain)
case .navigateToLogin:
state.navigationDestination = .login
return .none
case .navigateToMain:
state.navigationDestination = .main
state.shouldShowMainApp = true
return .none
}
}
}
}

View File

@@ -15,5 +15,7 @@
<array> <array>
<string>Bayon-Regular.ttf</string> <string>Bayon-Regular.ttf</string>
</array> </array>
<key>API_SIGNING_KEY</key>
<string></string>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,89 @@
import SwiftUI
@MainActor
final class CreateFeedViewModel: ObservableObject {
@Published var content: String = ""
@Published var selectedImages: [UIImage] = []
@Published var isPublishing: Bool = false
@Published var errorMessage: String? = nil
var canPublish: Bool { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !selectedImages.isEmpty }
}
struct CreateFeedPage: View {
@StateObject private var viewModel = CreateFeedViewModel()
let onDismiss: () -> Void
var body: some View {
GeometryReader { _ in
ZStack {
Color(hex: 0x0C0527).ignoresSafeArea()
VStack(spacing: 16) {
HStack {
Button(action: onDismiss) {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.system(size: 18, weight: .medium))
}
Spacer()
Text(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
.foregroundColor(.white)
.font(.system(size: 18, weight: .medium))
Spacer()
Button(action: publish) {
if viewModel.isPublishing {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text(LocalizedString("createFeed.publish", comment: "Publish"))
.foregroundColor(.white)
.font(.system(size: 14, weight: .medium))
}
}
.disabled(!viewModel.canPublish || viewModel.isPublishing)
.opacity((!viewModel.canPublish || viewModel.isPublishing) ? 0.6 : 1)
}
.padding(.horizontal, 16)
.padding(.top, 12)
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A))
if viewModel.content.isEmpty {
Text(LocalizedString("createFeed.enterContent", comment: "Enter Content"))
.foregroundColor(.white.opacity(0.5))
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
TextEditor(text: $viewModel.content)
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.scrollContentBackground(.hidden)
.frame(height: 200)
}
.frame(height: 200)
.padding(.horizontal, 20)
if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.font(.system(size: 14))
}
Spacer()
}
}
}
.navigationBarBackButtonHidden(true)
}
private func publish() {
viewModel.isPublishing = true
Task { @MainActor in
try? await Task.sleep(nanoseconds: 500_000_000)
viewModel.isPublishing = false
onDismiss()
}
}
}

View File

@@ -16,6 +16,7 @@ class LoginViewModel: ObservableObject {
// MARK: - Callbacks // MARK: - Callbacks
var onLoginSuccess: (() -> Void)? var onLoginSuccess: (() -> Void)?
private var hasSentSuccess: Bool = false
// MARK: - Public Methods // MARK: - Public Methods
func onIDLoginTapped() { func onIDLoginTapped() {
@@ -47,22 +48,20 @@ class LoginViewModel: ObservableObject {
} }
func onLoginCompleted() { func onLoginCompleted() {
guard !hasSentSuccess else { return }
isAnyLoginCompleted = true isAnyLoginCompleted = true
showIDLogin = false
showEmailLogin = false
hasSentSuccess = true
onLoginSuccess?() onLoginSuccess?()
} }
func onBackFromIDLogin() { func onBackFromIDLogin() {
showIDLogin = false showIDLogin = false
if isAnyLoginCompleted {
onLoginSuccess?()
}
} }
func onBackFromEmailLogin() { func onBackFromEmailLogin() {
showEmailLogin = false showEmailLogin = false
if isAnyLoginCompleted {
onLoginSuccess?()
}
} }
} }
@@ -73,78 +72,76 @@ struct LoginPage: View {
let onLoginSuccess: () -> Void let onLoginSuccess: () -> Void
var body: some View { var body: some View {
NavigationStack { GeometryReader { geometry in
GeometryReader { geometry in ZStack {
ZStack { backgroundView
backgroundView
VStack(spacing: 0) {
VStack(spacing: 0) { Image("top")
Image("top") .resizable()
.resizable() .aspectRatio(375/400, contentMode: .fit)
.aspectRatio(375/400, contentMode: .fit) .frame(maxWidth: .infinity)
.frame(maxWidth: .infinity)
HStack { HStack {
Text(LocalizedString("login.app_title", comment: "")) Text(LocalizedString("login.app_title", comment: ""))
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.leading, 20) .padding(.leading, 20)
Spacer()
}
.padding(.bottom, 20) // top
Spacer() Spacer()
bottomSection
} }
.padding(.bottom, 20)
// - Spacer()
languageSettingsButton
.position(x: geometry.size.width - 40, y: 60)
APILoadingEffectView() bottomSection
} }
// -
languageSettingsButton
.position(x: geometry.size.width - 40, y: 60)
APILoadingEffectView()
} }
.ignoresSafeArea() }
.ignoresSafeArea()
.navigationBarHidden(true)
.navigationDestination(isPresented: $viewModel.showIDLogin) {
IDLoginPage(
onBack: {
viewModel.onBackFromIDLogin()
},
onLoginSuccess: {
viewModel.onLoginCompleted()
}
)
.navigationBarHidden(true) .navigationBarHidden(true)
.navigationDestination(isPresented: $viewModel.showIDLogin) { }
IDLoginPage( .navigationDestination(isPresented: $viewModel.showEmailLogin) {
onBack: { EMailLoginPage(
viewModel.onBackFromIDLogin() onBack: {
}, viewModel.onBackFromEmailLogin()
onLoginSuccess: { },
viewModel.onLoginCompleted() onLoginSuccess: {
} viewModel.onLoginCompleted()
) }
.navigationBarHidden(true)
}
.navigationDestination(isPresented: $viewModel.showEmailLogin) {
EMailLoginPage(
onBack: {
viewModel.onBackFromEmailLogin()
},
onLoginSuccess: {
viewModel.onLoginCompleted()
}
)
.navigationBarHidden(true)
}
.sheet(isPresented: $viewModel.showLanguageSettings) {
LanguageSettingsView(isPresented: $viewModel.showLanguageSettings)
}
.webView(
isPresented: $viewModel.showUserAgreement,
url: APIConfiguration.webURL(for: .userAgreement)
) )
.webView( .navigationBarHidden(true)
isPresented: $viewModel.showPrivacyPolicy, }
url: APIConfiguration.webURL(for: .privacyPolicy) .sheet(isPresented: $viewModel.showLanguageSettings) {
) LanguageSettingsView(isPresented: $viewModel.showLanguageSettings)
.alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $viewModel.showAgreementAlert) { }
Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { } .webView(
} message: { isPresented: $viewModel.showUserAgreement,
Text(LocalizedString("login.agreement_alert_message", comment: "")) url: APIConfiguration.webURL(for: .userAgreement)
} )
.webView(
isPresented: $viewModel.showPrivacyPolicy,
url: APIConfiguration.webURL(for: .privacyPolicy)
)
.alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $viewModel.showAgreementAlert) {
Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { }
} message: {
Text(LocalizedString("login.agreement_alert_message", comment: ""))
} }
.onAppear { .onAppear {
viewModel.onLoginSuccess = onLoginSuccess viewModel.onLoginSuccess = onLoginSuccess

View File

@@ -31,9 +31,10 @@ struct MainPage: View {
} }
} }
} }
.navigationDestination(for: String.self) { destination in .navigationDestination(for: String.self) { _ in EmptyView() }
switch destination { .navigationDestination(for: AppRoute.self) { route in
case "setting": switch route {
case .setting:
SettingPage( SettingPage(
onBack: { onBack: {
viewModel.navigationPath.removeLast() viewModel.navigationPath.removeLast()
@@ -43,6 +44,12 @@ struct MainPage: View {
} }
) )
.navigationBarHidden(true) .navigationBarHidden(true)
case .publish:
CreateFeedPage(
onDismiss: {
viewModel.navigationPath.removeLast()
}
)
default: default:
EmptyView() EmptyView()
} }

View File

@@ -1,182 +0,0 @@
import SwiftUI
import Combine
// MARK: - Splash ViewModel
@MainActor
class SplashViewModel: ObservableObject {
// MARK: - Published Properties
@Published var isLoading = true
@Published var shouldShowMainApp = false
@Published var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound
@Published var isCheckingAuthentication = false
@Published var navigationDestination: NavigationDestination?
// MARK: - Private Properties
private var cancellables = Set<AnyCancellable>()
// MARK: - Navigation Destination
enum NavigationDestination: Equatable {
case login
case main
}
// MARK: - Initialization
init() {
setupBindings()
}
// MARK: - Public Methods
func onAppear() {
isLoading = true
shouldShowMainApp = false
authenticationStatus = .notFound
isCheckingAuthentication = false
navigationDestination = nil
// 1
Task {
try await Task.sleep(nanoseconds: 1_000_000_000)
await MainActor.run {
self.splashFinished()
}
}
}
func splashFinished() {
isLoading = false
checkAuthentication()
}
func checkAuthentication() {
isCheckingAuthentication = true
Task {
let authStatus = await UserInfoManager.checkAuthenticationStatus()
await MainActor.run {
self.authenticationChecked(authStatus)
}
}
}
func authenticationChecked(_ status: UserInfoManager.AuthenticationStatus) {
isCheckingAuthentication = false
authenticationStatus = status
if status.canAutoLogin {
debugInfoSync("🎉 自动登录成功,开始获取用户信息")
fetchUserInfo()
} else {
debugInfoSync("🔑 需要手动登录")
navigateToLogin()
}
}
func fetchUserInfo() {
Task {
let success = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: LiveAPIService())
await MainActor.run {
self.userInfoFetched(success)
}
}
}
func userInfoFetched(_ success: Bool) {
if success {
debugInfoSync("✅ 用户信息获取成功,进入主页")
} else {
debugInfoSync("⚠️ 用户信息获取失败,但仍进入主页")
}
navigateToMain()
}
func navigateToLogin() {
navigationDestination = .login
}
func navigateToMain() {
navigationDestination = .main
shouldShowMainApp = true
}
// MARK: - Private Methods
private func setupBindings() {
// Combine
}
}
// MARK: - Splash View
struct Splash: View {
@StateObject private var viewModel = SplashViewModel()
var body: some View {
ZStack {
Group {
//
if let navigationDestination = viewModel.navigationDestination {
switch navigationDestination {
case .login:
//
LoginPage(
onLoginSuccess: {
//
viewModel.navigateToMain()
}
)
case .main:
//
MainPage(
onLogout: {
viewModel.navigateToLogin()
}
)
}
} else {
//
splashContent
}
}
.onAppear {
viewModel.onAppear()
}
// API Loading -
APILoadingEffectView()
}
}
//
private var splashContent: some View {
ZStack {
// -
LoginBackgroundView()
VStack(spacing: 32) {
Spacer()
.frame(height: 200) // storyboard
// Logo - 100x100
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
// - 40pt
Text(LocalizedString("splash.title", comment: "E-Parti"))
.font(.system(size: 40, weight: .regular))
.foregroundColor(.white)
Spacer()
}
}
}
}
//#Preview {
// Splash()
//}
#Preview {
Splash()
}

63
yana/MVVM/SplashV2.swift Normal file
View File

@@ -0,0 +1,63 @@
import SwiftUI
struct SplashV2: View {
@State private var showLogin = false
@State private var showMain = false
@State private var hasCheckedAuth = false
private let splashTransitionAnimation: Animation = .easeInOut(duration: 0.25)
var body: some View {
Group {
if showMain {
MainPage(onLogout: {
showMain = false
showLogin = true
})
} else if showLogin {
NavigationStack {
LoginPage(onLoginSuccess: {
showMain = true
})
}
} else {
ZStack {
LoginBackgroundView()
VStack(spacing: 32) {
Spacer().frame(height: 200)
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
Text(LocalizedString("splash.title", comment: "E-Parti"))
.font(.system(size: 40, weight: .regular))
.foregroundColor(.white)
Spacer()
}
}
.onAppear {
guard !hasCheckedAuth else { return }
hasCheckedAuth = true
Task { @MainActor in
debugInfoSync("🚀 SplashV2 启动,开始检查登录缓存")
let status = await UserInfoManager.checkAuthenticationStatus()
if status.canAutoLogin {
debugInfoSync("✅ 检测到可自动登录,尝试预取用户信息")
_ = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: LiveAPIService())
withAnimation(splashTransitionAnimation) {
showMain = true
}
} else {
debugInfoSync("🔑 未登录或缓存无效,进入登录页")
withAnimation(splashTransitionAnimation) {
showLogin = true
}
}
}
}
}
}
.ignoresSafeArea()
}
}

View File

@@ -56,9 +56,9 @@ class MainViewModel: ObservableObject {
func onTopRightButtonTapped() { func onTopRightButtonTapped() {
switch selectedTab { switch selectedTab {
case .feed: case .feed:
onAddButtonTapped?() navigationPath.append(AppRoute.publish)
case .me: case .me:
navigationPath.append("setting") navigationPath.append(AppRoute.setting)
} }
} }
} }

View File

@@ -0,0 +1,11 @@
import Foundation
///
enum AppRoute: Hashable {
case login
case main
case setting
case publish
}

View File

@@ -0,0 +1,38 @@
import Foundation
@preconcurrency import Combine
// @unchecked Sendable Future promise
private final class PromiseBox<Output, Failure: Error>: @unchecked Sendable {
private let fulfill: (Result<Output, Failure>) -> Void
init(_ fulfill: @escaping (Result<Output, Failure>) -> Void) { self.fulfill = fulfill }
func complete(_ result: Result<Output, Failure>) { fulfill(result) }
}
extension APIServiceProtocol {
/// async/await Combine Publisher
/// - Parameter request: APIRequestProtocol
/// - Returns: AnyPublisher<T.Response, APIError>
func requestPublisher<T: APIRequestProtocol>(_ request: T) -> AnyPublisher<T.Response, APIError> {
Deferred {
Future { promise in
let box = PromiseBox<T.Response, APIError>(promise)
Task(priority: .userInitiated) {
let result: Result<T.Response, APIError>
do {
let value = try await self.request(request)
result = .success(value)
} catch let apiError as APIError {
result = .failure(apiError)
} catch {
result = .failure(.unknown(error.localizedDescription))
}
box.complete(result)
}
}
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.eraseToAnyPublisher()
}
}

View File

@@ -0,0 +1,54 @@
import Foundation
import UIKit
@MainActor
struct DeviceContext: Sendable {
let languageCode: String
let osName: String
let osVersion: String
let deviceModel: String
let deviceId: String
let appName: String
let appVersion: String
let channel: String
let screenScale: String
static let shared: DeviceContext = {
// 线 UIKit/Bundle
let language = Locale.current.language.languageCode?.identifier ?? "en"
let osName = "iOS"
let osVersion = UIDevice.current.systemVersion
let deviceModel = UIDevice.current.model
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "eparty"
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
#if DEBUG
let channel = "molistar_enterprise"
#else
let channel = "appstore"
#endif
let scale = String(format: "%.2f", Double(UIScreen.main.scale))
return DeviceContext(
languageCode: language,
osName: osName,
osVersion: osVersion,
deviceModel: deviceModel,
deviceId: deviceId,
appName: appName,
appVersion: appVersion,
channel: channel,
screenScale: scale
)
}()
}
enum UserAgentProvider {
@MainActor
static func userAgent() -> String {
let ctx = DeviceContext.shared
return "\(ctx.appName)/\(ctx.appVersion) (\(ctx.deviceModel); \(ctx.osName) \(ctx.osVersion); Scale/\(ctx.screenScale))"
}
}

View File

@@ -0,0 +1,33 @@
import Foundation
import Network
///
/// WiFi=2, =1, /=0
final class NetworkMonitor: @unchecked Sendable {
static let shared = NetworkMonitor()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "com.yana.network.monitor")
private var _currentType: Int = 2 //
var currentType: Int { _currentType }
private init() {
monitor.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
let type: Int
if path.status == .satisfied {
if path.usesInterfaceType(.wifi) { type = 2 }
else if path.usesInterfaceType(.cellular) { type = 1 }
else { type = 0 }
} else {
type = 0
}
// 线线 UI
DispatchQueue.main.async { [weak self] in
self?._currentType = type
}
}
monitor.start(queue: queue)
}
}

View File

@@ -12,11 +12,10 @@ import Security
/// - /// -
/// - 线 /// - 线
/// - 访 /// - 访
@MainActor final class KeychainManager: @unchecked Sendable {
final class KeychainManager {
// MARK: - // MARK: -
@MainActor static let shared = KeychainManager() static let shared = KeychainManager()
private init() {} private init() {}
// MARK: - // MARK: -

View File

@@ -0,0 +1,32 @@
import Foundation
/// API
/// - Info.plist `API_SIGNING_KEY`
/// - Debug 退
/// - Release
enum SigningKeyProvider {
/// Info.plist
private static let plistKey = "API_SIGNING_KEY"
///
static func signingKey() -> String {
if let key = Bundle.main.object(forInfoDictionaryKey: plistKey) as? String,
!key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return key
}
#if DEBUG
// Debug 退 Info.plist API_SIGNING_KEY
let legacy = "rpbs6us1m8r2j9g6u06ff2bo18orwaya"
debugWarnSync("⚠️ API_SIGNING_KEY 未配置Debug 使用历史回退密钥(请尽快配置 Info.plist")
return legacy
#else
debugErrorSync("❌ 缺少 API_SIGNING_KEY请在 Info.plist 中配置")
assertionFailure("Missing API_SIGNING_KEY in Info.plist")
return ""
#endif
}
}

View File

@@ -1,91 +0,0 @@
import SwiftUI
import ComposableArchitecture
struct SplashView: View {
let store: StoreOf<SplashFeature>
var body: some View {
ZStack {
Group {
//
if let navigationDestination = store.navigationDestination {
switch navigationDestination {
case .login:
//
LoginView(
store: Store(
initialState: LoginFeature.State()
) {
LoginFeature()
},
onLoginSuccess: {
//
store.send(.navigateToMain)
}
)
case .main:
//
MainView(
store: Store(
initialState: MainFeature.State()
) {
MainFeature()
},
onLogout: {
store.send(.navigateToLogin)
}
)
}
} else {
//
splashContent
}
}
.onAppear {
store.send(.onAppear)
}
// API Loading -
APILoadingEffectView()
}
}
//
private var splashContent: some View {
ZStack {
// -
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 32) {
Spacer()
.frame(height: 200) // storyboard
// Logo - 100x100
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
// - 40pt
Text(LocalizedString("splash.title", comment: "E-Parti"))
.font(.system(size: 40, weight: .regular))
.foregroundColor(.white)
Spacer()
}
}
}
}
//#Preview {
// SplashView(
// store: Store(
// initialState: SplashFeature.State()
// ) {
// SplashFeature()
// }
// )
//}

View File

@@ -6,7 +6,6 @@
// //
import SwiftUI import SwiftUI
import ComposableArchitecture
@main @main
struct yanaApp: App { struct yanaApp: App {
@@ -24,7 +23,7 @@ struct yanaApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
Splash() SplashV2()
} }
} }
} }