chore: initial commit

这个提交包含在:
XuqmGroup 2026-04-21 22:07:29 +08:00
当前提交 681850af38
共有 13 个文件被更改,包括 479 次插入0 次删除

10
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,10 @@
node_modules/
dist/
.DS_Store
*.class
target/
build/
.gradle/
*.iml
.idea/
*.log

61
PUBLISH.md 普通文件
查看文件

@ -0,0 +1,61 @@
# iOS SDK 发版流程
## 方式一CocoaPods + 私有 Spec 仓库(推荐)
### 1. 创建私有 Spec Repo只需一次
```bash
# 在 GitLab/GitHub 创建一个空仓库,例如 xuqm-specs
pod repo add xuqm-specs https://your-git-host.com/xuqm/xuqm-specs.git
```
### 2. 发版步骤
```bash
# 1. 更新 XuqmSDK.podspec 中的 version 字段
# 2. 提交代码并打 tag
git tag 0.1.0
git push origin 0.1.0
# 3. 验证 podspec
pod spec lint XuqmSDK.podspec --allow-warnings
# 4. 推送到私有 spec repo
pod repo push xuqm-specs XuqmSDK.podspec --allow-warnings
```
### 3. 客户端集成
```ruby
# Podfile
source 'https://your-git-host.com/xuqm/xuqm-specs.git'
source 'https://cdn.cocoapods.org/'
pod 'XuqmSDK', '~> 0.1.0'
```
---
## 方式二Swift Package Manager更现代,适合 Xcode 原生集成)
SPM 基于 Git 标签发版,无需额外基础设施:
```bash
# 打 tag 即可发版
git tag 0.1.0
git push origin 0.1.0
```
客户端在 Xcode → File → Add Package Dependencies 中填写 Git 仓库地址和版本号。
---
## 推荐选择
| 场景 | 推荐方式 |
|------|---------|
| 纯 Swift/SwiftUI 项目,Xcode 15+ | **SPM**(已有 Package.swift |
| 需要支持 ObjC 混编 / 已有 Cocoapods 体系 | **CocoaPods 私有 Spec** |
| 两种都要支持 | 同时维护 Package.swift + XuqmSDK.podspec |
当前项目已同时提供 `Package.swift`SPM`XuqmSDK.podspec`CocoaPods,两种方式均可使用。

22
Package.swift 普通文件
查看文件

@ -0,0 +1,22 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "XuqmSDK",
platforms: [.iOS(.v16), .macOS(.v13)],
products: [
.library(name: "XuqmSDK", targets: ["XuqmSDK"]),
],
targets: [
.target(
name: "XuqmSDK",
path: "Sources/XuqmSDK",
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
),
.testTarget(
name: "XuqmSDKTests",
dependencies: ["XuqmSDK"],
path: "Tests/XuqmSDKTests"
),
]
)

查看文件

