Files
e-party-iOS/yana/Features/FeedListFeature.swift
edwinQQQ 3d00e459e3 feat: 更新文档和视图以支持iOS 17及优化用户体验
- 更新Yana项目文档,调整适用版本至iOS 17,确保与最新开发环境兼容。
- 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。
- 添加默认初始化器以简化状态管理,确保各个Feature的状态一致性。
- 更新视图组件,移除不必要的硬编码,增强代码可读性和维护性。
- 修复多个视图中的逻辑错误,确保功能正常运行。
2025-07-29 17:57:42 +08:00

262 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 // EditFeedView
//
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?)
// 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 .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
}
}
}