feat: 更新动态点赞与加载状态管理以提升用户体验

- 在DetailFeature和FeedListFeature中增强点赞功能的状态管理,确保用户交互流畅。
- 新增API加载效果视图,提升用户在操作过程中的反馈体验。
- 更新视图组件以支持点赞加载状态,优化用户界面交互。
- 改进错误处理逻辑,确保在API请求失败时提供友好的错误提示。
This commit is contained in:
edwinQQQ
2025-07-28 16:05:22 +08:00
parent e286229f6f
commit d35071d3de
13 changed files with 389 additions and 332 deletions

View File

@@ -1,39 +1,48 @@
--- ---
description: Description:
globs: globs:
alwaysApply: true alwaysApply: true
--- ---
# CONTEXT # Background
This project based on iOS 16.0+ & SwiftUI & TCA 1.20.2 This project is based on iOS 16.0+, SwiftUI, and TCA 1.20.2
I wish to receive advice using the latest tools and seek step-by-step guidance to fully understand the implementation process. I would like advice on using the latest tools and seek step-by-step guidance to fully understand the implementation process.
## OBJECTIVE ## Objective
As an expert AI programming assistant, your task is to provide me with clear, readable, and effective code. You should: As a professional AI programming assistant, your task is to provide me with clear, readable, and efficient code. You should:
- Utilize the latest versions of SwiftUI, Swift(6) and TCA(1.20.2), being familiar with the newest features and best practices. - Use the latest versions of SwiftUI, Swift(6), and TCA(1.20.2), and be familiar with the latest features and best practices.
- Provide careful and accurate answers that are well-founded and thoughtfully considered.
- **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.**
- Strictly adhere to my requirements and meticulously complete the tasks.
- Begin by outlining your proposed approach with detailed steps or pseudocode.
- Upon confirming the plan, proceed to write the code.
## STYLE - Provide careful, accurate answers that are well-reasoned and well-thought-out.
- Keep answers concise and direct, minimizing unnecessary wording. - **Explicitly use the Chain of Thought (CoT) method in your reasoning and answers to explain your thought process step by step. **
- Emphasize code readability over performance optimization. - Follow my instructions and complete the task meticulously.
- Maintain a professional and supportive tone, ensuring clarity of content.
## RESPONSE FORMAT - Start by outlining your proposed approach with detailed steps or pseudocode.
- **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.** - Once you have confirmed your plan, start writing code.
- The reply should include: - After coding is done, no compilation check is required, remind me to check
1. **Step-by-Step Plan**: Describe the implementation process with detailed pseudocode or step-by-step explanations, showcasing your thought process.
2. **Code Implementation**: Provide correct, up-to-date, error-free, fully functional, runnable, secure, and efficient code. The code should: ## Style
- Include all necessary imports and properly name key components.
- Fully implement all requested features, leaving no to-dos, placeholders, or omissions. - Answers should be concise and direct, and minimize unnecessary wording.
3. **Concise Response**: Minimize unnecessary verbosity, focusing only on essential information. - Emphasize code readability rather than performance optimization.
- Maintain a professional and supportive tone to ensure clarity.
- If a correct answer may not exist, please point it out. If you do not know the answer, please honestly inform me rather than guessing.
## Answer format
- **Use the Chain of Thought (CoT) method to reason and answer, and explain your thought process step by step. **
- The answer should include the following:
1. **Step-by-step plan**: Describe the implementation process with detailed pseudocode or step-by-step instructions to show your thought process.
2. **Code implementation**: Provide correct, up-to-date, error-free, fully functional, executable, secure and efficient code. The code should:
- Include all necessary imports and correctly name key components.
- Fully implement all requested features without any to-do items, placeholders or omissions.
3. **Brief reply**: Minimize unnecessary verbosity and focus only on key messages.
- If there is no correct answer, please point it out. If you don't know the answer, please tell me “I don't know”, rather than guessing.

View File