@ -0,0 +1,53 @@
import Foundation
public struct ApiResponse<T: Decodable>: Decodable {
public let code: Int
public let status: String
public let data: T?
public let message: String
}
public final class ApiClient: @unchecked Sendable {
public static let shared = ApiClient()
private var config: SDKConfig?
private var session: URLSession = .shared
private init() {}
func configure(with config: SDKConfig) {
self.config = config
}
public func request<T: Decodable>(
path: String,
method: String = "GET",
queryItems: [URLQueryItem]? = nil,
body: (some Encodable)? = nil as String?
) async throws -> T {
guard let config else { throw URLError(.badURL) }
let tokenStore = await XuqmSDK.shared.tokenStore
var components = URLComponents(url: config.apiBaseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false)!
if let qi = queryItems { components.queryItems = qi }
var req = URLRequest(url: components.url!)
req.httpMethod = method
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let token = tokenStore?.get() {
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
if let b = body {
req.httpBody = try JSONEncoder().encode(b)
}
let (data, response) = try await session.data(for: req)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
let wrapper = try JSONDecoder().decode(ApiResponse<T>.self, from: data)
guard let result = wrapper.data else { throw URLError(.cannotDecodeContentData) }
return result
}
}

查看文件

@ -0,0 +1,26 @@
import Foundation
public struct SDKConfig: Sendable {
public let appId: String
public let appKey: String
public let appSecret: String
public let apiBaseURL: URL
public let imWebSocketURL: URL
public let debug: Bool
public init(
appId: String,
appKey: String,
appSecret: String,
apiBaseURL: URL,
imWebSocketURL: URL,
debug: Bool = false
) {
self.appId = appId
self.appKey = appKey
self.appSecret = appSecret
self.apiBaseURL = apiBaseURL
self.imWebSocketURL = imWebSocketURL
self.debug = debug
}
}

查看文件

@ -0,0 +1,18 @@
import Foundation
public final class TokenStore: @unchecked Sendable {
private let key = "com.xuqm.sdk.token"
public func save(_ token: String) {
UserDefaults.standard.set(token, forKey: key)
}
public func get() -> String? {
UserDefaults.standard.string(forKey: key)
}
public func clear() {
UserDefaults.standard.removeObject(forKey: key)
}
}

查看文件

@ -0,0 +1,24 @@
import Foundation
@MainActor
public final class XuqmSDK {
public static let shared = XuqmSDK()
private(set) var config: SDKConfig?
private(set) var tokenStore: TokenStore?
private init() {}
public func initialize(config: SDKConfig) {
self.config = config
self.tokenStore = TokenStore()
ApiClient.shared.configure(with: config)
}
public func requireConfig() -> SDKConfig {
guard let config else {
fatalError("XuqmSDK not initialized. Call XuqmSDK.shared.initialize() first.")
}
return config
}
}

查看文件

@ -0,0 +1,80 @@
import Foundation
public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked Sendable {
public weak var delegate: ImEventDelegate?
private var webSocketTask: URLSessionWebSocketTask?
private var session: URLSession?
private let wsURL: URL
private let token: String
private let appId: String
private var shouldReconnect = false
public init(wsURL: URL, token: String, appId: String) {
self.wsURL = wsURL
self.token = token
self.appId = appId
super.init()
}
public func connect() {
shouldReconnect = true
var request = URLRequest(url: wsURL)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
webSocketTask = session?.webSocketTask(with: request)
webSocketTask?.resume()
receiveMessage()
}
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
if case .string(let text) = message {
self?.handleMessage(text)
}
self?.receiveMessage()
case .failure(let error):
self?.delegate?.imClientDidError(error.localizedDescription)
}
}
}
private func handleMessage(_ text: String) {
guard let data = text.data(using: .utf8),
let msg = try? JSONDecoder().decode(ImMessage.self, from: data) else { return }
if msg.chatType == .group {
delegate?.imClientDidReceiveGroupMessage(msg)
} else {
delegate?.imClientDidReceiveMessage(msg)
}
}
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
let payload: [String: Any] = [
"type": "chat.send",
"data": ["appId": appId, "toId": toId, "chatType": chatType.rawValue,
"msgType": msgType.rawValue, "content": content]
]
guard let data = try? JSONSerialization.data(withJSONObject: payload),
let text = String(data: data, encoding: .utf8) else { return }
webSocketTask?.send(.string(text)) { _ in }
}
public func disconnect() {
shouldReconnect = false
webSocketTask?.cancel(with: .normalClosure, reason: nil)
}
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?) {
delegate?.imClientDidConnect()
}
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
let reasonStr = reason.flatMap { String(data: $0, encoding: .utf8) }
delegate?.imClientDidDisconnect(reason: reasonStr)
}
}

查看文件

@ -0,0 +1,40 @@
import Foundation
@MainActor
public final class ImSDK {
public static let shared = ImSDK()
private var client: ImClient?
private init() {}
public func login(userId: String, nickname: String? = nil, avatar: String? = nil) async throws {
let config = XuqmSDK.shared.requireConfig()
struct LoginResponse: Decodable { let token: String }
var items = [URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "userId", value: userId)]
if let n = nickname { items.append(URLQueryItem(name: "nickname", value: n)) }
if let a = avatar { items.append(URLQueryItem(name: "avatar", value: a)) }
let res: LoginResponse = try await ApiClient.shared.request(
path: "/api/im/auth/login", method: "POST", queryItems: items
)
XuqmSDK.shared.tokenStore?.save(res.token)
client = ImClient(wsURL: config.imWebSocketURL, token: res.token, appId: config.appId)
client?.connect()
}
public func setDelegate(_ delegate: ImEventDelegate) {
client?.delegate = delegate
}
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
client?.sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content)
}
public func disconnect() {
client?.disconnect()
client = nil
}
}

