feat: add pull to refresh
This commit is contained in:
parent
3e1d2bcc16
commit
29439c9746
@ -16,6 +16,7 @@
|
|||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
||||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
|
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
|
||||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
|
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
|
||||||
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; };
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
|
||||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; };
|
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; };
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; };
|
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; };
|
||||||
@ -153,6 +154,7 @@
|
|||||||
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
||||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
||||||
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
|
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
|
||||||
|
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
|
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
|
||||||
2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = "<group>"; };
|
2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = "<group>"; };
|
||||||
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
||||||
@ -357,6 +359,7 @@
|
|||||||
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */,
|
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */,
|
||||||
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */,
|
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */,
|
||||||
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */,
|
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */,
|
||||||
|
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */,
|
||||||
);
|
);
|
||||||
path = PublicTimeline;
|
path = PublicTimeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1047,6 +1050,7 @@
|
|||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||||
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
||||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
|
@ -5,22 +5,22 @@
|
|||||||
// Created by sxiaojian on 2021/1/27.
|
// Created by sxiaojian on 2021/1/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
import AVKit
|
import AVKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
final class PublicTimelineViewController: UIViewController, NeedsDependency, TimelinePostTableViewCellDelegate {
|
final class PublicTimelineViewController: UIViewController, NeedsDependency, TimelinePostTableViewCellDelegate {
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
var viewModel: PublicTimelineViewModel!
|
var viewModel: PublicTimelineViewModel!
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
lazy var tableView: UITableView = {
|
lazy var tableView: UITableView = {
|
||||||
let tableView = UITableView()
|
let tableView = UITableView()
|
||||||
tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self))
|
tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self))
|
||||||
@ -30,16 +30,30 @@ final class PublicTimelineViewController: UIViewController, NeedsDependency, Tim
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PublicTimelineViewController {
|
extension PublicTimelineViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(PublicTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
// bind refresh control
|
||||||
|
viewModel.isFetchingLatestTimeline
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isFetching in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !isFetching {
|
||||||
|
UIView.animate(withDuration: 0.5) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
tableView.backgroundColor = Asset.Colors.tootDark.color
|
tableView.backgroundColor = Asset.Colors.tootDark.color
|
||||||
view.addSubview(tableView)
|
view.addSubview(tableView)
|
||||||
@ -60,6 +74,7 @@ extension PublicTimelineViewController {
|
|||||||
timelinePostTableViewCellDelegate: self
|
timelinePostTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
viewModel.fetchLatest()
|
viewModel.fetchLatest()
|
||||||
@ -67,7 +82,7 @@ extension PublicTimelineViewController {
|
|||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: fetch user timeline latest response error: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: fetch user timeline latest response error: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -77,12 +92,22 @@ extension PublicTimelineViewController {
|
|||||||
}
|
}
|
||||||
.store(in: &viewModel.disposeBag)
|
.store(in: &viewModel.disposeBag)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Selector
|
||||||
|
|
||||||
|
extension PublicTimelineViewController {
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
guard viewModel.stateMachine.enter(PublicTimelineViewModel.State.Loading.self) else {
|
||||||
|
sender.endRefreshing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
extension PublicTimelineViewController: UITableViewDelegate {
|
extension PublicTimelineViewController: UITableViewDelegate {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
||||||
@ -94,12 +119,9 @@ extension PublicTimelineViewController: UITableViewDelegate {
|
|||||||
return ceil(frame.height)
|
return ceil(frame.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
// Created by sxiaojian on 2021/1/27.
|
// Created by sxiaojian on 2021/1/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
extension PublicTimelineViewModel {
|
extension PublicTimelineViewModel {
|
||||||
func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
@ -20,26 +20,27 @@ extension PublicTimelineViewModel {
|
|||||||
.autoconnect()
|
.autoconnect()
|
||||||
.share()
|
.share()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
diffableDataSource = TimelineSection.tableViewDiffableDataSource(
|
diffableDataSource = TimelineSection.tableViewDiffableDataSource(
|
||||||
for: tableView,
|
for: tableView,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate)
|
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate
|
||||||
|
)
|
||||||
items.value = []
|
items.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - NSFetchedResultsControllerDelegate
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
|
||||||
extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
let indexes = tootIDs.value
|
let indexes = tootIDs.value
|
||||||
let toots = fetchedResultsController.fetchedObjects ?? []
|
let toots = fetchedResultsController.fetchedObjects ?? []
|
||||||
guard toots.count == indexes.count else { return }
|
|
||||||
|
|
||||||
let items: [Item] = toots
|
let items: [Item] = toots
|
||||||
.compactMap { toot -> (Int, Toot)? in
|
.compactMap { toot -> (Int, Toot)? in
|
||||||
guard toot.deletedAt == nil else { return nil }
|
guard toot.deletedAt == nil else { return nil }
|
||||||
@ -49,5 +50,4 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||||||
.map { Item.toot(objectID: $0.1.objectID) }
|
.map { Item.toot(objectID: $0.1.objectID) }
|
||||||
self.items.value = items
|
self.items.value = items
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
//
|
||||||
|
// PublicTimelineViewModel+State.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/2.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
extension PublicTimelineViewModel {
|
||||||
|
class State: GKState {
|
||||||
|
weak var viewModel: PublicTimelineViewModel?
|
||||||
|
|
||||||
|
init(viewModel: PublicTimelineViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PublicTimelineViewModel.State {
|
||||||
|
class Initial: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
viewModel.fetchLatest()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: fetch user timeline latest response error: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
|
let tootsIDs = response.value.map { $0.id }
|
||||||
|
viewModel.tootIDs.value = tootsIDs
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type, is LoadingMore.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type, is LoadingMore.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingMore: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
viewModel.loadMore()
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: load more fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
|
let tootsIDs = response.value.map { $0.id }
|
||||||
|
viewModel.tootIDs.value = tootsIDs
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,28 +5,39 @@
|
|||||||
// Created by sxiaojian on 2021/1/27.
|
// Created by sxiaojian on 2021/1/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
import AlamofireImage
|
||||||
import UIKit
|
|
||||||
import GameplayKit
|
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import AlamofireImage
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
class PublicTimelineViewModel: NSObject {
|
class PublicTimelineViewModel: NSObject {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let fetchedResultsController: NSFetchedResultsController<Toot>
|
let fetchedResultsController: NSFetchedResultsController<Toot>
|
||||||
|
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||||
weak var tableView: UITableView?
|
weak var tableView: UITableView?
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
||||||
|
|
||||||
|
lazy var stateMachine: GKStateMachine = {
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
State.Initial(viewModel: self),
|
||||||
|
State.Loading(viewModel: self),
|
||||||
|
State.Fail(viewModel: self),
|
||||||
|
State.Idle(viewModel: self),
|
||||||
|
State.LoadingMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(State.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
let tootIDs = CurrentValueSubject<[String], Never>([])
|
let tootIDs = CurrentValueSubject<[String], Never>([])
|
||||||
let items = CurrentValueSubject<[Item], Never>([])
|
let items = CurrentValueSubject<[Item], Never>([])
|
||||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
@ -49,7 +60,7 @@ class PublicTimelineViewModel: NSObject {
|
|||||||
}()
|
}()
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.fetchedResultsController.delegate = self
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
items
|
items
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
@ -57,7 +68,7 @@ class PublicTimelineViewModel: NSObject {
|
|||||||
.sink { [weak self] items in
|
.sink { [weak self] items in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: items did change", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>()
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
@ -82,14 +93,16 @@ class PublicTimelineViewModel: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PublicTimelineViewModel {
|
extension PublicTimelineViewModel {
|
||||||
|
|
||||||
func fetchLatest() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
func fetchLatest() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||||
return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
|
return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadMore() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||||
|
return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user