chore: sync local changes

这个提交包含在:
XuqmGroup 2026-05-07 19:39:48 +08:00
父节点 bc77850c3e
当前提交 5df6d70065
共有 20 个文件被更改,包括 548 次插入91 次删除

查看文件

@ -6,6 +6,7 @@ let package = Package(
platforms: [.iOS(.v16), .macOS(.v13)],
products: [
.library(name: "XuqmSDK", targets: ["XuqmSDK"]),
.library(name: "XuqmWebViewSDK", targets: ["XuqmWebViewSDK"]),
],
targets: [
.target(
@ -13,6 +14,12 @@ let package = Package(
path: "Sources/XuqmSDK",
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
),
.target(
name: "XuqmWebViewSDK",
dependencies: ["XuqmSDK"],
path: "Sources/XuqmWebViewSDK",
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
),
.testTarget(
name: "XuqmSDKTests",
dependencies: ["XuqmSDK"],

查看文件

@ -8,8 +8,9 @@
XuqmGroup-iOSSDK/
├── Package.swift # SPM 包定义
├── XuqmSDK.podspec # CocoaPods 发版描述
├── XuqmWebViewSDK.podspec # WebView 独立模块发版描述
├── PUBLISH.md # 发版操作手册
── Sources/XuqmSDK/
── Sources/XuqmSDK/
├── Core/
│ ├── XuqmSDK.swift # 入口init / setToken
│ ├── SDKConfig.swift # 配置结构体
@ -21,6 +22,11 @@ XuqmGroup-iOSSDK/
│ └── PushSDK.swift # APNs Token 注册
└── Update/
└── UpdateSDK.swift # 版本检查 / App 更新
└── Sources/XuqmWebViewSDK/
├── XWebViewBridge.swift # 页面配置 / 控制器桥接
├── XWebViewPage.swift # 独立页面
├── XWebViewTypes.swift # 配置 / 控制器协议
└── XWebViewView.swift # 嵌入式组件
```
## 集成
@ -39,7 +45,7 @@ dependencies: [
.package(url: "https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git", from: "0.1.0")
],
targets: [
.target(name: "YourApp", dependencies: ["XuqmSDK"])
.target(name: "YourApp", dependencies: ["XuqmSDK", "XuqmWebViewSDK"])
]
```
@ -51,6 +57,7 @@ source 'https://xuqinmin.com/xuqinmin12/xuqm-specs.git'
source 'https://cdn.cocoapods.org/'
pod 'XuqmSDK', '~> 0.1.0'
pod 'XuqmWebViewSDK', '~> 0.1.0'
```
---
@ -81,6 +88,32 @@ struct MyApp: App {
await XuqmSDK.setToken(token)
```
### 3. WebView 独立模块
`XuqmWebViewSDK` 不需要单独初始化,直接依赖即可使用。
```swift
import XuqmWebViewSDK
// 嵌入式组件:直接放到任意 SwiftUI 页面中
XWebViewView(
config: XWebViewConfig(
url: "https://example.com",
title: "嵌入式网页"
)
)
// 独立页面:在导航栈中直接 push
NavigationLink("打开页面模式") {
XWebViewPage(
config: XWebViewConfig(
url: "https://example.com",
title: "独立页面"
)
)
}
```
---
## Core
@ -227,6 +260,8 @@ if result.needsUpdate, let info = result.info {
`UpdateSDK` 这里只负责 iOS App 版本更新。RN 热更新如果需要,走独立的 RN SDK / RN 更新模块,不并入统一发版能力。
`XuqmWebViewSDK` 提供独立的 WebView 组件和页面,可与 `XuqmSDK` 同时使用,也可单独依赖。
---
## 发版

查看文件

@ -31,7 +31,6 @@ public extension SDKConfig {
}
extension SDKConfig {
var appId: String { appKey }
}
enum SDKEndpoints {

查看文件

@ -11,13 +11,18 @@ public final class XuqmSDK: NSObject {
private var userSig: String?
private var cachedDeviceToken: String?
private var lastInitializedAppKey: String?
private override init() {
super.init()
}
public func initialize(config: SDKConfig) {
if let lastInitializedAppKey, lastInitializedAppKey == config.appKey {
return
}
self.config = config
self.lastInitializedAppKey = config.appKey
self.tokenStore = TokenStore()
ApiClient.shared.configure(with: config)
if config.autoRegisterPush {
@ -35,6 +40,9 @@ public final class XuqmSDK: NSObject {
}
public func login(userId: String, userSig: String) async {
if currentUserId == userId && self.userSig == userSig {
return
}
self.currentUserId = userId
self.userSig = userSig

查看文件

@ -13,16 +13,16 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
private var groupSubscriptions = Set<String>()
private let tokenOverride: String?
private let appIdOverride: String?
private let appKeyOverride: String?
private var activeWsURL: URL?
private var activeToken: String?
private var activeAppId: String?
private var activeAppKey: String?
private var activeUserId: String?
public init(token: String? = nil, appId: String? = nil) {
public init(token: String? = nil, appKey: String? = nil) {
self.tokenOverride = token
self.appIdOverride = appId
self.appKeyOverride = appKey
super.init()
}
@ -37,7 +37,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
activeWsURL = SDKEndpoints.imWebSocketURL
activeToken = tokenOverride
activeAppId = appIdOverride
activeAppKey = appKeyOverride
guard let activeWsURL, let activeToken else {
delegate?.imClientDidError("IM config or token not found")
@ -73,7 +73,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
let now = Int64(Date().timeIntervalSince1970 * 1000)
let message = ImMessage(
id: messageId,
appId: activeAppId ?? "",
appKey: activeAppKey ?? "",
fromUserId: activeUserId ?? "",
fromId: activeUserId,
toId: toId,
@ -87,8 +87,8 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
createdAt: now,
editedAt: nil
)
guard let activeAppId else {
delegate?.imClientDidError("IM appId not configured")
guard let activeAppKey else {
delegate?.imClientDidError("IM appKey not configured")
return message.failedCopy()
}
let sent = sendFrame(
@ -98,7 +98,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
"content-type": "application/json",
],
body: encodeJSONString([
"appId": activeAppId,
"appKey": activeAppKey,
"messageId": messageId,
"toId": toId,
"chatType": chatType.rawValue,
@ -111,8 +111,8 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
}
public func revoke(messageId: String) {
guard let activeAppId else {
delegate?.imClientDidError("IM appId not configured")
guard let activeAppKey else {
delegate?.imClientDidError("IM appKey not configured")
return
}
sendFrame(
@ -122,15 +122,15 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
"content-type": "application/json",
],
body: encodeJSONString([
"appId": activeAppId,
"appKey": activeAppKey,
"messageId": messageId,
]),
)
}
public func sync() {
guard let activeAppId else {
delegate?.imClientDidError("IM appId not configured")
guard let activeAppKey else {
delegate?.imClientDidError("IM appKey not configured")
return
}
sendFrame(
@ -139,7 +139,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
"destination": "/app/chat.sync",
"content-type": "application/json",
],
body: encodeJSONString(["appId": activeAppId]),
body: encodeJSONString(["appKey": activeAppKey]),
)
}
@ -321,7 +321,7 @@ private extension ImMessage {
func failedCopy() -> ImMessage {
ImMessage(
id: id,
appId: appId,
appKey: appKey,
fromUserId: fromUserId,
fromId: fromId,
toId: toId,

查看文件

@ -12,6 +12,7 @@ public final class ImSDK {
private weak var delegate: ImEventDelegate?
private weak var conversationDelegate: ConversationDelegate?
private var currentUserId: String?
private var currentUserSig: String?
private var _conversations: [ConversationData] = []
public private(set) var connectionState: ImConnectionState = .disconnected
@ -31,11 +32,15 @@ public final class ImSDK {
}
public func login(_ userId: String, _ userSig: String) async throws {
if currentUserId == userId && currentUserSig == userSig {
return
}
let config = XuqmSDK.shared.requireConfig()
currentUserId = userId
currentUserSig = userSig
XuqmSDK.shared.tokenStore?.save(userSig)
client?.disconnect()
client = ImClient(token: userSig, appId: config.appId)
client = ImClient(token: userSig, appKey: config.appKey)
client?.setCurrentUserId(userId)
client?.delegate = self
updateConnectionState(.connecting)
@ -328,7 +333,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/messages/\(messageId)/revoke",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -337,7 +342,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/messages/\(messageId)",
method: "PUT",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: EditMessageRequest(content: content)
)
}
@ -359,7 +364,7 @@ public final class ImSDK {
) -> ImMessage {
ImMessage(
id: UUID().uuidString,
appId: XuqmSDK.shared.requireConfig().appId,
appKey: XuqmSDK.shared.requireConfig().appKey,
fromUserId: currentUserId ?? "",
fromId: currentUserId,
toId: toId,
@ -471,14 +476,14 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
let groups: [ImGroup] = try await ApiClient.shared.request(
path: "/api/im/groups",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
return groups
}
public func listPublicGroups(keyword: String? = nil) async throws -> [ImGroup] {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
var items = [URLQueryItem(name: "appKey", value: config.appKey)]
if let keyword { items.append(URLQueryItem(name: "keyword", value: keyword)) }
let groups: [ImGroup] = try await ApiClient.shared.request(
path: "/api/im/groups/public",
@ -492,7 +497,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/admin/users/search",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "keyword", value: keyword),
URLQueryItem(name: "size", value: String(size)),
]
@ -504,7 +509,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/admin/groups/search",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "keyword", value: keyword),
URLQueryItem(name: "size", value: String(size)),
]
@ -522,7 +527,7 @@ public final class ImSDK {
) async throws -> PageResult<ImMessage> {
let config = XuqmSDK.shared.requireConfig()
var items: [URLQueryItem] = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "size", value: String(size)),
]
@ -542,7 +547,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/groups",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: ImCreateGroupRequest(name: name, memberIds: memberIds, groupType: groupType)
)
}
@ -555,7 +560,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -564,7 +569,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members/search",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "keyword", value: keyword),
URLQueryItem(name: "size", value: String(size))
]
@ -650,7 +655,7 @@ public final class ImSDK {
public func sendGroupJoinRequest(groupId: String, remark: String? = nil) async throws -> GroupJoinRequest {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
var items = [URLQueryItem(name: "appKey", value: config.appKey)]
if let remark { items.append(URLQueryItem(name: "remark", value: remark)) }
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests",
@ -663,7 +668,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -672,7 +677,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests/\(requestId)/accept",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -681,7 +686,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests/\(requestId)/reject",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -689,7 +694,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friends",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -699,7 +704,7 @@ public final class ImSDK {
path: "/api/im/friends",
method: "POST",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "friendId", value: friendId),
]
)
@ -710,7 +715,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends",
method: "DELETE",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -719,13 +724,13 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends/\(friendId)",
method: "DELETE",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
public func setFriendGroup(friendId: String, groupName: String? = nil) async throws {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
var items = [URLQueryItem(name: "appKey", value: config.appKey)]
if let groupName {
items.append(URLQueryItem(name: "groupName", value: groupName))
}
@ -740,7 +745,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friends/groups",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -748,7 +753,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friends/groups/\(groupName)",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -757,7 +762,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/friend-requests",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "direction", value: direction),
]
)
@ -766,7 +771,7 @@ public final class ImSDK {
public func sendFriendRequest(toUserId: String, remark: String? = nil) async throws -> FriendRequest {
let config = XuqmSDK.shared.requireConfig()
var items = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "toUserId", value: toUserId),
]
if let remark { items.append(URLQueryItem(name: "remark", value: remark)) }
@ -782,7 +787,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/friend-requests/\(requestId)/accept",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -791,7 +796,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/friend-requests/\(requestId)/reject",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -799,7 +804,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/blacklist",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -809,7 +814,7 @@ public final class ImSDK {
path: "/api/im/blacklist",
method: "POST",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "blockedUserId", value: blockedUserId),
]
)
@ -821,7 +826,7 @@ public final class ImSDK {
path: "/api/im/blacklist",
method: "DELETE",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "blockedUserId", value: blockedUserId),
]
)
@ -832,7 +837,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/blacklist/check",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "targetUserId", value: targetUserId),
]
)
@ -842,7 +847,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/accounts/\(userId)",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -853,7 +858,7 @@ public final class ImSDK {
gender: String? = nil
) async throws -> UserProfile {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
var items = [URLQueryItem(name: "appKey", value: config.appKey)]
if let nickname { items.append(URLQueryItem(name: "nickname", value: nickname)) }
if let avatar { items.append(URLQueryItem(name: "avatar", value: avatar)) }
if let gender { items.append(URLQueryItem(name: "gender", value: gender)) }
@ -868,7 +873,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
let result: [ConversationData] = try await ApiClient.shared.request(
path: "/api/im/conversations",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
_conversations = result
conversationDelegate?.conversationsDidChange(_conversations)
@ -881,7 +886,7 @@ public final class ImSDK {
path: "/api/im/conversations/\(targetId)/read",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "chatType", value: chatType.rawValue),
]
)
@ -893,7 +898,7 @@ public final class ImSDK {
path: "/api/im/conversations/\(targetId)/pinned",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "pinned", value: String(pinned)),
]
@ -906,7 +911,7 @@ public final class ImSDK {
path: "/api/im/conversations/\(targetId)/muted",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "muted", value: String(muted)),
]
@ -919,7 +924,7 @@ public final class ImSDK {
path: "/api/im/conversations/\(targetId)/hidden",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "hidden", value: String(hidden)),
]
@ -929,7 +934,7 @@ public final class ImSDK {
public func setConversationGroup(targetId: String, chatType: ChatType, groupName: String? = nil) async throws {
let config = XuqmSDK.shared.requireConfig()
var items = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "chatType", value: chatType.rawValue),
]
if let groupName {
@ -946,7 +951,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/conversation-groups",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -954,7 +959,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/conversation-groups/\(groupName)",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -964,7 +969,7 @@ public final class ImSDK {
path: "/api/im/conversations/\(targetId)/draft",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "draft", value: draft),
]
@ -977,7 +982,7 @@ public final class ImSDK {
path: "/api/im/conversations/\(targetId)",
method: "DELETE",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "chatType", value: chatType.rawValue),
]
)
@ -992,7 +997,7 @@ public final class ImSDK {
let config = XuqmSDK.shared.requireConfig()
let result: [String: Int] = try await ApiClient.shared.request(
path: "/api/im/messages/offline/count",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
return result["count"] ?? 0
}
@ -1002,7 +1007,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/messages/offline",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)]
)
}
@ -1011,7 +1016,7 @@ public final class ImSDK {
return try await ApiClient.shared.request(
path: "/api/im/admin/groups/\(groupId)/read-receipts",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: ImGroupReadReceiptRequest(messageIds: messageIds)
)
}
@ -1021,7 +1026,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends/batch",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchFriendIdsRequest(friendIds: friendIds)
)
}
@ -1031,7 +1036,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends/batch/remove",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchFriendIdsRequest(friendIds: friendIds)
)
}
@ -1041,7 +1046,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friend-requests/batch/accept",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchRequestIdsRequest(requestIds: requestIds)
)
}
@ -1051,7 +1056,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friend-requests/batch/reject",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchRequestIdsRequest(requestIds: requestIds)
)
}
@ -1061,7 +1066,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members/batch",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchUserIdsRequest(userIds: userIds)
)
}
@ -1071,7 +1076,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members/batch/remove",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchUserIdsRequest(userIds: userIds)
)
}
@ -1081,7 +1086,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests/batch/accept",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchRequestIdsRequest(requestIds: requestIds)
)
}
@ -1091,7 +1096,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/join-requests/batch/reject",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: BatchRequestIdsRequest(requestIds: requestIds)
)
}
@ -1101,7 +1106,7 @@ public final class ImSDK {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members/\(userId)/info",
method: "PUT",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: ModifyMemberInfoRequest(nickname: nickname, role: role)
)
}
@ -1110,6 +1115,8 @@ public final class ImSDK {
client?.disconnect()
client = nil
updateConnectionState(.disconnected)
currentUserId = nil
currentUserSig = nil
}
private func fetchHistoryInternal(
@ -1125,7 +1132,7 @@ public final class ImSDK {
) async throws -> [ImMessage] {
let config = XuqmSDK.shared.requireConfig()
var items = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "size", value: String(size)),
]

