fix: refresh travel albums after OTG return
This commit is contained in:
@@ -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")])
|
||||||
|
|||||||
Reference in New Issue
Block a user