feat: 添加项目基础文件和依赖管理

新增.gitignore、Podfile和Podfile.lock文件以管理项目依赖,添加README.md文件提供项目简介和安装步骤,创建NIMSessionManager、ClientConfig、LogManager和NetworkManager等管理类以支持网络请求和日志记录功能,更新AppDelegate和ContentView以集成NIM SDK和实现用户登录功能。
This commit is contained in:
edwinQQQ
2025-05-29 16:14:28 +08:00
parent 374cc654d7
commit a0200c8859
25 changed files with 2081 additions and 23 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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, Void> { state, action, _ in
switch action {
case .increment:
state.count += 1
return .none
case .decrement:
state.count -= 1
return .none
}
}
}

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
Pods
.vscode
yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
*.xcbkptlist

40
Podfile Normal file
View File

@@ -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

127
Podfile.lock Normal file
View File

@@ -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

78
README.md Normal file
View File

@@ -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 及以上版本
- 已配置框架冲突处理脚本

21
tools/YYUtility/LICENSE Normal file
View File

@@ -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.

View File

@@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>";
};
4C55BD992DB64C3C0021505D /* yana */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CD4A30C2DBA2D6A00F49950 /* Exceptions for "yana" folder in "yana" target */,
);
path = yana;
sourceTree = "<group>";
};
@@ -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 = "<group>";
};
@@ -41,35 +90,92 @@
isa = PBXGroup;
children = (
4C3E651F2DB61F7A00E5A455 /* yana.app */,
4C4C8FBD2DE5AF9200384527 /* yanaAPITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
4C4C8FE72DE6F05300384527 /* tools */ = {
isa = PBXGroup;
children = (
);
path = tools;
sourceTree = "<group>";
};
556C2003CCDA5AC2C56882D0 /* Frameworks */ = {
isa = PBXGroup;
children = (
D8529F57AF9337F626C670ED /* Pods_yana.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
87A8B7A8B4E2D53BA55B66D1 /* Pods */ = {
isa = PBXGroup;
children = (
0977E1E6E883533DD125CAD4 /* Pods-yana.debug.xcconfig */,
977CD030E95CB064179F3A1B /* Pods-yana.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
/* 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 */;

View File

@@ -7,7 +7,7 @@
<key>yana.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>23</integer>
</dict>
</dict>
</dict>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:yana.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "A60FAB2A-3184-45B2-920F-A3D7A086CF95"
type = "0"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "BF83E194-5D1D-4B84-AD21-2D4CDCC124DE"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NIMSessionManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "97"
endingLineNumber = "97"
landmarkName = "onLoginStatus(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "5E054207-7C17-4F34-A910-1C9F814EC837"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NIMSessionManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "101"
endingLineNumber = "101"
landmarkName = "onLoginFailed(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "164971C8-E03E-4FAD-891E-C07DFA41444D"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NIMSessionManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "105"
endingLineNumber = "105"
landmarkName = "onKickedOffline(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "9A59F819-E987-4891-AEDD-AE98333E1722"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NIMSessionManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "112"
endingLineNumber = "112"
landmarkName = "onLoginClientChanged(_:clients:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "1268A929-970C-4C74-B3E5-09976D796C5E"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NetworkManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "54"
endingLineNumber = "54"
landmarkName = "String"
landmarkType = "21">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "DB8B4E7E-87FD-4E45-86F7-D21CF24F3B08"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Configs/ClientConfig.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "14"
endingLineNumber = "14"
landmarkName = "initializeClient()"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "ADC3C5EC-46AE-4FDA-9FD6-D685B5C36044"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NetworkManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "519"
endingLineNumber = "519"
landmarkName = "request(_:didValidateRequest:response:data:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "492235D2-D281-4F70-B43C-C09990DC22EC"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NetworkManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "326"
endingLineNumber = "326"
landmarkName = "enhancedRequest(path:method:queryItems:bodyParameters:responseType:completion:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "198A1AE8-A7A4-4A66-A4D3-DF86D873E2AE"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "yana/Managers/NetworkManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "361"
endingLineNumber = "361"
landmarkName = "enhancedRequest(path:method:queryItems:bodyParameters:responseType:completion:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

59
yana/APIs/API.swift Normal file
View File

@@ -0,0 +1,59 @@
import Foundation
import Alamofire
enum HttpRequestMethod: String {
case get = "GET"
case post = "POST"
//
}
typealias HttpRequestCompletion = (Result<Data, Error>) -> 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<Data, NetworkError>) 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)
}
}

9
yana/AppDelegate.swift Normal file
View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -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)")
}
}
}
}