@@ -36,7 +36,6 @@ struct DetailFeature {
case showImagePreview([String], Int) case showImagePreview([String], Int)
case hideImagePreview case hideImagePreview
case imagePreviewDismissed case imagePreviewDismissed
case onLikeSuccess(Int, Bool) // dynamicId, newLikeState
case dismissView case dismissView
// IDactions // IDactions
@@ -45,7 +44,9 @@ struct DetailFeature {
} }
var body: some ReducerOf<Self> { var body: some ReducerOf<Self> {
Reduce { state, action in Reduce {
state,
action in
switch action { switch action {
case .onAppear: case .onAppear:
// ID // ID
@@ -69,9 +70,10 @@ struct DetailFeature {
return .none return .none
case let .likeDynamic(dynamicId, uid, likedUid, worldId): case let .likeDynamic(dynamicId, uid, likedUid, worldId):
// loading
state.isLikeLoading = true state.isLikeLoading = true
let status = state.moment.isLike ? 0 : 1 let status = state.moment.isLike ? 0 : 1 // 0: , 1:
let request = LikeDynamicRequest( let request = LikeDynamicRequest(
dynamicId: dynamicId, dynamicId: dynamicId,
uid: uid, uid: uid,
@@ -80,59 +82,71 @@ struct DetailFeature {
worldId: worldId worldId: worldId
) )
return .run { send in return .run { [apiService] send in
let result = await TaskResult { do {
try await apiService.request(request) let response: LikeDynamicResponse = try await apiService.request(request)
await send(.likeResponse(.success(response)))
} catch {
await send(.likeResponse(.failure(error)))
} }
await send(.likeResponse(result))
} }
case let .likeResponse(.success(response)): case let .likeResponse(.success(response)):
state.isLikeLoading = false if let data = response.data, let success = data.success, success {
// // API
return .send(.onLikeSuccess(state.moment.dynamicId, !state.moment.isLike)) let newLikeState = !state.moment.isLike //
//
let updatedMoment = MomentsInfo(
dynamicId: state.moment.dynamicId,
uid: state.moment.uid,
nick: state.moment.nick,
avatar: state.moment.avatar,
type: state.moment.type,
content: state.moment.content,
likeCount: data.likeCount ?? state.moment.likeCount,
isLike: newLikeState,
commentCount: state.moment.commentCount,
publishTime: state.moment.publishTime,
worldId: state.moment.worldId,
status: state.moment.status,
playCount: state.moment.playCount,
dynamicResList: state.moment.dynamicResList,
gender: state.moment.gender,
squareTop: state.moment.squareTop,
topicTop: state.moment.topicTop,
newUser: state.moment.newUser,
defUser: state.moment.defUser,
scene: state.moment.scene,
userVipInfoVO: state.moment.userVipInfoVO,
headwearPic: state.moment.headwearPic,
headwearEffect: state.moment.headwearEffect,
headwearType: state.moment.headwearType,
headwearName: state.moment.headwearName,
headwearId: state.moment.headwearId,
experLevelPic: state.moment.experLevelPic,
charmLevelPic: state.moment.charmLevelPic,
isCustomWord: state.moment.isCustomWord,
labelList: state.moment.labelList
)
state.moment = updatedMoment
// loading
state.isLikeLoading = false
} else {
// APIAPILoadingManager
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
}
case let .onLikeSuccess(dynamicId, newLikeState): // loading
// state.isLikeLoading = false
// MomentsInfoisLikeletmoment
let updatedMoment = MomentsInfo(
dynamicId: state.moment.dynamicId,
uid: state.moment.uid,
nick: state.moment.nick,
avatar: state.moment.avatar,
type: state.moment.type,
content: state.moment.content,
likeCount: state.moment.likeCount,
isLike: newLikeState,
commentCount: state.moment.commentCount,
publishTime: state.moment.publishTime,
worldId: state.moment.worldId,
status: state.moment.status,
playCount: state.moment.playCount,
dynamicResList: state.moment.dynamicResList,
gender: state.moment.gender,
squareTop: state.moment.squareTop,
topicTop: state.moment.topicTop,
newUser: state.moment.newUser,
defUser: state.moment.defUser,
scene: state.moment.scene,
userVipInfoVO: state.moment.userVipInfoVO,
headwearPic: state.moment.headwearPic,
headwearEffect: state.moment.headwearEffect,
headwearType: state.moment.headwearType,
headwearName: state.moment.headwearName,
headwearId: state.moment.headwearId,
experLevelPic: state.moment.experLevelPic,
charmLevelPic: state.moment.charmLevelPic,
isCustomWord: state.moment.isCustomWord,
labelList: state.moment.labelList
)
state.moment = updatedMoment
return .none return .none
case let .likeResponse(.failure(error)): case let .likeResponse(.failure(error)):
// loading
state.isLikeLoading = false state.isLikeLoading = false
// // APILoadingManager
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
return .none return .none
case .deleteDynamic: case .deleteDynamic:
@@ -179,4 +193,4 @@ struct DetailFeature {
} }
} }
} }
} }

View File

@@ -41,7 +41,7 @@ struct FeedListFeature {
case detailDismissed case detailDismissed
// Action // Action
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
case likeResponse(TaskResult<LikeDynamicResponse>) case likeResponse(TaskResult<LikeDynamicResponse>, dynamicId: Int)
// Action // Action
} }
@@ -149,15 +149,17 @@ struct FeedListFeature {
state.selectedMoment = nil state.selectedMoment = nil
return .none return .none
case let .likeDynamic(dynamicId, uid, likedUid, worldId): case let .likeDynamic(dynamicId, uid, likedUid, worldId):
//
guard let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) else {
return .none
}
// loading // loading
state.likeLoadingDynamicIds.insert(dynamicId) 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 currentMoment = state.moments[index]
let status = currentMoment.isLike ? 0 : 1 // 0: , 1: let status = currentMoment.isLike ? 0 : 1 // 0: , 1:
@@ -170,71 +172,74 @@ struct FeedListFeature {
) )
return .run { [apiService] send in return .run { [apiService] send in
let result = await TaskResult { do {
try await apiService.request(request) let response: LikeDynamicResponse = try await apiService.request(request)
await send(.likeResponse(.success(response), dynamicId: dynamicId))
} catch {
await send(.likeResponse(.failure(error), dynamicId: dynamicId))
} }
await send(.likeResponse(result))
} }
case let .likeResponse(.success(response)): case let .likeResponse(.success(response), dynamicId):
// loading
if let data = response.data, let success = data.success, success { if let data = response.data, let success = data.success, success {
// // API
// loadingdynamicId if let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) {
let loadingDynamicIds = state.likeLoadingDynamicIds //
for dynamicId in loadingDynamicIds { let currentMoment = state.moments[index]
if let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) { let newLikeState = !currentMoment.isLike //
let currentMoment = state.moments[index]
let newLikeState = !currentMoment.isLike //
let newLikeCount = data.likeCount ?? currentMoment.likeCount let updatedMoment = MomentsInfo(
dynamicId: currentMoment.dynamicId,
// uid: currentMoment.uid,
let updatedMoment = MomentsInfo( nick: currentMoment.nick,
dynamicId: currentMoment.dynamicId, avatar: currentMoment.avatar,
uid: currentMoment.uid, type: currentMoment.type,
nick: currentMoment.nick, content: currentMoment.content,
avatar: currentMoment.avatar, likeCount: data.likeCount ?? currentMoment.likeCount,
type: currentMoment.type, isLike: newLikeState,
content: currentMoment.content, commentCount: currentMoment.commentCount,
likeCount: newLikeCount, publishTime: currentMoment.publishTime,
isLike: newLikeState, worldId: currentMoment.worldId,
commentCount: currentMoment.commentCount, status: currentMoment.status,
publishTime: currentMoment.publishTime, playCount: currentMoment.playCount,
worldId: currentMoment.worldId, dynamicResList: currentMoment.dynamicResList,
status: currentMoment.status, gender: currentMoment.gender,
playCount: currentMoment.playCount, squareTop: currentMoment.squareTop,
dynamicResList: currentMoment.dynamicResList, topicTop: currentMoment.topicTop,
gender: currentMoment.gender, newUser: currentMoment.newUser,
squareTop: currentMoment.squareTop, defUser: currentMoment.defUser,
topicTop: currentMoment.topicTop, scene: currentMoment.scene,
newUser: currentMoment.newUser, userVipInfoVO: currentMoment.userVipInfoVO,
defUser: currentMoment.defUser, headwearPic: currentMoment.headwearPic,
scene: currentMoment.scene, headwearEffect: currentMoment.headwearEffect,
userVipInfoVO: currentMoment.userVipInfoVO, headwearType: currentMoment.headwearType,
headwearPic: currentMoment.headwearPic, headwearName: currentMoment.headwearName,
headwearEffect: currentMoment.headwearEffect, headwearId: currentMoment.headwearId,
headwearType: currentMoment.headwearType, experLevelPic: currentMoment.experLevelPic,
headwearName: currentMoment.headwearName, charmLevelPic: currentMoment.charmLevelPic,
headwearId: currentMoment.headwearId, isCustomWord: currentMoment.isCustomWord,
experLevelPic: currentMoment.experLevelPic, labelList: currentMoment.labelList
charmLevelPic: currentMoment.charmLevelPic, )
isCustomWord: currentMoment.isCustomWord, state.moments[index] = updatedMoment
labelList: currentMoment.labelList
)
state.moments[index] = updatedMoment
break // 退
}
} }
// loading
state.likeLoadingDynamicIds.removeAll()
} else {
// APIAPILoadingManager
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
} }
// loading // loading
state.likeLoadingDynamicIds.removeAll() state.likeLoadingDynamicIds.removeAll()
return .none return .none
case let .likeResponse(.failure(error)): case let .likeResponse(.failure(error), dynamicId):
// loading // loading
state.likeLoadingDynamicIds.removeAll() state.likeLoadingDynamicIds.removeAll()
// APILoadingManager
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
return .none return .none
} }
} }

