Files
e-party-iOS/yana/Features/FeedListFeature.swift
edwinQQQ 1f17960b8d feat: 新增CreateFeedView优化任务总结文档及相关功能实现
- 在CreateFeedView中优化发布按钮样式,增加圆角背景和渐变色。
- 移除内容输入区域的深灰色背景,提升UI体验。
- 实现点击发布按钮时自动收起键盘功能。
- 添加发布成功通知机制,确保外层刷新列表数据。
- 更新相关Feature以支持跨Feature通信和状态管理。
2025-07-31 14:23:15 +08:00

267 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import ComposableArchitecture
@Reducer
struct FeedListFeature {
@Dependency(\.apiService) var apiService
@ObservableState
struct State: Equatable {
var isFirstLoad: Bool = true
var feeds: [Feed] = [] // feed
var isLoading: Bool = false
var error: String? = nil
var isEditFeedPresented: Bool = false // CreateFeedView
//
var moments: [MomentsInfo] = []
//
var isLoaded: Bool = false
//
var currentPage: Int = 1
var hasMore: Bool = true
var isLoadingMore: Bool = false
// DetailView
var showDetail: Bool = false
var selectedMoment: MomentsInfo?
//
var likeLoadingDynamicIds: Set<Int> = []
init() {
//
}
}
enum Action: Equatable {
case onAppear
case reload
case loadMore
case loadMoreResponse(TaskResult<MomentsLatestResponse>)
case editFeedButtonTapped // add
case editFeedDismissed //
case testButtonTapped //
//
case fetchFeeds
case fetchFeedsResponse(TaskResult<MomentsLatestResponse>)
// DetailViewAction
case showDetail(MomentsInfo)
case detailDismissed
// Action
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
case likeResponse(TaskResult<LikeDynamicResponse>, dynamicId: Int, loadingId: UUID?)
// CreateFeed
case createFeedPublishSuccess
// Action
}
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .onAppear:
guard state.isFirstLoad else { return .none }
state.isFirstLoad = false
return .send(.fetchFeeds)
case .reload:
//
state.isLoading = true
state.error = nil
state.currentPage = 1
state.hasMore = true
state.isLoaded = true
return .run { [apiService] send in
await send(.fetchFeedsResponse(TaskResult {
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
return try await apiService.request(request)
}))
}
case .loadMore:
//
guard state.hasMore, !state.isLoadingMore, !state.isLoading else { return .none }
state.isLoadingMore = true
let lastDynamicId: String = {
if let last = state.moments.last {
return String(last.dynamicId)
} else {
return ""
}
}()
return .run { [apiService] send in
await send(.loadMoreResponse(TaskResult {
let request = LatestDynamicsRequest(dynamicId: lastDynamicId, pageSize: 20, types: [.text, .picture])
return try await apiService.request(request)
}))
}
case let .loadMoreResponse(.success(response)):
state.isLoadingMore = false
if let list = response.data?.dynamicList {
if list.isEmpty {
state.hasMore = false
} else {
state.moments.append(contentsOf: list)
state.currentPage += 1
state.hasMore = (list.count >= 20)
}
state.error = nil
} else {
state.hasMore = false
state.error = response.message
}
return .none
case let .loadMoreResponse(.failure(error)):
state.isLoadingMore = false
state.hasMore = false
state.error = error.localizedDescription
return .none
case .fetchFeeds:
state.isLoading = true
state.error = nil
// API
return .run { [apiService] send in
await send(.fetchFeedsResponse(TaskResult {
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
return try await apiService.request(request)
}))
}
case let .fetchFeedsResponse(.success(response)):
state.isLoading = false
if let list = response.data?.dynamicList {
state.moments = list
state.error = nil
state.currentPage = 1
state.hasMore = (list.count >= 20)
} else {
state.moments = []
state.error = response.message
state.hasMore = false
}
return .none
case let .fetchFeedsResponse(.failure(error)):
state.isLoading = false
state.moments = []
state.error = error.localizedDescription
state.hasMore = false
return .none
case .editFeedButtonTapped:
state.isEditFeedPresented = true
return .none
case .editFeedDismissed:
state.isEditFeedPresented = false
return .none
case .createFeedPublishSuccess:
// CreateFeed
return .send(.reload)
case .testButtonTapped:
debugInfoSync("[LOG] FeedListFeature testButtonTapped")
return .none
case let .showDetail(moment):
state.selectedMoment = moment
state.showDetail = true
return .none
case .detailDismissed:
state.showDetail = false
state.selectedMoment = nil
return .none
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
// loading
state.likeLoadingDynamicIds.insert(dynamicId)
//
guard let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) else {
//
setAPILoadingErrorSync(UUID(), errorMessage: "找不到对应的动态")
state.likeLoadingDynamicIds.remove(dynamicId)
return .none
}
let currentMoment = state.moments[index]
let status = currentMoment.isLike ? 0 : 1 // 0: , 1:
let request = LikeDynamicRequest(
dynamicId: dynamicId,
uid: uid,
status: status,
likedUid: likedUid,
worldId: worldId
)
return .run { [apiService] send in
let loadingId = await APILoadingManager.shared.startLoading(
shouldShowLoading: request.shouldShowLoading,
shouldShowError: request.shouldShowError
)
do {
let response: LikeDynamicResponse = try await apiService.request(request)
await send(.likeResponse(.success(response), dynamicId: dynamicId, loadingId: loadingId))
} catch {
await send(.likeResponse(.failure(error), dynamicId: dynamicId, loadingId: loadingId))
}
}
case let .likeResponse(.success(response), dynamicId, loadingId):
state.likeLoadingDynamicIds.remove(dynamicId)
if let loadingId = loadingId {
if let data = response.data, let success = data.success, success {
Task { @MainActor in
APILoadingManager.shared.finishLoading(loadingId)
}
if let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) {
let currentMoment = state.moments[index]
let newLikeState = !currentMoment.isLike
let updatedMoment = MomentsInfo(
dynamicId: currentMoment.dynamicId,
uid: currentMoment.uid,
nick: currentMoment.nick,
avatar: currentMoment.avatar,
type: currentMoment.type,
content: currentMoment.content,
likeCount: data.likeCount ?? currentMoment.likeCount,
isLike: newLikeState,
commentCount: currentMoment.commentCount,
publishTime: currentMoment.publishTime,
worldId: currentMoment.worldId,
status: currentMoment.status,
playCount: currentMoment.playCount,
dynamicResList: currentMoment.dynamicResList,
gender: currentMoment.gender,
squareTop: currentMoment.squareTop,
topicTop: currentMoment.topicTop,
newUser: currentMoment.newUser,
defUser: currentMoment.defUser,
scene: currentMoment.scene,
userVipInfoVO: currentMoment.userVipInfoVO,
headwearPic: currentMoment.headwearPic,
headwearEffect: currentMoment.headwearEffect,
headwearType: currentMoment.headwearType,
headwearName: currentMoment.headwearName,
headwearId: currentMoment.headwearId,
experLevelPic: currentMoment.experLevelPic,
charmLevelPic: currentMoment.charmLevelPic,
isCustomWord: currentMoment.isCustomWord,
labelList: currentMoment.labelList
)
state.moments[index] = updatedMoment
}
} else {
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
setAPILoadingErrorSync(loadingId, errorMessage: errorMessage)
}
}
return .none
case let .likeResponse(.failure(error), dynamicId, loadingId):
state.likeLoadingDynamicIds.remove(dynamicId)
if let loadingId = loadingId {
setAPILoadingErrorSync(loadingId, errorMessage: error.localizedDescription)
}
return .none
}
}
}
// Feed
enum Feed: Equatable, Identifiable {
case placeholder(id: UUID = UUID())
var id: UUID {
switch self {
case .placeholder(let id): return id
}
}
}