View File

@@ -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 {

11
yana/Info.plist Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -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 falseDebug
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)
}

View File

@@ -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)
}
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
}
}

View File

@@ -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
// )
// }
}

View File

@@ -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<UInt8>.allocate(capacity: digestLen)
CC_MD5(str!, strLen, result)
let hash = NSMutableString()
for i in 0..<digestLen {
hash.appendFormat("%02x", result[i])
}
result.deallocate()
return hash as String
}
}
//
struct BaseParameters: Encodable {
let acceptLanguage: String
let os: String = "iOS"
let osVersion: String
let ispType: String
let channel: String
let model: String
let deviceId: String
let appVersion: String
let app: String
let mcc: String?
let pub_sign: String
enum CodingKeys: String, CodingKey {
case acceptLanguage = "Accept-Language"
case os, osVersion, ispType, channel, model, deviceId
case appVersion, app, mcc
case pub_sign
}
init() {
// 使
self.acceptLanguage = LanguageManager.getCurrentLanguage()
//
self.osVersion = UIDevice.current.systemVersion
//
let networkInfo = CTTelephonyNetworkInfo()
var ispType = "65535"
var mcc: String? = nil // mcc
if #available(iOS 12.0, *) {
// 使 API
if let carriers = networkInfo.serviceSubscriberCellularProviders,
let carrier = carriers.values.first {
ispType = (
carrier.mobileNetworkCode != nil ? carrier.mobileNetworkCode : ""
) ?? "65535"
mcc = carrier.mobileCountryCode
}
} else {
//
if let carrier = networkInfo.subscriberCellularProvider {
ispType = (
carrier.mobileNetworkCode != nil ? carrier.mobileNetworkCode : ""
) ?? "65535"
mcc = carrier.mobileCountryCode
}
}
self.ispType = ispType
self.mcc = mcc // mcc
//
self.channel = ChannelManager.getCurrentChannel()
//
self.model = DeviceManager.getDeviceModel()
//
self.deviceId = UIDevice.current.identifierForVendor?.uuidString ?? ""
//
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
//
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? ""
// pub_sign
let key = "rpbs6us1m8r2j9g6u06ff2bo18orwaya"
let signString = "key=\(key)"
self.pub_sign = signString.md5().uppercased()
}
}
final class NetworkManager {
static let shared = NetworkManager()
//
struct NetworkResponse<T> {
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<T: Decodable>(
path: String,
queryItems: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> 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<T: Decodable, P: Encodable>(
path: String,
parameters: P,
completion: @escaping (Result<T, NetworkError>) -> 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<T>(
path: String,
method: HTTPMethod = .get,
queryItems: [String: String]? = nil,
bodyParameters: Encodable? = nil,
responseType: T.Type = Data.self,
completion: @escaping (Result<NetworkResponse<T>, 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<T>(
statusCode: Int,
completion: (Result<NetworkResponse<T>, 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<T>(
_ error: AFError,
statusCode: Int,
completion: (Result<NetworkResponse<T>, 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<T: Decodable, P: Encodable>(
_ path: String,
method: HTTPMethod = .get,
parameters: P? = nil,
completion: @escaping (Result<T, NetworkError>) -> 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<T: Decodable>(
_ path: String,
method: HTTPMethod = .get,
parameters: [String: Any]? = nil,
completion: @escaping (Result<T, NetworkError>) -> 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
}
}
}

View File

@@ -0,0 +1,4 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

View File

@@ -6,9 +6,12 @@
//
import SwiftUI
import NIMSDK
@main
struct yanaApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()

View File

@@ -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")
// APIbaseURLmock
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)
}
}