查看文件

@ -40,7 +40,7 @@ public enum MsgStatus: String, Codable, Sendable {
public struct ImMessage: Codable, Sendable {
public let id: String
public let appId: String
public let appKey: String
public let fromUserId: String
public let fromId: String?
public let toId: String
@ -84,7 +84,7 @@ public extension ImEventDelegate {
public struct ImGroup: Codable, Sendable {
public let id: String
public let appId: String
public let appKey: String
public let name: String
public let groupType: String?
public let creatorId: String
@ -116,7 +116,7 @@ public struct ConversationGroupItem: Codable, Sendable {
public struct FriendRequest: Codable, Sendable {
public let id: String
public let appId: String
public let appKey: String
public let fromUserId: String
public let toUserId: String
public let remark: String?
@ -127,7 +127,7 @@ public struct FriendRequest: Codable, Sendable {
public struct GroupJoinRequest: Codable, Sendable {
public let id: String
public let appId: String
public let appKey: String
public let groupId: String
public let requesterId: String
public let remark: String?
@ -138,7 +138,7 @@ public struct GroupJoinRequest: Codable, Sendable {
public struct BlacklistEntry: Codable, Sendable {
public let id: String
public let appId: String
public let appKey: String
public let userId: String
public let blockedUserId: String
public let createdAt: Int64
@ -161,7 +161,7 @@ public struct GroupReadReceiptSummary: Codable, Sendable {
public struct UserProfile: Codable, Sendable {
public let id: String?
public let appId: String?
public let appKey: String?
public let userId: String
public let nickname: String?
public let avatar: String?

查看文件

@ -71,7 +71,7 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
path: "/api/push/register",
method: "POST",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "userId", value: userId),
URLQueryItem(name: "vendor", value: vendor.rawValue),
URLQueryItem(name: "token", value: token),
@ -110,7 +110,7 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
path: "/api/push/unregister",
method: "DELETE",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "userId", value: userId),
URLQueryItem(name: "vendor", value: vendor.rawValue),
URLQueryItem(name: "deviceId", value: deviceId),
@ -166,7 +166,7 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
let response: SdkRuntimeConfigResponse = try await ApiClient.shared.request(
path: "/api/sdk/config",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "platform", value: "IOS"),
]
)

查看文件

@ -23,7 +23,7 @@ public final class UpdateSDK {
return try await ApiClient.shared.request(
path: "/api/v1/updates/app/check",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "appKey", value: config.appKey),
URLQueryItem(name: "platform", value: "IOS"),
URLQueryItem(name: "currentVersionCode", value: "\(currentVersionCode)"),
]

查看文件

@ -0,0 +1,32 @@
import Foundation
@MainActor
public final class XWebViewBridge {
public static let shared = XWebViewBridge()
private var config = XWebViewConfig()
private weak var controller: AnyObject?
private init() {}
public func open(_ config: XWebViewConfig) {
self.config = config
}
public func currentConfig() -> XWebViewConfig {
config
}
public func setController(_ controller: (any XWebViewController)?) {
self.controller = controller
}
public func currentController() -> (any XWebViewController)? {
controller as? any XWebViewController
}
}
@MainActor
public func openXWebView(_ config: XWebViewConfig) {
XWebViewBridge.shared.open(config)
}

查看文件

@ -0,0 +1,37 @@
import SwiftUI
public struct XWebViewPage: View {
private let config: XWebViewConfig
public init(config: XWebViewConfig? = nil) {
self.config = config ?? XWebViewBridge.shared.currentConfig()
}
public var body: some View {
NavigationStack {
Group {
#if canImport(UIKit)
XWebViewView(config: config)
.navigationTitle(config.title.isEmpty ? "WebView" : config.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if !config.hideToolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
if let controller = XWebViewBridge.shared.currentController(), controller.canGoBack() {
controller.goBack()
}
} label: {
Image(systemName: "chevron.left")
}
}
}
}
#else
XWebViewView(config: config)
.navigationTitle(config.title.isEmpty ? "WebView" : config.title)
#endif
}
}
}
}

查看文件

@ -0,0 +1,33 @@
import Foundation
public struct XWebViewConfig: Sendable {
public var url: String
public var title: String
public var hideToolbar: Bool
public var hideStatusBar: Bool
public var userAgent: String?
public init(
url: String = "",
title: String = "",
hideToolbar: Bool = false,
hideStatusBar: Bool = false,
userAgent: String? = nil
) {
self.url = url
self.title = title
self.hideToolbar = hideToolbar
self.hideStatusBar = hideStatusBar
self.userAgent = userAgent
}
}
public protocol XWebViewController: AnyObject {
func canGoBack() -> Bool
func canGoForward() -> Bool
func currentUrl() -> String?
func goBack()
func goForward()
func reload()
func load(url: String)
}

查看文件

@ -0,0 +1,111 @@
import SwiftUI
#if canImport(UIKit)
import WebKit
@MainActor
public struct XWebViewView: UIViewRepresentable {
private let config: XWebViewConfig
public init(config: XWebViewConfig? = nil) {
self.config = config ?? XWebViewBridge.shared.currentConfig()
}
public func makeCoordinator() -> Coordinator {
Coordinator()
}
public func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero)
webView.navigationDelegate = context.coordinator
webView.uiDelegate = context.coordinator
if let userAgent = config.userAgent {
webView.customUserAgent = userAgent
}
context.coordinator.attach(webView)
XWebViewBridge.shared.setController(context.coordinator)
if !config.url.isEmpty, let url = URL(string: config.url) {
webView.load(URLRequest(url: url))
}
return webView
}
public func updateUIView(_ webView: WKWebView, context: Context) {
context.coordinator.attach(webView)
if webView.url == nil, !config.url.isEmpty, let url = URL(string: config.url) {
webView.load(URLRequest(url: url))
}
}
public static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
coordinator.detach()
if XWebViewBridge.shared.currentController() === coordinator {
XWebViewBridge.shared.setController(nil)
}
}
@MainActor
public final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate, XWebViewController {
private weak var webView: WKWebView?
func attach(_ webView: WKWebView) {
self.webView = webView
}
func detach() {
webView = nil
}
public func canGoBack() -> Bool {
webView?.canGoBack == true
}
public func canGoForward() -> Bool {
webView?.canGoForward == true
}
public func currentUrl() -> String? {
webView?.url?.absoluteString
}
public func goBack() {
webView?.goBack()
}
public func goForward() {
webView?.goForward()
}
public func reload() {
webView?.reload()
}
public func load(url: String) {
guard let url = URL(string: url) else { return }
webView?.load(URLRequest(url: url))
}
}
}
#else
public struct XWebViewView: View {
private let config: XWebViewConfig
public init(config: XWebViewConfig? = nil) {
self.config = config ?? XWebViewBridge.shared.currentConfig()
}
public var body: some View {
VStack(spacing: 12) {
Text(config.title.isEmpty ? "WebView" : config.title)
.font(.headline)
Text("XWebView is available on iOS with UIKit-backed WebView.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
#endif

查看文件

@ -8,7 +8,7 @@ final class SmokeTests: XCTestCase {
XCTAssertEqual(config.appKey, "ak_test")
XCTAssertTrue(config.debug)
XCTAssertFalse(config.autoRegisterPush)
XCTAssertEqual(config.appId, "ak_test")
XCTAssertEqual(config.appKey, "ak_test")
}
func testTokenStoreSaveGetClear() {
@ -22,7 +22,7 @@ final class SmokeTests: XCTestCase {
func testImMessageCoding() throws {
let msg = ImMessage(
id: "msg_1",
appId: "ak_test",
appKey: "ak_test",
fromUserId: "user_a",
fromId: "user_a",
toId: "user_b",

查看文件

@ -15,6 +15,7 @@ let package = Package(
name: "XuqmDemo",
dependencies: [
.product(name: "XuqmSDK", package: "XuqmGroup-iOSSDK"),
.product(name: "XuqmWebViewSDK", package: "XuqmGroup-iOSSDK"),
],
path: "Sources"
),

查看文件

@ -3,6 +3,7 @@ import XuqmSDK
enum AppRoute: Hashable {
case chat(targetId: String, targetName: String)
case webView
}
struct DemoUser: Identifiable {

查看文件

@ -27,8 +27,8 @@ final class AuthViewModel: ObservableObject {
let res: DemoLoginResponse = try await ApiClient.shared.request(
path: "/api/demo/auth/login",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
body: ["appId": config.appId, "userId": userId, "password": password]
queryItems: [URLQueryItem(name: "appKey", value: config.appKey)],
body: ["appKey": config.appKey, "userId": userId, "password": password]
)
await XuqmSDK.shared.login(userId: res.profile.userId, userSig: res.imToken)
currentUserId = res.profile.userId

查看文件

@ -0,0 +1,159 @@
import SwiftUI
import XuqmWebViewSDK
#if canImport(UIKit)
import UIKit
#endif
struct WebViewDemoView: View {
@Binding var path: NavigationPath
private enum WebViewMode: String, CaseIterable, Identifiable {
case embedded = "嵌入式"
case page = "页面模式"
var id: String { rawValue }
}
@State private var url = "https://example.com"
@State private var title = "示例网页"
@State private var mode: WebViewMode = .embedded
@State private var statusVersion = 0
private var config: XWebViewConfig {
XWebViewConfig(
url: url.trimmingCharacters(in: .whitespacesAndNewlines),
title: title.trimmingCharacters(in: .whitespacesAndNewlines)
)
}
private var currentUrl: String {
let controllerUrl = XWebViewBridge.shared.currentController()?.currentUrl()
return (controllerUrl?.isEmpty == false ? controllerUrl : nil) ?? config.url
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("XWebView")
.font(.title2)
.fontWeight(.bold)
Text("可输入 URL,并在嵌入式组件和独立页面之间切换。")
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 12) {
TextField("URL", text: $url)
TextField("标题", text: $title)
Picker("模式", selection: $mode) {
ForEach(WebViewMode.allCases) { item in
Text(item.rawValue).tag(item)
}
}
.pickerStyle(.segmented)
}
VStack(alignment: .leading, spacing: 12) {
Text("嵌入式组件")
.font(.headline)
if mode == .embedded {
XWebViewView(config: config)
.frame(height: 320)
.clipShape(RoundedRectangle(cornerRadius: 16))
} else {
Text("当前处于页面模式,点击下面按钮会 push 到独立页面。")
.foregroundStyle(.secondary)
}
}
VStack(alignment: .leading, spacing: 12) {
Text("页面控制")
.font(.headline)
Text("当前 URL: \(currentUrl.isEmpty ? "未加载" : currentUrl)")
.font(.caption)
.foregroundStyle(.secondary)
HStack(spacing: 12) {
Button {
XWebViewBridge.shared.currentController()?.goBack()
statusVersion += 1
} label: {
Text("后退")
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(Color.gray.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.disabled(!(XWebViewBridge.shared.currentController()?.canGoBack() ?? false))
Button {
XWebViewBridge.shared.currentController()?.goForward()
statusVersion += 1
} label: {
Text("前进")
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(Color.gray.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.disabled(!(XWebViewBridge.shared.currentController()?.canGoForward() ?? false))
}
HStack(spacing: 12) {
Button {
XWebViewBridge.shared.currentController()?.reload()
statusVersion += 1
} label: {
Text("刷新")
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(Color.gray.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
Button {
#if canImport(UIKit)
UIPasteboard.general.string = currentUrl
#endif
statusVersion += 1
} label: {
Text("复制 URL")
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(Color.gray.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
Button {
XWebViewBridge.shared.currentController()?.load(url: url.trimmingCharacters(in: .whitespacesAndNewlines))
statusVersion += 1
} label: {
Text("重新加载输入 URL")
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Button {
openXWebView(config)
path.append(AppRoute.webView)
} label: {
Text("打开页面模式")
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
.padding()
}
.navigationTitle("网页")
}
}

查看文件

@ -1,5 +1,6 @@
import SwiftUI
import XuqmSDK
import XuqmWebViewSDK
@main
struct XuqmDemoApp: App {
@ -30,7 +31,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
private func initializeSDK() {
let config = SDKConfig(
appKey: "ak_demo_chat",
appSecret: "demo_secret",
debug: true
)
XuqmSDK.shared.initialize(config: config)
@ -82,17 +82,31 @@ struct MainTabView: View {
}
.tag(1)
WebViewDemoView(path: $path)
.tabItem {
Image(systemName: "globe")
Text("网页")
}
.tag(2)
ProfileView(authViewModel: authViewModel)
.tabItem {
Image(systemName: "person.fill")
Text("我的")
}
.tag(2)
.tag(3)
}
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .chat(let targetId, let targetName):
ChatView(targetId: targetId, targetName: targetName, currentUserId: authViewModel.currentUserId)
case .webView:
XWebViewPage(
config: XWebViewConfig(
url: "https://example.com",
title: "独立页面"
)
)
}
}
}

13
XuqmWebViewSDK.podspec 普通文件
查看文件

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