View File

@@ -137,91 +137,91 @@ private struct SimpleErrorView: View {
// MARK: - Preview // MARK: - Preview
#if DEBUG //#if DEBUG
struct APILoadingEffectView_Previews: PreviewProvider { //struct APILoadingEffectView_Previews: PreviewProvider {
static var previews: some View { // static var previews: some View {
ZStack { // ZStack {
// // //
Rectangle() // Rectangle()
.fill(Color.blue.opacity(0.3)) // .fill(Color.blue.opacity(0.3))
.ignoresSafeArea() // .ignoresSafeArea()
//
VStack(spacing: 20) { // VStack(spacing: 20) {
Text("背景内") // Text("")
.font(.title) // .font(.title)
//
Button("测试按") { // Button("") {
debugInfoSync("按钮被点击了") // debugInfoSync("")
} // }
.padding() // .padding()
.background(Color.blue) // .background(Color.blue)
.foregroundColor(.white) // .foregroundColor(.white)
.cornerRadius(8) // .cornerRadius(8)
} // }
//
// Loading Effect View // // Loading Effect View
APILoadingEffectView() // APILoadingEffectView()
} // }
.previewDisplayName("API Loading Effect") // .previewDisplayName("API Loading Effect")
.onAppear { // .onAppear {
// // //
Task { // Task {
let manager = APILoadingManager.shared // let manager = APILoadingManager.shared
//
// loading // // loading
let id1 = manager.startLoading() // let id1 = manager.startLoading()
//
// 2 // // 2
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { // DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
Task { // Task {
manager.setError(id1, errorMessage: "网络连接失败,请检查网络设") // manager.setError(id1, errorMessage: "")
} // }
} // }
} // }
} // }
} // }
} //}
//
// MARK: - Preview Helpers //// MARK: - Preview Helpers
//
/// /////
private struct PreviewStateModifier: ViewModifier { //private struct PreviewStateModifier: ViewModifier {
let showLoading: Bool // let showLoading: Bool
let showError: Bool // let showError: Bool
let errorMessage: String // let errorMessage: String
//
func body(content: Content) -> some View { // func body(content: Content) -> some View {
content // content
.onAppear { // .onAppear {
Task { // Task {
let manager = APILoadingManager.shared // let manager = APILoadingManager.shared
//
if showLoading { // if showLoading {
let _ = manager.startLoading() // let _ = manager.startLoading()
} // }
//
if showError { // if showError {
let id = manager.startLoading() // let id = manager.startLoading()
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 // try? await Task.sleep(nanoseconds: 1_000_000_000) // 1
manager.setError(id, errorMessage: errorMessage) // manager.setError(id, errorMessage: errorMessage)
} // }
} // }
} // }
} // }
} //}
//
extension View { //extension View {
/// // ///
func previewLoadingState( // func previewLoadingState(
showLoading: Bool = false, // showLoading: Bool = false,
showError: Bool = false, // showError: Bool = false,
errorMessage: String = "示例错误信" // errorMessage: String = ""
) -> some View { // ) -> some View {
self.modifier(PreviewStateModifier( // self.modifier(PreviewStateModifier(
showLoading: showLoading, // showLoading: showLoading,
showError: showError, // showError: showError,
errorMessage: errorMessage // errorMessage: errorMessage
)) // ))
} // }
} //}
#endif //#endif

View File

@@ -134,4 +134,18 @@ extension APILoadingManager {
return .failure(error) return .failure(error)
} }
} }
}
// MARK: - Global Convenience Methods
/// 便fire-and-forget
/// - Parameters:
/// - id: ID
/// - errorMessage:
func setAPILoadingErrorSync(_ id: UUID, errorMessage: String) {
Task {
await MainActor.run {
APILoadingManager.shared.setError(id, errorMessage: errorMessage)
}
}
} }