查看文件

@ -0,0 +1,47 @@
import Foundation
public enum ChatType: String, Codable, Sendable {
case single = "SINGLE"
case group = "GROUP"
}
public enum MsgType: String, Codable, Sendable {
case text = "TEXT"
case image = "IMAGE"
case video = "VIDEO"
case audio = "AUDIO"
case file = "FILE"
case custom = "CUSTOM"
case location = "LOCATION"
case notify = "NOTIFY"
case revoked = "REVOKED"
case forward = "FORWARD"
}
public enum MsgStatus: String, Codable, Sendable {
case sent = "SENT"
case delivered = "DELIVERED"
case read = "READ"
case revoked = "REVOKED"
}
public struct ImMessage: Codable, Sendable {
public let id: String
public let appId: String
public let fromUserId: String
public let toId: String
public let chatType: ChatType
public let msgType: MsgType
public let content: String
public let status: MsgStatus
public let mentionedUserIds: String?
public let createdAt: String
}
public protocol ImEventDelegate: AnyObject {
func imClientDidConnect()
func imClientDidDisconnect(reason: String?)
func imClientDidReceiveMessage(_ message: ImMessage)
func imClientDidReceiveGroupMessage(_ message: ImMessage)
func imClientDidError(_ error: String)
}

查看文件

@ -0,0 +1,29 @@
import Foundation
public enum PushVendor: String {
case apns = "APNS"
case fcm = "FCM"
}
@MainActor
public final class PushSDK {
public static let shared = PushSDK()
private init() {}
public func registerToken(_ token: String, userId: String, vendor: PushVendor = .apns) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/push/register",
method: "POST",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "userId", value: userId),
URLQueryItem(name: "vendor", value: vendor.rawValue),
URLQueryItem(name: "token", value: token),
]
)
}
}
private struct EmptyResponse: Decodable {}

查看文件

@ -0,0 +1,57 @@
import Foundation
import UIKit
public struct AppUpdateInfo: Decodable, Sendable {
public let needsUpdate: Bool
public let versionName: String?
public let versionCode: Int?
public let changeLog: String?
public let forceUpdate: Bool?
public let appStoreUrl: String?
}
public struct RnUpdateInfo: Decodable, Sendable {
public let needsUpdate: Bool
public let latestVersion: String
public let downloadUrl: String
public let md5: String
public let minCommonVersion: String
public let note: String
}
@MainActor
public final class UpdateSDK {
public static let shared = UpdateSDK()
private init() {}
public func checkAppUpdate(currentVersionCode: Int) async throws -> AppUpdateInfo {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/v1/updates/app/check",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "platform", value: "IOS"),
URLQueryItem(name: "currentVersionCode", value: "\(currentVersionCode)"),
]
)
}
public func openAppStore(url: String) {
guard let storeURL = URL(string: url) else { return }
UIApplication.shared.open(storeURL)
}
public func checkRnUpdate(moduleId: String, currentVersion: String) async throws -> RnUpdateInfo {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/v1/rn/update/check",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "moduleId", value: moduleId),
URLQueryItem(name: "platform", value: "IOS"),
URLQueryItem(name: "currentVersion", value: currentVersion),
]
)
}
}

12
XuqmSDK.podspec 普通文件
查看文件

@ -0,0 +1,12 @@
Pod::Spec.new do |s|
s.name = 'XuqmSDK'
s.version = '0.1.0'
s.summary = 'XuqmGroup iOS SDK — IM, Push Notifications, Version Management'
s.homepage = 'https://github.com/xuqinmin/XuqmGroup-iOSSDK'
s.license = { :type => 'MIT' }
s.author = { 'XuqmGroup' => 'dev@xuqm.com' }
s.source = { :git => 'https://github.com/xuqinmin/XuqmGroup-iOSSDK.git', :tag => s.version.to_s }
s.ios.deployment_target = '16.0'
s.swift_version = '5.9'
s.source_files = 'Sources/XuqmSDK/**/*.swift'
end