
- 在Podfile中添加Alamofire依赖,并更新Podfile.lock以反映更改。 - 新增动态内容API文档,详细描述`dynamic/square/latestDynamics`接口的请求参数、响应数据结构及示例。 - 实现动态内容的模型和API请求结构,支持获取最新动态列表。 - 更新FeedView和HomeView以集成动态内容展示,增强用户体验。 - 添加动态卡片组件,展示用户动态信息及互动功能。
521 lines
16 KiB
Markdown
521 lines
16 KiB
Markdown
# **dynamic/square/latestDynamics API 文档**
|
||
|
||
## **概述**
|
||
|
||
`dynamic/square/latestDynamics` 是获取朋友圈动态最新列表的 API 接口,用于获取用户动态内容的最新更新。
|
||
|
||
## **接口信息**
|
||
|
||
| 属性 | 值 |
|
||
|------|-----|
|
||
| **接口路径** | `GET /dynamic/square/latestDynamics` |
|
||
| **请求方法** | `GET` |
|
||
| **认证要求** | 需要 `pub_uid` 和 `pub_ticket` |
|
||
| **内容类型** | `application/json` |
|
||
|
||
## **请求参数**
|
||
|
||
| 参数名 | 类型 | 必填 | 描述 | 示例值 |
|
||
|--------|------|------|------|--------|
|
||
| `dynamicId` | `String` | 否 | 最新动态的ID,用于分页加载。首次请求传空字符串 | `""` 或 `"123456"` |
|
||
| `pageSize` | `String` | 是 | 每页返回的数据数量 | `"20"` |
|
||
| `types` | `String` | 是 | 动态内容类型,多个类型用逗号分隔 | `"0,2"` |
|
||
|
||
### **types 参数说明**
|
||
- `0`: 纯文字动态
|
||
- `2`: 图片动态
|
||
|
||
## **响应数据结构**
|
||
|
||
### **成功响应 (200)**
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": {
|
||
"dynamicList": [
|
||
{
|
||
"dynamicId": "123456",
|
||
"uid": "789012",
|
||
"nick": "用户昵称",
|
||
"avatar": "https://example.com/avatar.jpg",
|
||
"gender": 1,
|
||
"age": 25,
|
||
"type": 0,
|
||
"content": "动态内容文字",
|
||
"likeCount": "15",
|
||
"isLike": false,
|
||
"commentCount": "3",
|
||
"publishTime": "2024-01-15 10:30:00",
|
||
"worldId": 456,
|
||
"worldName": "话题名称",
|
||
"squareTop": false,
|
||
"topicTop": false,
|
||
"newUser": false,
|
||
"defUser": 0,
|
||
"inRoomUid": "",
|
||
"dynamicResList": [
|
||
{
|
||
"resUrl": "https://example.com/image.jpg",
|
||
"format": "jpg",
|
||
"width": 720,
|
||
"height": 960
|
||
}
|
||
],
|
||
"userVipInfoVO": {
|
||
"vipLevel": 3,
|
||
"vipExpire": "2024-12-31"
|
||
},
|
||
"headwearPic": "https://example.com/headwear.png",
|
||
"headwearEffect": "https://example.com/effect.svga",
|
||
"headwearType": 1,
|
||
"expertLevelPic": "https://example.com/expert_lv3.png",
|
||
"charmLevelPic": "https://example.com/charm_lv2.png",
|
||
"nameplatePic": "https://example.com/nameplate.png",
|
||
"nameplateWord": "自定义铭牌",
|
||
"isCustomWord": true,
|
||
"labelList": ["新人", "活跃"]
|
||
}
|
||
],
|
||
"nextDynamicId": "123455"
|
||
}
|
||
}
|
||
```
|
||
|
||
### **错误响应**
|
||
|
||
```json
|
||
{
|
||
"code": 400,
|
||
"message": "参数错误",
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
## **Swift 实现示例**
|
||
|
||
### **1. 数据模型定义**
|
||
|
||
```swift
|
||
// MARK: - 响应数据模型
|
||
struct MomentsLatestResponse: Codable {
|
||
let code: Int
|
||
let message: String
|
||
let data: MomentsListData?
|
||
}
|
||
|
||
struct MomentsListData: Codable {
|
||
let dynamicList: [MomentsInfo]
|
||
let nextDynamicId: String
|
||
}
|
||
|
||
struct MomentsInfo: Codable {
|
||
let dynamicId: String
|
||
let uid: String
|
||
let nick: String
|
||
let avatar: String
|
||
let gender: Int
|
||
let age: Int
|
||
let type: Int
|
||
let content: String
|
||
let likeCount: String
|
||
let isLike: Bool
|
||
let commentCount: String
|
||
let publishTime: String
|
||
let worldId: Int
|
||
let worldName: String?
|
||
let squareTop: Bool
|
||
let topicTop: Bool
|
||
let newUser: Bool
|
||
let defUser: Int
|
||
let inRoomUid: String?
|
||
let dynamicResList: [MomentsPicture]?
|
||
let userVipInfoVO: UserVipInfo?
|
||
let headwearPic: String?
|
||
let headwearEffect: String?
|
||
let headwearType: Int?
|
||
let expertLevelPic: String?
|
||
let charmLevelPic: String?
|
||
let nameplatePic: String?
|
||
let nameplateWord: String?
|
||
let isCustomWord: Bool?
|
||
let labelList: [String]?
|
||
}
|
||
|
||
struct MomentsPicture: Codable {
|
||
let resUrl: String
|
||
let format: String
|
||
let width: CGFloat
|
||
let height: CGFloat
|
||
}
|
||
|
||
struct UserVipInfo: Codable {
|
||
let vipLevel: Int
|
||
let vipExpire: String?
|
||
}
|
||
|
||
// MARK: - 内容类型枚举
|
||
enum MomentsContentType: Int, CaseIterable {
|
||
case text = 0 // 纯文字
|
||
case picture = 2 // 图片
|
||
}
|
||
```
|
||
|
||
### **2. API 服务实现**
|
||
|
||
```swift
|
||
import Foundation
|
||
import Combine
|
||
|
||
class MomentsAPIService {
|
||
|
||
private let baseURL = "https://api.yourapp.com"
|
||
private let session = URLSession.shared
|
||
|
||
// MARK: - 获取最新动态列表
|
||
func fetchLatestMoments(
|
||
dynamicId: String = "",
|
||
pageSize: Int = 20,
|
||
types: [MomentsContentType] = [.text, .picture]
|
||
) -> AnyPublisher<MomentsListData, Error> {
|
||
|
||
// 构建请求参数
|
||
var components = URLComponents(string: "\(baseURL)/dynamic/square/latestDynamics")!
|
||
components.queryItems = [
|
||
URLQueryItem(name: "dynamicId", value: dynamicId),
|
||
URLQueryItem(name: "pageSize", value: String(pageSize)),
|
||
URLQueryItem(name: "types", value: types.map { String($0.rawValue) }.joined(separator: ","))
|
||
]
|
||
|
||
guard let url = components.url else {
|
||
return Fail(error: APIError.invalidURL)
|
||
.eraseToAnyPublisher()
|
||
}
|
||
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = "GET"
|
||
|
||
// 添加认证头
|
||
if let uid = AuthManager.shared.currentUID {
|
||
request.setValue(uid, forHTTPHeaderField: "pub_uid")
|
||
}
|
||
if let ticket = AuthManager.shared.currentTicket {
|
||
request.setValue(ticket, forHTTPHeaderField: "pub_ticket")
|
||
}
|
||
|
||
// 添加其他公共头
|
||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||
request.setValue(AppInfo.version, forHTTPHeaderField: "App-Version")
|
||
request.setValue(Locale.current.languageCode ?? "en", forHTTPHeaderField: "Accept-Language")
|
||
|
||
return session.dataTaskPublisher(for: request)
|
||
.map(\.data)
|
||
.decode(type: MomentsLatestResponse.self, decoder: JSONDecoder())
|
||
.compactMap { response in
|
||
guard response.code == 200 else {
|
||
throw APIError.serverError(response.code, response.message)
|
||
}
|
||
return response.data
|
||
}
|
||
.receive(on: DispatchQueue.main)
|
||
.eraseToAnyPublisher()
|
||
}
|
||
}
|
||
|
||
// MARK: - 错误类型定义
|
||
enum APIError: Error, LocalizedError {
|
||
case invalidURL
|
||
case noData
|
||
case serverError(Int, String)
|
||
case networkError(Error)
|
||
|
||
var errorDescription: String? {
|
||
switch self {
|
||
case .invalidURL:
|
||
return "无效的URL"
|
||
case .noData:
|
||
return "无数据返回"
|
||
case .serverError(let code, let message):
|
||
return "服务器错误 (\(code)): \(message)"
|
||
case .networkError(let error):
|
||
return "网络错误: \(error.localizedDescription)"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### **3. ViewModel 实现**
|
||
|
||
```swift
|
||
import Foundation
|
||
import Combine
|
||
|
||
@MainActor
|
||
class MomentsLatestViewModel: ObservableObject {
|
||
|
||
@Published var moments: [MomentsInfo] = []
|
||
@Published var isLoading = false
|
||
@Published var hasMoreData = true
|
||
@Published var errorMessage: String?
|
||
|
||
private var nextDynamicId = ""
|
||
private let apiService = MomentsAPIService()
|
||
private var cancellables = Set<AnyCancellable>()
|
||
|
||
// MARK: - 加载最新数据
|
||
func loadLatestMoments() {
|
||
loadMoments(isRefresh: true)
|
||
}
|
||
|
||
// MARK: - 加载更多数据
|
||
func loadMoreMoments() {
|
||
guard hasMoreData && !isLoading else { return }
|
||
loadMoments(isRefresh: false)
|
||
}
|
||
|
||
// MARK: - 私有方法:统一加载逻辑
|
||
private func loadMoments(isRefresh: Bool) {
|
||
isLoading = true
|
||
errorMessage = nil
|
||
|
||
let dynamicId = isRefresh ? "" : nextDynamicId
|
||
|
||
apiService.fetchLatestMoments(
|
||
dynamicId: dynamicId,
|
||
pageSize: 20,
|
||
types: [.text, .picture]
|
||
)
|
||
.sink(
|
||
receiveCompletion: { [weak self] completion in
|
||
self?.isLoading = false
|
||
if case .failure(let error) = completion {
|
||
self?.errorMessage = error.localizedDescription
|
||
}
|
||
},
|
||
receiveValue: { [weak self] data in
|
||
if isRefresh {
|
||
self?.moments = data.dynamicList
|
||
} else {
|
||
self?.moments.append(contentsOf: data.dynamicList)
|
||
}
|
||
|
||
self?.nextDynamicId = data.nextDynamicId
|
||
self?.hasMoreData = !data.dynamicList.isEmpty
|
||
}
|
||
)
|
||
.store(in: &cancellables)
|
||
}
|
||
|
||
// MARK: - 点赞操作
|
||
func toggleLike(for momentId: String) {
|
||
// 实现点赞逻辑
|
||
guard let index = moments.firstIndex(where: { $0.dynamicId == momentId }) else { return }
|
||
|
||
moments[index] = MomentsInfo(
|
||
dynamicId: moments[index].dynamicId,
|
||
uid: moments[index].uid,
|
||
nick: moments[index].nick,
|
||
avatar: moments[index].avatar,
|
||
gender: moments[index].gender,
|
||
age: moments[index].age,
|
||
type: moments[index].type,
|
||
content: moments[index].content,
|
||
likeCount: moments[index].isLike ?
|
||
String(max(0, Int(moments[index].likeCount) ?? 0 - 1)) :
|
||
String((Int(moments[index].likeCount) ?? 0) + 1),
|
||
isLike: !moments[index].isLike,
|
||
commentCount: moments[index].commentCount,
|
||
publishTime: moments[index].publishTime,
|
||
worldId: moments[index].worldId,
|
||
worldName: moments[index].worldName,
|
||
squareTop: moments[index].squareTop,
|
||
topicTop: moments[index].topicTop,
|
||
newUser: moments[index].newUser,
|
||
defUser: moments[index].defUser,
|
||
inRoomUid: moments[index].inRoomUid,
|
||
dynamicResList: moments[index].dynamicResList,
|
||
userVipInfoVO: moments[index].userVipInfoVO,
|
||
headwearPic: moments[index].headwearPic,
|
||
headwearEffect: moments[index].headwearEffect,
|
||
headwearType: moments[index].headwearType,
|
||
expertLevelPic: moments[index].expertLevelPic,
|
||
charmLevelPic: moments[index].charmLevelPic,
|
||
nameplatePic: moments[index].nameplatePic,
|
||
nameplateWord: moments[index].nameplateWord,
|
||
isCustomWord: moments[index].isCustomWord,
|
||
labelList: moments[index].labelList
|
||
)
|
||
}
|
||
}
|
||
```
|
||
|
||
### **4. SwiftUI 视图实现**
|
||
|
||
```swift
|
||
import SwiftUI
|
||
|
||
struct MomentsLatestView: View {
|
||
@StateObject private var viewModel = MomentsLatestViewModel()
|
||
|
||
var body: some View {
|
||
NavigationView {
|
||
List {
|
||
ForEach(viewModel.moments, id: \.dynamicId) { moment in
|
||
MomentCardView(moment: moment) {
|
||
viewModel.toggleLike(for: moment.dynamicId)
|
||
}
|
||
.onAppear {
|
||
// 当显示最后一个元素时加载更多
|
||
if moment.dynamicId == viewModel.moments.last?.dynamicId {
|
||
viewModel.loadMoreMoments()
|
||
}
|
||
}
|
||
}
|
||
|
||
if viewModel.isLoading {
|
||
HStack {
|
||
Spacer()
|
||
ProgressView()
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
.refreshable {
|
||
viewModel.loadLatestMoments()
|
||
}
|
||
.navigationTitle("最新动态")
|
||
.onAppear {
|
||
if viewModel.moments.isEmpty {
|
||
viewModel.loadLatestMoments()
|
||
}
|
||
}
|
||
.alert("错误", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||
Button("确定") {
|
||
viewModel.errorMessage = nil
|
||
}
|
||
} message: {
|
||
Text(viewModel.errorMessage ?? "")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
struct MomentCardView: View {
|
||
let moment: MomentsInfo
|
||
let onLike: () -> Void
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
// 用户信息
|
||
HStack {
|
||
AsyncImage(url: URL(string: moment.avatar)) { image in
|
||
image
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill)
|
||
} placeholder: {
|
||
Circle()
|
||
.fill(Color.gray.opacity(0.3))
|
||
}
|
||
.frame(width: 40, height: 40)
|
||
.clipShape(Circle())
|
||
|
||
VStack(alignment: .leading) {
|
||
Text(moment.nick)
|
||
.font(.headline)
|
||
Text(moment.publishTime)
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
|
||
// 动态内容
|
||
if !moment.content.isEmpty {
|
||
Text(moment.content)
|
||
.font(.body)
|
||
}
|
||
|
||
// 图片内容
|
||
if let pictures = moment.dynamicResList, !pictures.isEmpty {
|
||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3)) {
|
||
ForEach(pictures.indices, id: \.self) { index in
|
||
AsyncImage(url: URL(string: pictures[index].resUrl)) { image in
|
||
image
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill)
|
||
} placeholder: {
|
||
Rectangle()
|
||
.fill(Color.gray.opacity(0.3))
|
||
}
|
||
.frame(height: 100)
|
||
.clipped()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 操作栏
|
||
HStack {
|
||
Button(action: onLike) {
|
||
HStack {
|
||
Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||
.foregroundColor(moment.isLike ? .red : .gray)
|
||
Text(moment.likeCount)
|
||
.foregroundColor(.gray)
|
||
}
|
||
}
|
||
|
||
Spacer()
|
||
|
||
HStack {
|
||
Image(systemName: "message")
|
||
.foregroundColor(.gray)
|
||
Text(moment.commentCount)
|
||
.foregroundColor(.gray)
|
||
}
|
||
}
|
||
}
|
||
.padding()
|
||
.background(Color(.systemBackground))
|
||
.cornerRadius(8)
|
||
}
|
||
}
|
||
```
|
||
|
||
## **使用说明**
|
||
|
||
### **基本用法**
|
||
```swift
|
||
let viewModel = MomentsLatestViewModel()
|
||
|
||
// 加载最新数据
|
||
viewModel.loadLatestMoments()
|
||
|
||
// 加载更多数据
|
||
viewModel.loadMoreMoments()
|
||
```
|
||
|
||
### **分页逻辑**
|
||
- 首次请求:`dynamicId` 传空字符串
|
||
- 后续分页:使用上次响应中的 `nextDynamicId`
|
||
- 无更多数据:返回的 `dynamicList` 为空数组
|
||
|
||
### **错误处理**
|
||
- 网络错误:检查网络连接
|
||
- 401 认证失败:重新登录获取 ticket
|
||
- 其他服务器错误:显示具体错误信息
|
||
|
||
### **性能优化建议**
|
||
1. 使用图片缓存库(如 Kingfisher)
|
||
2. 实现虚拟列表避免内存过载
|
||
3. 预加载下一页数据提升用户体验
|
||
4. 实现本地缓存减少网络请求
|
||
|
||
## **注意事项**
|
||
|
||
1. **认证要求**:所有请求必须包含有效的 `pub_uid` 和 `pub_ticket`
|
||
2. **参数验证**:`pageSize` 建议范围为 10-50
|
||
3. **类型过滤**:`types` 参数支持多选,用逗号分隔
|
||
4. **数据更新**:推荐使用下拉刷新获取最新数据
|
||
5. **错误重试**:网络错误时实现自动重试机制 |