View File

@@ -11,19 +11,19 @@ struct OptimizedDynamicCardView: View {
let onImageTap: (_ images: [String], _ index: Int) -> Void let onImageTap: (_ images: [String], _ index: Int) -> Void
// //
let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void
// loading
let isLikeLoading: Bool
// //
let isDetailMode: Bool let isDetailMode: Bool
// loading
let isLikeLoading: Bool
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void, onLikeTap: @escaping (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void, isLikeLoading: Bool = false, isDetailMode: Bool = false) { init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void, onLikeTap: @escaping (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void, isDetailMode: Bool = false, isLikeLoading: Bool = false) {
self.moment = moment self.moment = moment
self.allMoments = allMoments self.allMoments = allMoments
self.currentIndex = currentIndex self.currentIndex = currentIndex
self.onImageTap = onImageTap self.onImageTap = onImageTap
self.onLikeTap = onLikeTap self.onLikeTap = onLikeTap
self.isLikeLoading = isLikeLoading
self.isDetailMode = isDetailMode self.isDetailMode = isDetailMode
self.isLikeLoading = isLikeLoading
} }
public var body: some View { public var body: some View {
@@ -101,7 +101,9 @@ struct OptimizedDynamicCardView: View {
HStack(spacing: 20) { HStack(spacing: 20) {
// Like // Like
Button(action: { Button(action: {
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId) if !isLikeLoading {
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId)
}
}) { }) {
HStack(spacing: 4) { HStack(spacing: 4) {
if isLikeLoading { if isLikeLoading {

View File

@@ -52,8 +52,8 @@ struct DetailView: View {
onLikeTap: { dynamicId, uid, likedUid, worldId in onLikeTap: { dynamicId, uid, likedUid, worldId in
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId)) store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
}, },
isLikeLoading: store.isLikeLoading, isDetailMode: true, //
isDetailMode: true // isLikeLoading: store.isLikeLoading
) )
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.top, 16) .padding(.top, 16)

View File

@@ -254,6 +254,9 @@ private struct LoginContentView: View {
} }
Spacer() Spacer()
} }
// API Loading
APILoadingEffectView()
} }
} }
} }

View File

@@ -95,8 +95,8 @@ struct MomentCardView: View {
currentIndex: index, currentIndex: index,
onImageTap: onImageTap, onImageTap: onImageTap,
onLikeTap: onLikeTap, onLikeTap: onLikeTap,
isLikeLoading: isLikeLoading, isDetailMode: false,
isDetailMode: false isLikeLoading: isLikeLoading
) )
.onTapGesture { .onTapGesture {
onTap() onTap()
@@ -244,6 +244,9 @@ struct FeedListView: View {
} }
.frame(maxWidth: .infinity, alignment: .top) .frame(maxWidth: .infinity, alignment: .top)
} }
// API Loading
APILoadingEffectView()
} }
.onAppear { .onAppear {
viewStore.send(.onAppear) viewStore.send(.onAppear)

View File

@@ -112,77 +112,75 @@ struct IDLoginView: View {
Button(action: { Button(action: {
isPasswordVisible.toggle() isPasswordVisible.toggle()
}) { }) {
Image(systemName: isPasswordVisible ? "eye.slash" : "eye") Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
.foregroundColor(.white.opacity(0.7)) .foregroundColor(.white.opacity(0.6))
.font(.system(size: 18)) .font(.system(size: 16))
} }
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
} }
}
.padding(.horizontal, 32) //
HStack {
// Forgot Password Spacer()
HStack { Button(action: {
Spacer() showRecoverPassword = true
Button(action: { }) {
showRecoverPassword = true Text(NSLocalizedString("id_login.forgot_password", comment: ""))
}) { .font(.system(size: 14))
Text(NSLocalizedString("id_login.forgot_password", comment: "")) .foregroundColor(.white.opacity(0.8))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
}
.padding(.horizontal, 32)
.padding(.top, 16)
Spacer()
.frame(height: 60)
//
Button(action: {
// action
store.send(.loginButtonTapped(userID: userID, password: password))
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? NSLocalizedString("id_login.logging_in", comment: "") : NSLocalizedString("id_login.login_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
} }
} }
.frame(height: 56) .padding(.horizontal, 8)
}
.disabled(store.isLoading || userID.isEmpty || password.isEmpty) //
.opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 50% Button(action: {
.padding(.horizontal, 32) // action
store.send(.loginButtonTapped(userID: userID, password: password))
// }) {
if let errorMessage = store.errorMessage { ZStack {
Text(errorMessage) //
.font(.system(size: 14)) LinearGradient(
.foregroundColor(.red) colors: [
.padding(.top, 16) Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
.padding(.horizontal, 32) Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? NSLocalizedString("id_login.logging_in", comment: "") : NSLocalizedString("id_login.login_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || userID.isEmpty || password.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 50%
.padding(.horizontal, 32)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
} }
Spacer() // API Loading
APILoadingEffectView()
} }
} }
} }

View File

@@ -99,8 +99,7 @@ struct LoginView: View {
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in .onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
topImageHeight = imageHeight topImageHeight = imageHeight
} }
// 使"top"40pt
Spacer() Spacer()
.frame(height: 120) .frame(height: 120)
@@ -118,6 +117,9 @@ struct LoginView: View {
.padding(.bottom, 140) .padding(.bottom, 140)
} }
// API Loading
APILoadingEffectView()
// NavigationLink navigationDestination // NavigationLink navigationDestination
} }
} }

View File

@@ -98,7 +98,9 @@ struct InternalMainView: View {
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) } send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
)) ))
} }
.padding(.bottom, geometry.safeAreaInsets.bottom + 60)
// API Loading
APILoadingEffectView()
} }
} }
} }

View File

@@ -6,43 +6,48 @@ struct SplashView: View {
var body: some View { var body: some View {
WithPerceptionTracking { WithPerceptionTracking {
Group { ZStack {
// Group {
if let navigationDestination = store.navigationDestination { //
switch navigationDestination { if let navigationDestination = store.navigationDestination {
case .login: switch navigationDestination {
// case .login:
LoginView( //
store: Store( LoginView(
initialState: LoginFeature.State() store: Store(
) { initialState: LoginFeature.State()
LoginFeature() ) {
}, LoginFeature()
onLoginSuccess: { },
// onLoginSuccess: {
store.send(.navigateToMain) //
} store.send(.navigateToMain)
) }
case .main: )
// case .main:
MainView( //
store: Store( MainView(
initialState: MainFeature.State() store: Store(
) { initialState: MainFeature.State()
MainFeature() ) {
}, MainFeature()
onLogout: { },
store.send(.navigateToLogin) onLogout: {
} store.send(.navigateToLogin)
) }
)
}
} else {
//
splashContent
} }
} else {
//
splashContent
} }
} .onAppear {
.onAppear { store.send(.onAppear)
store.send(.onAppear) }
// API Loading -
APILoadingEffectView()
} }
} }
} }