fix: refresh travel albums after OTG return

This commit is contained in:
2026-08-14 16:46:05 +08:00
parent 83afcb6deb
commit 8e444f7434
3 changed files with 75 additions and 3 deletions
@@ -31,6 +31,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
private let aiRetouchButton = UIButton(type: .system) private let aiRetouchButton = UIButton(type: .system)
private let deleteSelectedButton = UIButton(type: .system) private let deleteSelectedButton = UIButton(type: .system)
private let uploadButton = UIButton(type: .system) private let uploadButton = UIButton(type: .system)
private var refreshState = TravelAlbumReturnRefreshState()
init( init(
albumId: Int, albumId: Int,
@@ -262,6 +263,18 @@ final class TravelAlbumDetailViewController: BaseViewController {
Task { await viewModel.refreshAll(api: api) } 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 @MainActor
private func applyViewModel() { private func applyViewModel() {
if let album = viewModel.album { if let album = viewModel.album {
@@ -306,7 +319,10 @@ final class TravelAlbumDetailViewController: BaseViewController {
if !snapshot.itemIdentifiers.isEmpty { if !snapshot.itemIdentifiers.isEmpty {
snapshot.reconfigureItems(snapshot.itemIdentifiers) snapshot.reconfigureItems(snapshot.itemIdentifiers)
} }
dataSource.apply(snapshot, animatingDifferences: true) dataSource.apply(
snapshot,
animatingDifferences: !refreshState.suppressesSnapshotAnimations
)
if !viewModel.isRefreshing { if !viewModel.isRefreshing {
refreshControl.endRefreshing() refreshControl.endRefreshing()
@@ -482,6 +498,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
@objc private func uploadTapped() { @objc private func uploadTapped() {
let album = viewModel.album let album = viewModel.album
refreshState.markRefreshNeeded()
let controller = WiredCameraTransferViewController( let controller = WiredCameraTransferViewController(
viewModel: WiredCameraTransferViewModel( viewModel: WiredCameraTransferViewModel(
albumId: album?.id ?? viewModel.albumId, albumId: album?.id ?? viewModel.albumId,
@@ -7,6 +7,27 @@ import Kingfisher
import SnapKit import SnapKit
import UIKit 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`。 /// 新增相册入口页,对齐 Android `TravelAlbumEntryScreen`。
final class TravelAlbumEntryViewController: BaseViewController { final class TravelAlbumEntryViewController: BaseViewController {
private let viewModel = TravelAlbumEntryViewModel() private let viewModel = TravelAlbumEntryViewModel()
@@ -17,6 +38,7 @@ final class TravelAlbumEntryViewController: BaseViewController {
private let tableView = UITableView(frame: .zero, style: .plain) private let tableView = UITableView(frame: .zero, style: .plain)
private let emptyView = TravelAlbumEmptyView() private let emptyView = TravelAlbumEmptyView()
private var dataSource: UITableViewDiffableDataSource<Int, TravelAlbum>! private var dataSource: UITableViewDiffableDataSource<Int, TravelAlbum>!
private var refreshState = TravelAlbumReturnRefreshState()
init(api: (any TravelAlbumServing)? = nil) { init(api: (any TravelAlbumServing)? = nil) {
self.api = api ?? NetworkServices.shared.travelAlbumAPI self.api = api ?? NetworkServices.shared.travelAlbumAPI
@@ -99,6 +121,18 @@ final class TravelAlbumEntryViewController: BaseViewController {
Task { await viewModel.loadAlbums(api: api) } 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 @MainActor
private func applyViewModel() { private func applyViewModel() {
heroCard.isLoading = viewModel.isCreating heroCard.isLoading = viewModel.isCreating
@@ -109,7 +143,10 @@ final class TravelAlbumEntryViewController: BaseViewController {
var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbum>() var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbum>()
snapshot.appendSections([0]) snapshot.appendSections([0])
snapshot.appendItems(viewModel.albums) snapshot.appendItems(viewModel.albums)
dataSource.apply(snapshot, animatingDifferences: true) dataSource.apply(
snapshot,
animatingDifferences: !refreshState.suppressesSnapshotAnimations
)
if viewModel.isLoading && viewModel.albums.isEmpty { if viewModel.isLoading && viewModel.albums.isEmpty {
showLoading() showLoading()
} else { } else {
@@ -162,6 +199,7 @@ final class TravelAlbumEntryViewController: BaseViewController {
} }
private func pushWiredTransfer(album: TravelAlbum) { private func pushWiredTransfer(album: TravelAlbum) {
refreshState.markRefreshNeeded()
let controller = WiredCameraTransferViewController( let controller = WiredCameraTransferViewController(
viewModel: WiredCameraTransferViewModel( viewModel: WiredCameraTransferViewModel(
albumId: album.id, albumId: album.id,
@@ -177,7 +215,11 @@ extension TravelAlbumEntryViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
guard let album = dataSource.itemIdentifier(for: indexPath) else { return } 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
)
} }
} }
@@ -99,6 +99,19 @@ final class TravelAlbumAIJobViewModelTests: XCTestCase {
@MainActor @MainActor
/// 旅拍相册 ViewModel 测试。 /// 旅拍相册 ViewModel 测试。
final class TravelAlbumEntryViewModelTests: XCTestCase { 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 { func testLoadAlbumsUpdatesListAndTotal() async {
let api = TravelAlbumMockAPI() let api = TravelAlbumMockAPI()
api.listResponse = TravelAlbumListResponse(total: 1, list: [TravelAlbum(id: 1, name: "A")]) api.listResponse = TravelAlbumListResponse(total: 1, list: [TravelAlbum(id: 1, name: "A")])