From a0200c88590b60c9833b12bee45913b4ca8cde5c Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Thu, 29 May 2025 16:14:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=96=87=E4=BB=B6=E5=92=8C=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增.gitignore、Podfile和Podfile.lock文件以管理项目依赖,添加README.md文件提供项目简介和安装步骤,创建NIMSessionManager、ClientConfig、LogManager和NetworkManager等管理类以支持网络请求和日志记录功能,更新AppDelegate和ContentView以集成NIM SDK和实现用户登录功能。 --- .cursor/rules/swift-assistant-style.mdc | 69 ++ .cursor/rules/swift-swiftui-dev-rules.mdc | 115 +++ .../swift-tca-architecture-guidelines.mdc | 52 ++ .gitignore | 4 + Podfile | 40 ++ Podfile.lock | 127 ++++ README.md | 78 ++ tools/YYUtility/LICENSE | 21 + yana.xcodeproj/project.pbxproj | 306 +++++++- .../xcschemes/xcschememanagement.plist | 2 +- yana.xcworkspace/contents.xcworkspacedata | 10 + .../xcdebugger/Breakpoints_v2.xcbkptlist | 152 ++++ yana/APIs/API.swift | 59 ++ yana/AppDelegate.swift | 9 + yana/Configs/AppConfig.swift | 4 +- yana/Configs/ClientConfig.swift | 32 + yana/ContentView.swift | 58 +- yana/Info.plist | 11 + yana/Managers/LogManager.swift | 51 ++ yana/Managers/NIMConfigurationManager.swift | 35 +- yana/Managers/NIMSessionManager.swift | 127 ++++ yana/Managers/NetworkManager.swift | 667 ++++++++++++++++++ yana/yana-Bridging-Header.h | 4 + yana/yanaApp.swift | 3 + yanaAPITests/yanaAPITests.swift | 68 ++ 25 files changed, 2081 insertions(+), 23 deletions(-) create mode 100644 .cursor/rules/swift-assistant-style.mdc create mode 100644 .cursor/rules/swift-swiftui-dev-rules.mdc create mode 100644 .cursor/rules/swift-tca-architecture-guidelines.mdc create mode 100644 .gitignore create mode 100644 Podfile create mode 100644 Podfile.lock create mode 100644 README.md create mode 100644 tools/YYUtility/LICENSE create mode 100644 yana.xcworkspace/contents.xcworkspacedata create mode 100644 yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 yana/APIs/API.swift create mode 100644 yana/AppDelegate.swift create mode 100644 yana/Configs/ClientConfig.swift create mode 100644 yana/Info.plist create mode 100644 yana/Managers/LogManager.swift create mode 100644 yana/Managers/NIMSessionManager.swift create mode 100644 yana/Managers/NetworkManager.swift create mode 100644 yana/yana-Bridging-Header.h create mode 100644 yanaAPITests/yanaAPITests.swift diff --git a/.cursor/rules/swift-assistant-style.mdc b/.cursor/rules/swift-assistant-style.mdc new file mode 100644 index 0000000..89e6229 --- /dev/null +++ b/.cursor/rules/swift-assistant-style.mdc @@ -0,0 +1,69 @@ +--- +description: +globs: +alwaysApply: true +--- + + # CONTEXT + + I am a native Chinese speaker who has just begun learning Swift 6 and Xcode 16, and I am enthusiastic about exploring new technologies. I wish to receive advice using the latest tools and + seek step-by-step guidance to fully understand the implementation process. Since many excellent code resources are in English, I hope my questions can be thoroughly understood. Therefore, + I would like the AI assistant to think and reason in English, then translate the English responses into Chinese for me. + + --- + + # OBJECTIVE + + As an expert AI programming assistant, your task is to provide me with clear and readable SwiftUI code. You should: + + - Utilize the latest versions of SwiftUI and Swift, being familiar with the newest features and best practices. + - Provide careful and accurate answers that are well-founded and thoughtfully considered. + - **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.** + - Strictly adhere to my requirements and meticulously complete the tasks. + - Begin by outlining your proposed approach with detailed steps or pseudocode. + - Upon confirming the plan, proceed to write the code. + + --- + + # STYLE + + - Keep answers concise and direct, minimizing unnecessary wording. + - Emphasize code readability over performance optimization. + - Maintain a professional and supportive tone, ensuring clarity of content. + + --- + + # TONE + + - Be positive and encouraging, helping me improve my programming skills. + - Be professional and patient, assisting me in understanding each step. + + --- + + # AUDIENCE + + The target audience is me—a native Chinese developer eager to learn Swift 6 and Xcode 16, seeking guidance and advice on utilizing the latest technologies. + + --- + + # RESPONSE FORMAT + + - **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.** + - Conduct reasoning, thinking, and code writing in English. + - The final reply should translate the English into Chinese for me. + - The reply should include: + + 1. **Step-by-Step Plan**: Describe the implementation process with detailed pseudocode or step-by-step explanations, showcasing your thought process. + 2. **Code Implementation**: Provide correct, up-to-date, error-free, fully functional, runnable, secure, and efficient code. The code should: + - Include all necessary imports and properly name key components. + - Fully implement all requested features, leaving no to-dos, placeholders, or omissions. + 3. **Concise Response**: Minimize unnecessary verbosity, focusing only on essential information. + + - If a correct answer may not exist, please point it out. If you do not know the answer, please honestly inform me rather than guessing. + + --- + + # START ANALYSIS + + If you understand, please prepare to assist me and await my question. + \ No newline at end of file diff --git a/.cursor/rules/swift-swiftui-dev-rules.mdc b/.cursor/rules/swift-swiftui-dev-rules.mdc new file mode 100644 index 0000000..ebf188f --- /dev/null +++ b/.cursor/rules/swift-swiftui-dev-rules.mdc @@ -0,0 +1,115 @@ +--- +description: +globs: +alwaysApply: true +--- + You are an expert iOS developer using Swift and SwiftUI. Follow these guidelines: + + # Architechture + - Use TCA(The Composable Architecture) architecture with SwiftUI & Swift + + # Code Structure + - Use Swift's latest features and protocol-oriented programming + - Prefer value types (structs) over classes + - Follow Apple's Human Interface Guidelines + + + # Naming + - camelCase for vars/funcs, PascalCase for types + - Verbs for methods (fetchData) + - Boolean: use is/has/should prefixes + - Clear, descriptive names following Apple style + + + # Swift Best Practices + + - Strong type system, proper optionals + - async/await for concurrency + - Result type for errors + - @Published, @StateObject for state + - Prefer let over var + - Protocol extensions for shared code + + + # UI Development + + - SwiftUI first, UIKit when needed + - SF Symbols for icons + - Support dark mode, dynamic type + - SafeArea and GeometryReader for layout + - Handle all screen sizes and orientations + - Implement proper keyboard handling + + + # Performance + + - Profile with Instruments + - Lazy load views and images + - Optimize network requests + - Background task handling + - Proper state management + - Memory management + + + # Data & State + + - CoreData for complex models + - UserDefaults for preferences + - Combine for reactive code + - Clean data flow architecture + - Proper dependency injection + - Handle state restoration + + + # Security + + - Encrypt sensitive data + - Use Keychain securely + - Certificate pinning + - Biometric auth when needed + - App Transport Security + - Input validation + + + # Testing & Quality + + - XCTest for unit tests + - XCUITest for UI tests + - Test common user flows + - Performance testing + - Error scenarios + - Accessibility testing + + + # Essential Features + + - Deep linking support + - Push notifications + - Background tasks + - Localization + - Error handling + - Analytics/logging + + + # Development Process + + - Use SwiftUI previews + - Git branching strategy + - Code review process + - CI/CD pipeline + - Documentation + - Unit test coverage + + + # App Store Guidelines + + - Privacy descriptions + - App capabilities + - In-app purchases + - Review guidelines + - App thinning + - Proper signing + + + Follow Apple's documentation for detailed implementation guidance. + \ No newline at end of file diff --git a/.cursor/rules/swift-tca-architecture-guidelines.mdc b/.cursor/rules/swift-tca-architecture-guidelines.mdc new file mode 100644 index 0000000..ca4e919 --- /dev/null +++ b/.cursor/rules/swift-tca-architecture-guidelines.mdc @@ -0,0 +1,52 @@ +--- +description: +globs: +alwaysApply: true +--- +# TCA Architecture Guidelines +- Use The Composable Architecture (TCA) for state management and side effect handling. +- Define reducers for each feature and use @Reducer annotation. +- Use stores to manage state. +- Follow unidirectional data flow (state -> view -> action -> reducer -> state). +- Use @State to manage local state and @ObservedObject or @EnvironmentObject to manage shared state. +- Make sure all side effects (such as network requests) are handled in reducers using Effects. +- Divide reducers by functionality and define a root reducer. +- Use Dependency Injection to manage external dependencies (such as network, database). +- Write tests for reducers to ensure correct state transitions. + +## Feature Structure +Each feature must include: +1. A `State` struct +2. An `Action` enum +3. A `Reducer` +4. A `View` (if applicable) + +## Naming Conventions +- Feature: `{{FeatureName}}Feature` +- State: `{{FeatureName}}Feature.State` +- Action: `{{FeatureName}}Feature.Action` +- Reducer: `{{FeatureName}}Feature.reducer` + +## Example +```swift +struct CounterFeature { + struct State: Equatable { + var count = 0 + } + + enum Action: Equatable { + case increment + case decrement + } + + static let reducer = Reducer { state, action, _ in + switch action { + case .increment: + state.count += 1 + return .none + case .decrement: + state.count -= 1 + return .none + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61e7675 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +Pods +.vscode +yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +*.xcbkptlist diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..919adf0 --- /dev/null +++ b/Podfile @@ -0,0 +1,40 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '15.6' + +target 'yana' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for yana + + # IM 即时通讯 + pod 'NIMSDK_LITE' + # 基础库 + pod 'NEChatKit', '10.6.1' + pod 'NEChatUIKit', '10.6.1' # 会话(聊天)组件 + pod 'NEContactUIKit', '10.6.1' # 通讯录组件 + pod 'NELocalConversationUIKit', '10.6.1' # 本地会话列表组件。 + + # Networks + pod 'Alamofire' +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + end + end + + # 新增冲突处理脚本 + installer.pods_project.targets.each do |target| + target.build_phases.each do |phase| + if phase.respond_to?(:name) && phase.name == 'Embed Frameworks' + phase.input_paths.delete_if { |path| + path.end_with?('nimsdk.xcframework', 'nimsocketrocket.xcframework', 'nimquic.xcframework', 'nimnos.xcframework') + } + end + end + end +end \ No newline at end of file diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..5e3ac1c --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,127 @@ +PODS: + - Alamofire (5.10.2) + - CocoaLumberjack (3.8.5): + - CocoaLumberjack/Core (= 3.8.5) + - CocoaLumberjack/Core (3.8.5) + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): + - libwebp/webp + - libwebp/mux (1.5.0): + - libwebp/demux + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): + - libwebp/sharpyuv + - MJRefresh (3.7.5) + - NEChatKit (10.6.1): + - NEChatKit/NOS (= 10.6.1) + - NEChatKit/NOS (10.6.1): + - NECommonKit (= 9.7.2) + - NECoreIM2Kit/NOS (= 1.0.9) + - NEChatUIKit (10.6.1): + - NEChatUIKit/NOS (= 10.6.1) + - NEChatUIKit/NOS (10.6.1): + - MJRefresh (= 3.7.5) + - NEChatKit/NOS + - NECommonUIKit (= 9.7.6) + - SDWebImageSVGKitPlugin + - SDWebImageWebPCoder + - NECommonKit (9.7.2): + - YXAlog + - NECommonUIKit (9.7.6): + - NECommonKit + - SDWebImage + - NEContactUIKit (10.6.1): + - NEContactUIKit/NOS (= 10.6.1) + - NEContactUIKit/NOS (10.6.1): + - MJRefresh (= 3.7.5) + - NEChatKit/NOS + - NECommonUIKit (= 9.7.6) + - NECoreIM2Kit/NOS (1.0.9): + - NECoreKit (= 9.7.5) + - NIMSDK_LITE (= 10.8.20) + - YXAlog (= 1.0.9) + - NECoreKit (9.7.5): + - YXAlog + - NELocalConversationUIKit (10.6.1): + - NELocalConversationUIKit/NOS (= 10.6.1) + - NELocalConversationUIKit/NOS (10.6.1): + - MJRefresh (= 3.7.5) + - NEChatKit/NOS + - NECommonUIKit (= 9.7.6) + - NIMSDK_LITE (10.8.20): + - NIMSDK_LITE/NOS (= 10.8.20) + - YXArtemis_XCFramework + - NIMSDK_LITE/NOS (10.8.20): + - YXArtemis_XCFramework + - SDWebImage (5.21.0): + - SDWebImage/Core (= 5.21.0) + - SDWebImage/Core (5.21.0) + - SDWebImageSVGKitPlugin (1.4.0): + - SDWebImage/Core (~> 5.10) + - SVGKit (~> 3.0) + - SDWebImageWebPCoder (0.14.6): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) + - SVGKit (3.0.0): + - CocoaLumberjack (~> 3.0) + - YXAlog (1.0.9) + - YXArtemis_XCFramework (1.1.4) + +DEPENDENCIES: + - Alamofire + - NEChatKit (= 10.6.1) + - NEChatUIKit (= 10.6.1) + - NEContactUIKit (= 10.6.1) + - NELocalConversationUIKit (= 10.6.1) + - NIMSDK_LITE + +SPEC REPOS: + trunk: + - Alamofire + - CocoaLumberjack + - libwebp + - MJRefresh + - NEChatKit + - NEChatUIKit + - NECommonKit + - NECommonUIKit + - NEContactUIKit + - NECoreIM2Kit + - NECoreKit + - NELocalConversationUIKit + - NIMSDK_LITE + - SDWebImage + - SDWebImageSVGKitPlugin + - SDWebImageWebPCoder + - SVGKit + - YXAlog + - YXArtemis_XCFramework + +SPEC CHECKSUMS: + Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 + CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0 + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + MJRefresh: fdf5e979eb406a0341468932d1dfc8b7f9fce961 + NEChatKit: c36d5824242fcbff0790bfa76316faabf09df8df + NEChatUIKit: 8b431a7d1ec5fbe7c4d079b9ae0dc5062cd5e146 + NECommonKit: f2359393571fcc105a7fc2fb0367a71319606042 + NECommonUIKit: b5373164800ff138dd075abac90e95379603bb60 + NEContactUIKit: 532609b8da3d2a7f274489e6e6109c6f8b774505 + NECoreIM2Kit: 0faffb84b4a2ac0fcc3705dbf4e72f022c01320f + NECoreKit: 0ccc64f01c8fdc7266f5a4df41de67447db18503 + NELocalConversationUIKit: 2f9208763b4f855d3cb3e3e105e733b020594f19 + NIMSDK_LITE: 22740bf6e2660cb7bafc40f8293fa04d3a77948e + SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 + SDWebImageSVGKitPlugin: 7542dd07c344ec3415ded0461a1161a6f087e0c9 + SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 + SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea + YXAlog: 6fdd73102ba0a16933dd7bef426d6011d913c041 + YXArtemis_XCFramework: d298161285aa9cf0c99800b17847dc99aef60617 + +PODFILE CHECKSUM: 4034a059527d37196c5dca32d338b37b71e31488 + +COCOAPODS: 1.16.2 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef3eca9 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Yana iOS 项目 + +## 项目简介 + +Yana 是一个基于 iOS 平台的即时通讯应用,使用 Swift 语言开发,集成了网易云信 SDK 实现即时通讯功能。 + +## 技术栈 + +- 开发语言:Swift +- 最低支持版本:iOS 15.6 +- 主要框架: + - NIMSDK_LITE:网易云信即时通讯 SDK + - NEChatKit:聊天核心组件 + - NEChatUIKit:会话(聊天)UI 组件 + - NEContactUIKit:通讯录 UI 组件 + - NELocalConversationUIKit:本地会话列表 UI 组件 + - Alamofire:网络请求框架 + +## 项目结构 + +``` +yana/ +├── AppDelegate.swift # 应用程序代理 +├── yanaApp.swift # SwiftUI 应用入口 +├── ContentView.swift # 主视图 +├── Managers/ # 管理器类 +├── Models/ # 数据模型 +├── Configs/ # 配置文件 +└── Assets.xcassets/ # 资源文件 +``` + +## 环境要求 + +- Xcode 13.0 或更高版本 +- iOS 15.6 或更高版本 +- CocoaPods 包管理器 + +## 安装步骤 + +1. 克隆项目到本地 +2. 在项目根目录执行: + + ```bash + pod install + ``` + +3. 打开 `yana.xcworkspace` 文件 +4. 编译运行项目 + +## 主要功能 + +- 即时通讯 +- 会话管理 +- 通讯录管理 +- 本地会话列表 + +## 注意事项 + +- 项目使用 CocoaPods 管理依赖 +- 需要配置网易云信相关密钥 +- 最低支持 iOS 15.6 版本 + +## 开发规范 + +- 遵循 Swift 官方编码规范 +- 使用 SwiftUI 构建用户界面 +- 采用 MVVM 架构模式 + +## 依赖版本 + +- NIMSDK 相关组件版本:10.6.1 +- Alamofire:最新版本 + +## 构建配置 + +- 项目使用动态框架 +- 支持 iOS 13.0 及以上版本 +- 已配置框架冲突处理脚本 diff --git a/tools/YYUtility/LICENSE b/tools/YYUtility/LICENSE new file mode 100644 index 0000000..a985831 --- /dev/null +++ b/tools/YYUtility/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 YY Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index e3f3265..2fe53d0 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -6,13 +6,50 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8529F57AF9337F626C670ED /* Pods_yana.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 4C4C8FC12DE5AF9200384527 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4C3E65172DB61F7A00E5A455 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4C3E651E2DB61F7A00E5A455; + remoteInfo = yana; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 0977E1E6E883533DD125CAD4 /* Pods-yana.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.debug.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.debug.xcconfig"; sourceTree = ""; }; 4C3E651F2DB61F7A00E5A455 /* yana.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = yana.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C4C8FBD2DE5AF9200384527 /* yanaAPITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = yanaAPITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 977CD030E95CB064179F3A1B /* Pods-yana.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.release.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.release.xcconfig"; sourceTree = ""; }; + D8529F57AF9337F626C670ED /* Pods_yana.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_yana.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 4CD4A30C2DBA2D6A00F49950 /* Exceptions for "yana" folder in "yana" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + "yana-Bridging-Header.h", + ); + target = 4C3E651E2DB61F7A00E5A455 /* yana */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ - 4C3E65212DB61F7A00E5A455 /* yana */ = { + 4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = { isa = PBXFileSystemSynchronizedRootGroup; + path = yanaAPITests; + sourceTree = ""; + }; + 4C55BD992DB64C3C0021505D /* yana */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4CD4A30C2DBA2D6A00F49950 /* Exceptions for "yana" folder in "yana" target */, + ); path = yana; sourceTree = ""; }; @@ -20,6 +57,14 @@ /* Begin PBXFrameworksBuildPhase section */ 4C3E651C2DB61F7A00E5A455 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C4C8FBA2DE5AF9200384527 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -32,8 +77,12 @@ 4C3E65162DB61F7A00E5A455 = { isa = PBXGroup; children = ( - 4C3E65212DB61F7A00E5A455 /* yana */, + 4C4C8FE72DE6F05300384527 /* tools */, + 4C55BD992DB64C3C0021505D /* yana */, + 4C4C8FBE2DE5AF9200384527 /* yanaAPITests */, 4C3E65202DB61F7A00E5A455 /* Products */, + 87A8B7A8B4E2D53BA55B66D1 /* Pods */, + 556C2003CCDA5AC2C56882D0 /* Frameworks */, ); sourceTree = ""; }; @@ -41,35 +90,92 @@ isa = PBXGroup; children = ( 4C3E651F2DB61F7A00E5A455 /* yana.app */, + 4C4C8FBD2DE5AF9200384527 /* yanaAPITests.xctest */, ); name = Products; sourceTree = ""; }; + 4C4C8FE72DE6F05300384527 /* tools */ = { + isa = PBXGroup; + children = ( + ); + path = tools; + sourceTree = ""; + }; + 556C2003CCDA5AC2C56882D0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D8529F57AF9337F626C670ED /* Pods_yana.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 87A8B7A8B4E2D53BA55B66D1 /* Pods */ = { + isa = PBXGroup; + children = ( + 0977E1E6E883533DD125CAD4 /* Pods-yana.debug.xcconfig */, + 977CD030E95CB064179F3A1B /* Pods-yana.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + 4C4C90522DE6FCF700384527 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ 4C3E651E2DB61F7A00E5A455 /* yana */ = { isa = PBXNativeTarget; buildConfigurationList = 4C3E652A2DB61F7B00E5A455 /* Build configuration list for PBXNativeTarget "yana" */; buildPhases = ( + E0BDB6E67FEFE696E7D48CE4 /* [CP] Check Pods Manifest.lock */, + 4C4C90522DE6FCF700384527 /* Headers */, 4C3E651B2DB61F7A00E5A455 /* Sources */, 4C3E651C2DB61F7A00E5A455 /* Frameworks */, 4C3E651D2DB61F7A00E5A455 /* Resources */, + 80E012C82442392F08611AA3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( - 4C3E65212DB61F7A00E5A455 /* yana */, + 4C55BD992DB64C3C0021505D /* yana */, ); name = yana; - packageProductDependencies = ( - ); productName = yana; productReference = 4C3E651F2DB61F7A00E5A455 /* yana.app */; productType = "com.apple.product-type.application"; }; + 4C4C8FBC2DE5AF9200384527 /* yanaAPITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4C4C8FC32DE5AF9200384527 /* Build configuration list for PBXNativeTarget "yanaAPITests" */; + buildPhases = ( + 4C4C8FB92DE5AF9200384527 /* Sources */, + 4C4C8FBA2DE5AF9200384527 /* Frameworks */, + 4C4C8FBB2DE5AF9200384527 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4C4C8FC22DE5AF9200384527 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 4C4C8FBE2DE5AF9200384527 /* yanaAPITests */, + ); + name = yanaAPITests; + productName = yanaAPITests; + productReference = 4C4C8FBD2DE5AF9200384527 /* yanaAPITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -83,6 +189,10 @@ 4C3E651E2DB61F7A00E5A455 = { CreatedOnToolsVersion = 16.3; }; + 4C4C8FBC2DE5AF9200384527 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = 4C3E651E2DB61F7A00E5A455; + }; }; }; buildConfigurationList = 4C3E651A2DB61F7A00E5A455 /* Build configuration list for PBXProject "yana" */; @@ -100,6 +210,7 @@ projectRoot = ""; targets = ( 4C3E651E2DB61F7A00E5A455 /* yana */, + 4C4C8FBC2DE5AF9200384527 /* yanaAPITests */, ); }; /* End PBXProject section */ @@ -112,8 +223,61 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4C4C8FBB2DE5AF9200384527 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 80E012C82442392F08611AA3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E0BDB6E67FEFE696E7D48CE4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-yana-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 4C3E651B2DB61F7A00E5A455 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -122,8 +286,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4C4C8FB92DE5AF9200384527 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 4C4C8FC22DE5AF9200384527 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4C3E651E2DB61F7A00E5A455 /* yana */; + targetProxy = 4C4C8FC12DE5AF9200384527 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 4C3E65282DB61F7B00E5A455 /* Debug */ = { isa = XCBuildConfiguration; @@ -248,6 +427,7 @@ }; 4C3E652B2DB61F7B00E5A455 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0977E1E6E883533DD125CAD4 /* Pods-yana.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -255,27 +435,50 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)", + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjack.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SVGKit/SVGKit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/libwebp/libwebp.framework/Headers\"", + ); + INFOPLIST_FILE = yana/Info.plist; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "此App将可发现和连接到您所用网络上的设备。"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "“MoliStar”需要您的同意,才可以进行定位服务,访问网络状态"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 20.20.61; PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; 4C3E652C2DB61F7B00E5A455 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 977CD030E95CB064179F3A1B /* Pods-yana.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -283,22 +486,90 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)", + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjack.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SVGKit/SVGKit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/libwebp/libwebp.framework/Headers\"", + ); + INFOPLIST_FILE = yana/Info.plist; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "此App将可发现和连接到您所用网络上的设备。"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "“MoliStar”需要您的同意,才可以进行定位服务,访问网络状态"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 20.20.61; PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 4C4C8FC42DE5AF9200384527 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = EKM7RAGNA6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yana.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yana"; + }; + name = Debug; + }; + 4C4C8FC52DE5AF9200384527 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = EKM7RAGNA6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yana.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yana"; }; name = Release; }; @@ -323,6 +594,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 4C4C8FC32DE5AF9200384527 /* Build configuration list for PBXNativeTarget "yanaAPITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4C4C8FC42DE5AF9200384527 /* Debug */, + 4C4C8FC52DE5AF9200384527 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */; diff --git a/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist b/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist index 482e391..770c8f3 100644 --- a/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/yana.xcodeproj/xcuserdata/edwinqqq.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ yana.xcscheme_^#shared#^_ orderHint - 0 + 23 diff --git a/yana.xcworkspace/contents.xcworkspacedata b/yana.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..670a2e6 --- /dev/null +++ b/yana.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..bcb484c --- /dev/null +++ b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yana/APIs/API.swift b/yana/APIs/API.swift new file mode 100644 index 0000000..540af6c --- /dev/null +++ b/yana/APIs/API.swift @@ -0,0 +1,59 @@ +import Foundation +import Alamofire + +enum HttpRequestMethod: String { + case get = "GET" + case post = "POST" + // 可扩展其他方法 +} + +typealias HttpRequestCompletion = (Result) -> Void + +class API { + // 通用请求方法 + static func makeRequest( + route: String, + method: HttpRequestMethod, + params: [String: Any], + completion: @escaping HttpRequestCompletion + ) { + let httpMethod: HTTPMethod = { + switch method { + case .get: return .get + case .post: return .post + } + }() + + NetworkManager.shared.request(route, method: httpMethod, parameters: params) { (result: Result) in + switch result { + case .success(let data): + completion(.success(data)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + // 具体接口方法示例 + static func getUserInfo(uid: String, completion: @escaping HttpRequestCompletion) { + let route = "user/get" + let params = ["uid": uid] + makeRequest(route: route, method: .get, params: params, completion: completion) + } + + static func phoneSmsCode(mobile: String, type: String, phoneAreaCode: String, completion: @escaping HttpRequestCompletion) { + let route = "sms/getCode" + let params = ["mobile": mobile, "type": type, "phoneAreaCode": phoneAreaCode] + makeRequest(route: route, method: .post, params: params, completion: completion) + } +} + +extension API //ClientConfig +{ + static func clientInit(completion: @escaping HttpRequestCompletion) { + makeRequest(route: "client/init", + method: .get, + params: [:], + completion: completion) + } +} diff --git a/yana/AppDelegate.swift b/yana/AppDelegate.swift new file mode 100644 index 0000000..6fa6bfb --- /dev/null +++ b/yana/AppDelegate.swift @@ -0,0 +1,9 @@ +import UIKit +import NIMSDK + +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + NIMConfigurationManager.setupNimSDK() + return true + } +} diff --git a/yana/Configs/AppConfig.swift b/yana/Configs/AppConfig.swift index e4ba840..fb85349 100644 --- a/yana/Configs/AppConfig.swift +++ b/yana/Configs/AppConfig.swift @@ -15,9 +15,9 @@ struct AppConfig { static var baseURL: String { switch current { case .development: - return "https://dev-api.yourdomain.com/v1" + return "http://beta.api.molistar.xyz" case .production: - return "https://api.yourdomain.com/v1" + return "https://api.hfighting.com" } } diff --git a/yana/Configs/ClientConfig.swift b/yana/Configs/ClientConfig.swift new file mode 100644 index 0000000..af2d197 --- /dev/null +++ b/yana/Configs/ClientConfig.swift @@ -0,0 +1,32 @@ +import Foundation + +final class ClientConfig { + static let shared = ClientConfig() + private init() {} + + func initializeClient() { + print("开始初始化客户端") + + NetworkManager.shared.enhancedRequest( + path: "client/init", + method: .get, + responseType: Data.self + ) { result in + switch result { + case .success(let response): + print("初始化成功,状态码:\(response.statusCode)") + if let data = response.data { + do { + let json = try JSONSerialization.jsonObject(with: data) + print("响应数据:\(json)") + } catch { + print("JSON解析失败:\(error)") + } + } + + case .failure(let error): + print("初始化失败:\(error.localizedDescription)") + } + } + } +} diff --git a/yana/ContentView.swift b/yana/ContentView.swift index 6cfde67..309788f 100644 --- a/yana/ContentView.swift +++ b/yana/ContentView.swift @@ -8,15 +8,71 @@ import SwiftUI struct ContentView: View { + @State private var account = "" + @State private var password = "" + + #if DEBUG + init() { + _account = State(initialValue: "3184") + _password = State(initialValue: "a0d5da073d14731cc7a01ecaa17b9174") + } + #endif + @State private var isLoading = false + @State private var loginError: String? + var body: some View { VStack { + // 新增测试按钮 + Button("测试初始化") { + ClientConfig.shared.initializeClient() + } + .padding(.top, 20) + + TextField("账号", text: $account) + .textFieldStyle(.roundedBorder) + .padding() + + SecureField("密码", text: $password) + .textFieldStyle(.roundedBorder) + .padding() + + Button(action: handleLogin) { + if isLoading { + ProgressView() + } else { + Text("登录") + } + } + .disabled(isLoading) + .alert("登录错误", isPresented: .constant(loginError != nil)) { + Button("确定") { loginError = nil } + } message: { + Text(loginError ?? "") + } + Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) - Text("Hello, world!") + Text("Hello, yana!") } .padding() } + + private func handleLogin() { + isLoading = true + NIMSessionManager.shared + .autoLogin(account: account, token: password) { error in + if let error = error { + loginError = error.localizedDescription + } else { + // 登录成功处理 + } + } +// NIMSessionManager.shared.login(account: account, token: password) { error in +// isLoading = false +// +// } + } } #Preview { diff --git a/yana/Info.plist b/yana/Info.plist new file mode 100644 index 0000000..6a6654d --- /dev/null +++ b/yana/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/yana/Managers/LogManager.swift b/yana/Managers/LogManager.swift new file mode 100644 index 0000000..228f1cf --- /dev/null +++ b/yana/Managers/LogManager.swift @@ -0,0 +1,51 @@ +import Foundation + +/// 日志等级 +public enum LogLevel: Int { + case verbose = 0 + case debug + case info + case warn + case error +} + +public class LogManager { + /// 单例 + public static let shared = LogManager() + private init() {} + + /// 日志输出 + /// - Parameters: + /// - level: 日志等级 + /// - message: 日志内容 + /// - onlyRelease: 是否仅在 Release 环境输出(默认 false,Debug 全部输出) + public func log(_ level: LogLevel, _ message: @autoclosure () -> String, onlyRelease: Bool = false) { + #if DEBUG + if onlyRelease { return } + print("[\(level)] \(message())") + #else + print("[\(level)] \(message())") + #endif + } +} + +// MARK: - 快捷方法 +public func logVerbose(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { + LogManager.shared.log(.verbose, message(), onlyRelease: onlyRelease) +} + +public func logDebug(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { + LogManager.shared.log(.debug, message(), onlyRelease: onlyRelease) +} + +public func logInfo(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { + LogManager.shared.log(.info, message(), onlyRelease: onlyRelease) +} + +public func logWarn(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { + LogManager.shared.log(.warn, message(), onlyRelease: onlyRelease) +} + +public func logError(_ message: @autoclosure () -> String, onlyRelease: Bool = false) { + LogManager.shared.log(.error, message(), onlyRelease: onlyRelease) +} \ No newline at end of file diff --git a/yana/Managers/NIMConfigurationManager.swift b/yana/Managers/NIMConfigurationManager.swift index 44fe559..19a9115 100644 --- a/yana/Managers/NIMConfigurationManager.swift +++ b/yana/Managers/NIMConfigurationManager.swift @@ -1,10 +1,33 @@ import NIMSDK +import NECoreKit +import NECoreIM2Kit +import NEChatKit +import NEChatUIKit struct NIMConfigurationManager { - static func setupSDK() { - NIMSDK.shared().register( - withAppID: "79bc37000f4018a2a24ea9dc6ca08d32", - cerName: "pikoDevelopPush" - ) + + static func setupNimSDK() { + let option = configureNIMSDKOption() + setupSDK(with: option) + setupChatSDK(with: option) } -} \ No newline at end of file + + static func setupSDK(with option: NIMSDKOption) { + NIMSDK.shared().register(with: option) + NIMSDKConfig.shared().shouldConsiderRevokedMessageUnreadCount = true + NIMSDKConfig.shared().shouldSyncStickTopSessionInfos = true + } + + static func setupChatSDK(with option: NIMSDKOption) { + let v2Option = V2NIMSDKOption() + v2Option.enableV2CloudConversation = false + IMKitClient.instance.setupIM2(option, v2Option) + } + + static func configureNIMSDKOption() -> NIMSDKOption { + let option = NIMSDKOption() + option.appKey = "79bc37000f4018a2a24ea9dc6ca08d32" + option.apnsCername = "pikoDevelopPush" + return option + } +} diff --git a/yana/Managers/NIMSessionManager.swift b/yana/Managers/NIMSessionManager.swift new file mode 100644 index 0000000..16287f1 --- /dev/null +++ b/yana/Managers/NIMSessionManager.swift @@ -0,0 +1,127 @@ +import Foundation +import NIMSDK + +// MARK: - 网络状态通知 +extension Notification.Name { + static let NIMNetworkStateChanged = Notification.Name("NIMNetworkStateChangedNotification") + static let NIMTokenExpired = Notification.Name("NIMTokenExpiredNotification") +} + +@objc +@objcMembers +final class NIMSessionManager: NSObject { + + static let shared = NIMSessionManager() + + // MARK: - 登录管理 + func autoLogin(account: String, token: String, completion: @escaping (Error?) -> Void) { + NIMSDK.shared().v2LoginService.add(self) + let data = NIMAutoLoginData() + data.account = account + data.token = token + data.forcedMode = false + NIMSDK.shared().loginManager.autoLogin(data) + } + + func login(account: String, token: String, completion: @escaping (Error?) -> Void) { + NIMSDK.shared().loginManager.login(account, token: token) { error in + if error == nil { + self.registerObservers() + } + completion(error) + } + } + + func logout() { + NIMSDK.shared().loginManager.logout { _ in + self.removeObservers() + } + } + + // MARK: - 消息监听 + private func registerObservers() { + // 在 autoLogin 方法中 +// NIMSDK.shared().v2LoginService.add(self as! V2NIMLoginServiceDelegate) + + // 在 registerObservers 方法中 +// NIMSDK.shared().v2LoginService.add(self as! V2NIMLoginServiceDelegate) + + // 在 removeObservers 方法中 +// NIMSDK.shared().v2LoginService.remove(self as! V2NIMLoginServiceDelegate) + NIMSDK.shared().chatManager.add(self) + NIMSDK.shared().loginManager.add(self) + } + + private func removeObservers() { + NIMSDK.shared().v2LoginService.remove(self) + NIMSDK.shared().chatManager.remove(self) + NIMSDK.shared().loginManager.remove(self) + } +} + +// MARK: - NIMChatManagerDelegate +extension NIMSessionManager: NIMChatManagerDelegate { + func onRecvMessages(_ messages: [NIMMessage]) { + NotificationCenter.default.post( + name: .NIMDidReceiveMessage, + object: messages + ) + } +} + +// MARK: - NIMLoginManagerDelegate +extension NIMSessionManager: NIMLoginManagerDelegate { + func onLogin(_ step: NIMLoginStep) { + NotificationCenter.default.post( + name: .NIMLoginStateChanged, + object: step + ) + } + + func onAutoLoginFailed(_ error: Error) { + if (error as NSError).code == 302 { + NotificationCenter.default.post(name: .NIMTokenExpired, object: nil) + } + } +} + +// MARK: - 通知定义 +extension Notification.Name { + static let NIMDidReceiveMessage = Notification.Name("NIMDidReceiveMessageNotification") + static let NIMLoginStateChanged = Notification.Name("NIMLoginStateChangedNotification") +} + +// MARK: - NIMV2LoginServiceDelegate +extension NIMSessionManager: V2NIMLoginListener { + func onLoginStatus(_ status: V2NIMLoginStatus) { + + } + + func onLoginFailed(_ error: V2NIMError) { + + } + + func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) { + + } + + func onLoginClientChanged( + _ change: V2NIMLoginClientChange, + clients: [V2NIMLoginClient]? + ) { + + } +// @objc func onLoginProcess(step: NIMV2LoginStep) { +// NotificationCenter.default.post( +// name: .NIMV2LoginStateChanged, +// object: step +// ) +// } +// +// @objc func onKickOut(result: NIMKickOutResult) { +// NotificationCenter.default.post( +// name: .NIMKickOutNotification, +// object: result +// ) +// } +} diff --git a/yana/Managers/NetworkManager.swift b/yana/Managers/NetworkManager.swift new file mode 100644 index 0000000..029e38e --- /dev/null +++ b/yana/Managers/NetworkManager.swift @@ -0,0 +1,667 @@ +import Foundation +import Alamofire +import CoreTelephony +import UIKit +import Darwin // 用于 utsname 结构体 +import CommonCrypto + +// 配置类 +//enum AppConfig { +// static let baseURL = "https://api.example.com" // 请替换为实际的 API 基础 URL +//} + +// 网络状态枚举 +enum NetworkStatus: Int { + case notReachable = 0 + case reachableViaWWAN = 1 + case reachableViaWiFi = 2 +} + +// 扩展错误类型 +enum NetworkError: Error { + case invalidURL + case requestFailed(statusCode: Int, message: String?) + case invalidResponse + case decodingFailed + case networkUnavailable + case serverError(message: String) + case unauthorized + case rateLimited + + var localizedDescription: String { + switch self { + case .invalidURL: + return "无效的 URL" + case .requestFailed(let statusCode, let message): + return "请求失败: \(statusCode), \(message ?? "未知错误")" + case .invalidResponse: + return "无效的响应" + case .decodingFailed: + return "数据解析失败" + case .networkUnavailable: + return "网络不可用" + case .serverError(let message): + return "服务器错误: \(message)" + case .unauthorized: + return "未授权访问" + case .rateLimited: + return "请求过于频繁" + } + } +} + +// MARK: - MD5 加密扩展 +extension String { + func md5() -> String { + let str = self.cString(using: .utf8) + let strLen = CUnsignedInt(self.lengthOfBytes(using: .utf8)) + let digestLen = Int(CC_MD5_DIGEST_LENGTH) + let result = UnsafeMutablePointer.allocate(capacity: digestLen) + + CC_MD5(str!, strLen, result) + + let hash = NSMutableString() + for i in 0.. { + let statusCode: Int + let data: T? + let headers: [AnyHashable: Any] + let metrics: URLSessionTaskMetrics? + + var isSuccessful: Bool { + return (200...299).contains(statusCode) + } + } + + private let reachability = NetworkReachabilityManager() + private var isNetworkReachable = false + private let baseURL = AppConfig.baseURL + private let session: Session + private let retryLimit = 2 + + // 网络状态监听回调 + var networkStatusChanged: ((NetworkStatus) -> Void)? + + init() { + let configuration = URLSessionConfiguration.af.default + configuration.httpShouldSetCookies = true + configuration.httpCookieAcceptPolicy = .always + configuration.timeoutIntervalForRequest = 60 + configuration.timeoutIntervalForResource = 60 + configuration.httpMaximumConnectionsPerHost = 10 + + // 支持的内容类型 + configuration.httpAdditionalHeaders = [ + "Accept": "application/json, text/json, text/javascript, text/html, text/plain, image/jpeg, image/png", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json" + ] + + // 强制 TLS 1.2+ 并禁用 HTTP/1.1 + configuration.tlsMinimumSupportedProtocolVersion = .TLSv12 + configuration.httpShouldUsePipelining = true + + // 重试策略 + let retrier = RetryPolicy(retryLimit: UInt(retryLimit)) + + session = Session( + configuration: configuration, + interceptor: retrier, + eventMonitors: [AlamofireLogger()] + ) + + // 添加网络可达性监听 + setupReachability() + } + + private func setupReachability() { + reachability?.startListening { [weak self] status in + guard let self = self else { return } + + switch status { + case .reachable(.ethernetOrWiFi): + self.isNetworkReachable = true + self.networkStatusChanged?(.reachableViaWiFi) + case .reachable(.cellular): + self.isNetworkReachable = true + self.networkStatusChanged?(.reachableViaWWAN) + case .notReachable: + self.isNetworkReachable = false + self.networkStatusChanged?(.notReachable) + case .unknown: + self.isNetworkReachable = false + self.networkStatusChanged?(.notReachable) + case .reachable(_): + self.isNetworkReachable = true + self.networkStatusChanged?(.reachableViaWiFi) + @unknown default: + fatalError("未知的网络状态") + } + } + } + + // MARK: - 便利方法 + + /// 发送 GET 请求 + func get( + path: String, + queryItems: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + enhancedRequest( + path: path, + method: .get, + queryItems: queryItems, + responseType: T.self + ) { result in + switch result { + case .success(let response): + if let data = response.data as? T { + completion(.success(data)) + } else { + completion(.failure(.decodingFailed)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + /// 发送 POST 请求 + func post( + path: String, + parameters: P, + completion: @escaping (Result) -> Void + ) { + enhancedRequest( + path: path, + method: .post, + bodyParameters: parameters, + responseType: T.self + ) { result in + switch result { + case .success(let response): + if let data = response.data as? T { + completion(.success(data)) + } else { + completion(.failure(.decodingFailed)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: - 核心请求方法 + func enhancedRequest( + path: String, + method: HTTPMethod = .get, + queryItems: [String: String]? = nil, + bodyParameters: Encodable? = nil, + responseType: T.Type = Data.self, + completion: @escaping (Result, NetworkError>) -> Void + ) { + // 前置网络检查 + guard isNetworkReachable else { + completion(.failure(.networkUnavailable)) + return + } + + guard let baseURL = URL(string: baseURL) else { + completion(.failure(.invalidURL)) + return + } + + var urlComponents = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true) + urlComponents?.queryItems = queryItems?.map { URLQueryItem(name: $0.key, value: $0.value) } + + guard let finalURL = urlComponents?.url else { + completion(.failure(.invalidURL)) + return + } + + // 合并基础参数和自定义参数 + // TODO: 补充加密验参:pub_sign + let baseParams = BaseParameters() + var parameters: Parameters = baseParams.dictionary ?? [:] + + if let customParams = bodyParameters { + if let dict = try? customParams.asDictionary() { + parameters.merge(dict) { (_, new) in new } + } + } + + session.request(finalURL, method: method, parameters: parameters, encoding: JSONEncoding.default, headers: commonHeaders) + .validate() + .responseData { [weak self] response in + guard let self = self else { return } + + let statusCode = response.response?.statusCode ?? -1 + let headers = response.response?.allHeaderFields ?? [:] + let metrics = response.metrics + + switch response.result { + case .success(let decodedData): + do { + let resultData: T + if T.self == Data.self { + resultData = decodedData as! T + } else if let decodableData = decodedData as? T { + resultData = decodableData + } else { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: NetworkError.decodingFailed)) + } + + let networkResponse = NetworkResponse( + statusCode: statusCode, + data: resultData, + headers: headers, + metrics: metrics + ) + + if networkResponse.isSuccessful { + completion(.success(networkResponse)) + } else { + self.handleErrorResponse(statusCode: statusCode, completion: completion) + } + } catch { + completion(.failure(.decodingFailed)) + } + + case .failure(let error): + self.handleRequestError(error, statusCode: statusCode, completion: completion) + } + } + } + + // MARK: - 错误处理 + + private func handleErrorResponse( + statusCode: Int, + completion: (Result, NetworkError>) -> Void + ) { + switch statusCode { + case 401: + completion(.failure(.unauthorized)) + case 429: + completion(.failure(.rateLimited)) + case 500...599: + completion(.failure(.serverError(message: "服务器错误 \(statusCode)"))) + default: + completion(.failure(.requestFailed(statusCode: statusCode, message: "请求失败"))) + } + } + + private func handleRequestError( + _ error: AFError, + statusCode: Int, + completion: (Result, NetworkError>) -> Void + ) { + if let underlyingError = error.underlyingError as? URLError { + switch underlyingError.code { + case .notConnectedToInternet: + completion(.failure(.networkUnavailable)) + default: + completion(.failure(.requestFailed( + statusCode: statusCode, + message: underlyingError.localizedDescription + ))) + } + } else { + completion(.failure(.invalidResponse)) + } + } + + // MARK: - 公共头信息 + private var commonHeaders: HTTPHeaders { + var headers = HTTPHeaders() + + // 公共头信息 + if let language = Locale.preferredLanguages.first { + headers.add(name: "Accept-Language", value: language) + } + headers.add(name: "App-Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0") + + // 登录相关头信息 + let uid = "" //AccountInfoStorage.instance?.getUid() ?? "" + let ticket = "" //AccountInfoStorage.instance?.getTicket() ?? "" + headers.add(name: "pub_uid", value: uid) + headers.add(name: "pub_ticket", value: ticket) + + return headers + } + + // MARK: - 公共请求方法 + func request( + _ path: String, + method: HTTPMethod = .get, + parameters: P? = nil, + completion: @escaping (Result) -> Void + ) { + enhancedRequest( + path: path, + method: method, + bodyParameters: parameters, + responseType: T.self + ) { result in + switch result { + case .success(let response): + if let data = response.data { + completion(.success(data)) + } else { + completion(.failure(.decodingFailed)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + // 添加一个便利方法,用于处理字典参数 + func request( + _ path: String, + method: HTTPMethod = .get, + parameters: [String: Any]? = nil, + completion: @escaping (Result) -> Void + ) { + // 将字典转换为 Data,然后再解码为 [String: String] + if let params = parameters { + do { + let jsonData = try JSONSerialization.data(withJSONObject: params) + let decoder = JSONDecoder() + let encodableParams = try decoder.decode([String: String].self, from: jsonData) + + request(path, method: method, parameters: encodableParams, completion: completion) + } catch { + completion(.failure(.decodingFailed)) + } + } else { + // 如果没有参数,使用空字典 + request(path, method: method, parameters: [String: String](), completion: completion) + } + } + + // MARK: - 语言管理 + func getCurrentLanguage() -> String { + return LanguageManager.getCurrentLanguage() + } + + func updateLanguage(_ language: String) { + LanguageManager.updateLanguage(language) + } +} + +// MARK: - Logger +final class AlamofireLogger: EventMonitor { + func requestDidResume(_ request: Request) { + let allHeaders = request.request?.allHTTPHeaderFields ?? [:] + let relevantHeaders = allHeaders.filter { !$0.key.contains("Authorization") } + + print("🚀 Request Started: \(request.description)") + print("📝 Headers: \(relevantHeaders)") + + if let httpBody = request.request?.httpBody, + let parameters = try? JSONSerialization.jsonObject(with: httpBody) { + print("📦 Parameters: \(parameters)") + } + } + + func request(_ request: DataRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, data: Data?) { + print("📥 Response Status: \(response.statusCode)") + + if let data = data, + let json = try? JSONSerialization.jsonObject(with: data) { + print("📄 Response Data: \(json)") + } + } +} + +// MARK: - Encodable Extension +private extension Encodable { + var dictionary: [String: Any]? { + guard let data = try? JSONEncoder().encode(self) else { return nil } + return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] } + } + + func asDictionary() throws -> [String: Any] { + let data = try JSONEncoder().encode(self) + guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { + throw NetworkError.decodingFailed + } + return dictionary + } +} + +// MARK: - 语言管理 +enum LanguageManager { + static let languageKey = "UserSelectedLanguage" + + // 映射语言代码 + static func mapLanguage(_ language: String) -> String { + // 处理完整的语言代码,如 "zh-Hans"、"zh-Hant"、"zh-HK" 等 + if language.hasPrefix("zh-Hans") || language.hasPrefix("zh-CN") { + return "zh-Hant" // 简体中文也返回繁体 + } else if language.hasPrefix("zh") { + return "zh-Hant" // 其他中文变体都返回繁体 + } else if language.hasPrefix("ar") { + return "ar" // 阿拉伯语 + } else if language.hasPrefix("tr") { + return "tr" // 土耳其语 + } else { + return "en" // 默认英文 + } + } + + // 获取当前语言 + static func getCurrentLanguage() -> String { + // 先从 UserDefaults 获取 + if let savedLanguage = UserDefaults.standard.string(forKey: languageKey) { + return savedLanguage + } + + // 获取系统首选语言 + let preferredLanguages = Locale.preferredLanguages.first +// let systemLanguage = preferredLanguages.first ?? Locale.current.languageCode ?? "en" + + // 映射并保存语言设置 + let mappedLanguage = mapLanguage(preferredLanguages ?? "en") + UserDefaults.standard.set(mappedLanguage, forKey: languageKey) + UserDefaults.standard.synchronize() + + return mappedLanguage + } + + // 更新语言设置 + static func updateLanguage(_ language: String) { + let mappedLanguage = mapLanguage(language) + UserDefaults.standard.set(mappedLanguage, forKey: languageKey) + UserDefaults.standard.synchronize() + } + + // 获取系统语言代码(调试用) + static func getSystemLanguageInfo() -> (preferred: [String], current: String?) { + return ( + Bundle.main.preferredLocalizations, + Locale.current.languageCode + ) + } +} + +// MARK: - 设备型号管理 +enum DeviceManager { + // 获取设备标识符 + static func getDeviceIdentifier() -> String { + var systemInfo = utsname() + uname(&systemInfo) + let machineMirror = Mirror(reflecting: systemInfo.machine) + let identifier = machineMirror.children.reduce("") { identifier, element in + guard let value = element.value as? Int8, value != 0 else { return identifier } + return identifier + String(UnicodeScalar(UInt8(value))) + } + return identifier + } + + // 映射设备标识符到具体型号 + static func mapDeviceModel(_ identifier: String) -> String { + switch identifier { + // iPhone + case "iPhone13,1": return "iPhone 12 mini" + case "iPhone13,2": return "iPhone 12" + case "iPhone13,3": return "iPhone 12 Pro" + case "iPhone13,4": return "iPhone 12 Pro Max" + case "iPhone14,4": return "iPhone 13 mini" + case "iPhone14,5": return "iPhone 13" + case "iPhone14,2": return "iPhone 13 Pro" + case "iPhone14,3": return "iPhone 13 Pro Max" + case "iPhone14,7": return "iPhone 14" + case "iPhone14,8": return "iPhone 14 Plus" + case "iPhone15,2": return "iPhone 14 Pro" + case "iPhone15,3": return "iPhone 14 Pro Max" + case "iPhone15,4": return "iPhone 15" + case "iPhone15,5": return "iPhone 15 Plus" + case "iPhone16,1": return "iPhone 15 Pro" + case "iPhone16,2": return "iPhone 15 Pro Max" + + // iPad + case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)" + case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro 12.9-inch (5th generation)" + case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11": return "iPad Pro 12.9-inch (6th generation)" + + // iPod + case "iPod9,1": return "iPod touch (7th generation)" + + // 模拟器 + case "i386", "x86_64", "arm64": return "Simulator \(mapDeviceModel(ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))" + + default: return identifier // 如果找不到对应的型号,返回原始标识符 + } + } + + // 获取具体的设备型号 + static func getDeviceModel() -> String { + let identifier = getDeviceIdentifier() + return mapDeviceModel(identifier) + } +} + +// MARK: - 渠道管理 +enum ChannelManager { + static let enterpriseBundleId = "com.stupidmonkey.yana.yana"//"com.hflighting.yumi" + + enum ChannelType: String { + case enterprise = "enterprise" + case testflight = "testflight" + case appstore = "appstore" + } + + // 判断是否企业版 + static func isEnterprise() -> Bool { + let bundleId = Bundle.main.bundleIdentifier ?? "" + return bundleId == enterpriseBundleId + } + + // 判断是否 TestFlight 版本 + static func isTestFlight() -> Bool { + return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" + } + + // 获取当前渠道 + static func getCurrentChannel() -> String { + if isEnterprise() { + return ChannelType.enterprise.rawValue + } else if isTestFlight() { + return ChannelType.testflight.rawValue + } else { + return ChannelType.appstore.rawValue + } + } +} diff --git a/yana/yana-Bridging-Header.h b/yana/yana-Bridging-Header.h new file mode 100644 index 0000000..1b2cb5d --- /dev/null +++ b/yana/yana-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/yana/yanaApp.swift b/yana/yanaApp.swift index 65255a2..7115b36 100644 --- a/yana/yanaApp.swift +++ b/yana/yanaApp.swift @@ -6,9 +6,12 @@ // import SwiftUI +import NIMSDK @main struct yanaApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var body: some Scene { WindowGroup { ContentView() diff --git a/yanaAPITests/yanaAPITests.swift b/yanaAPITests/yanaAPITests.swift new file mode 100644 index 0000000..41eaba1 --- /dev/null +++ b/yanaAPITests/yanaAPITests.swift @@ -0,0 +1,68 @@ +// +// yanaAPITests.swift +// yanaAPITests +// +// Created by P on 2025/5/27. +// + +import XCTest +@testable import yana + +final class yanaAPITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + + func testClientInit_Success() { + let expectation = self.expectation(description: "clientInit success") + API.clientInit { result in + switch result { + case .success(let data): + XCTAssertNotNil(data) + // 可根据实际返回内容进一步断言 + case .failure(let error): + XCTFail("Expected success, got error: \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testClientInit_Failure() { + // 可通过mock或断网等方式测试失败场景 + // 这里只做结构示例 + let expectation = self.expectation(description: "clientInit failure") + // 假设API支持注入baseURL或mock + API.clientInit { result in + switch result { + case .success(_): + // 若期望失败则此处应fail + XCTFail("Expected failure, got success") + case .failure(let error): + XCTAssertNotNil(error) + } + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } +}