From 8e444f7434a3e04df56bda497959abdb8562f1b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Fri, 14 Aug 2026 16:46:05 +0800 Subject: [PATCH] fix: refresh travel albums after OTG return --- .../TravelAlbumDetailViewController.swift | 19 +++++++- .../TravelAlbumEntryViewController.swift | 46 ++++++++++++++++++- .../TravelAlbumViewModelTests.swift | 13 ++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift b/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift index e22ed51..26d91ef 100644 --- a/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift +++ b/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift @@ -31,6 +31,7 @@ final class TravelAlbumDetailViewController: BaseViewController { private let aiRetouchButton = UIButton(type: .system) private let deleteSelectedButton = UIButton(type: .system) private let uploadButton = UIButton(type: .system) + private var refreshState = TravelAlbumReturnRefreshState() init( albumId: Int, @@ -262,6 +263,18 @@ final class TravelAlbumDetailViewController: BaseViewController { Task { await viewModel.refreshAll(api: api) } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + guard refreshState.beginRefreshIfNeeded() else { return } + Task { + await viewModel.refreshAll(api: api) + await MainActor.run { + self.applyViewModel() + self.refreshState.finishRefresh() + } + } + } + @MainActor private func applyViewModel() { if let album = viewModel.album { @@ -306,7 +319,10 @@ final class TravelAlbumDetailViewController: BaseViewController { if !snapshot.itemIdentifiers.isEmpty { snapshot.reconfigureItems(snapshot.itemIdentifiers) } - dataSource.apply(snapshot, animatingDifferences: true) + dataSource.apply( + snapshot, + animatingDifferences: !refreshState.suppressesSnapshotAnimations + ) if !viewModel.isRefreshing { refreshControl.endRefreshing() @@ -482,6 +498,7 @@ final class TravelAlbumDetailViewController: BaseViewController { @objc private func uploadTapped() { let album = viewModel.album + refreshState.markRefreshNeeded() let controller = WiredCameraTransferViewController( viewModel: WiredCameraTransferViewModel( albumId: album?.id ?? viewModel.albumId, diff --git a/suixinkan/UI/TravelAlbum/TravelAlbumEntryViewController.swift b/suixinkan/UI/TravelAlbum/TravelAlbumEntryViewController.swift index 0f8eaee..e576bf5 100644 --- a/suixinkan/UI/TravelAlbum/TravelAlbumEntryViewController.swift +++ b/suixinkan/UI/TravelAlbum/TravelAlbumEntryViewController.swift @@ -7,6 +7,27 @@ import Kingfisher import SnapKit import UIKit +/// 相册页面的一次性返回刷新状态,避免初次展示或重复出现时多发列表请求。 +struct TravelAlbumReturnRefreshState { + private var needsRefresh = false + private(set) var suppressesSnapshotAnimations = false + + mutating func markRefreshNeeded() { + needsRefresh = true + } + + mutating func beginRefreshIfNeeded() -> Bool { + guard needsRefresh else { return false } + needsRefresh = false + suppressesSnapshotAnimations = true + return true + } + + mutating func finishRefresh() { + suppressesSnapshotAnimations = false + } +} + /// 新增相册入口页,对齐 Android `TravelAlbumEntryScreen`。 final class TravelAlbumEntryViewController: BaseViewController { private let viewModel = TravelAlbumEntryViewModel() @@ -17,6 +38,7 @@ final class TravelAlbumEntryViewController: BaseViewController { private let tableView = UITableView(frame: .zero, style: .plain) private let emptyView = TravelAlbumEmptyView() private var dataSource: UITableViewDiffableDataSource! + private var refreshState = TravelAlbumReturnRefreshState() init(api: (any TravelAlbumServing)? = nil) { self.api = api ?? NetworkServices.shared.travelAlbumAPI @@ -99,6 +121,18 @@ final class TravelAlbumEntryViewController: BaseViewController { Task { await viewModel.loadAlbums(api: api) } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + guard refreshState.beginRefreshIfNeeded() else { return } + Task { + await viewModel.loadAlbums(api: api) + await MainActor.run { + self.applyViewModel() + self.refreshState.finishRefresh() + } + } + } + @MainActor private func applyViewModel() { heroCard.isLoading = viewModel.isCreating @@ -109,7 +143,10 @@ final class TravelAlbumEntryViewController: BaseViewController { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) snapshot.appendItems(viewModel.albums) - dataSource.apply(snapshot, animatingDifferences: true) + dataSource.apply( + snapshot, + animatingDifferences: !refreshState.suppressesSnapshotAnimations + ) if viewModel.isLoading && viewModel.albums.isEmpty { showLoading() } else { @@ -162,6 +199,7 @@ final class TravelAlbumEntryViewController: BaseViewController { } private func pushWiredTransfer(album: TravelAlbum) { + refreshState.markRefreshNeeded() let controller = WiredCameraTransferViewController( viewModel: WiredCameraTransferViewModel( albumId: album.id, @@ -177,7 +215,11 @@ extension TravelAlbumEntryViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let album = dataSource.itemIdentifier(for: indexPath) else { return } - navigationController?.pushViewController(TravelAlbumDetailViewController(albumId: album.id), animated: true) + refreshState.markRefreshNeeded() + navigationController?.pushViewController( + TravelAlbumDetailViewController(albumId: album.id, api: api), + animated: true + ) } } diff --git a/suixinkanTests/TravelAlbumViewModelTests.swift b/suixinkanTests/TravelAlbumViewModelTests.swift index 357d744..5d4677b 100644 --- a/suixinkanTests/TravelAlbumViewModelTests.swift +++ b/suixinkanTests/TravelAlbumViewModelTests.swift @@ -99,6 +99,19 @@ final class TravelAlbumAIJobViewModelTests: XCTestCase { @MainActor /// 旅拍相册 ViewModel 测试。 final class TravelAlbumEntryViewModelTests: XCTestCase { + func testReturnRefreshStateSuppressesAnimationUntilRefreshFinishes() { + var state = TravelAlbumReturnRefreshState() + + XCTAssertFalse(state.beginRefreshIfNeeded()) + XCTAssertFalse(state.suppressesSnapshotAnimations) + state.markRefreshNeeded() + XCTAssertTrue(state.beginRefreshIfNeeded()) + XCTAssertTrue(state.suppressesSnapshotAnimations) + state.finishRefresh() + XCTAssertFalse(state.suppressesSnapshotAnimations) + XCTAssertFalse(state.beginRefreshIfNeeded()) + } + func testLoadAlbumsUpdatesListAndTotal() async { let api = TravelAlbumMockAPI() api.listResponse = TravelAlbumListResponse(total: 1, list: [TravelAlbum(id: 1, name: "A")])